From 45f27fabfd2f4dc8d2bfb83bcc838a661cfb6d44 Mon Sep 17 00:00:00 2001 From: damp11113 Date: Sun, 9 Jun 2024 14:21:40 +0700 Subject: [PATCH] new xHE-Opus v2 new xHE-Opus v2 with parametric stereo --- createnewformat.reg | Bin 1086 -> 1210 bytes gui.py | 42 ++++- icon.ico | Bin 0 -> 214078 bytes icon.png | Bin 0 -> 13304 bytes icon.svg | 280 +++++++++++++++++++++++++++++ libxheopus.py | 425 +++++++++++++++++++++++++++++++++++++++++--- player.py | 4 +- realtime.py | 79 ++++++++ 8 files changed, 803 insertions(+), 27 deletions(-) create mode 100644 icon.ico create mode 100644 icon.png create mode 100644 icon.svg create mode 100644 realtime.py diff --git a/createnewformat.reg b/createnewformat.reg index a5c57da3f0bf563790aa23f4a4f275fbdc358b7d..5945a01281b38062a2f33473e15405cb75141dc5 100644 GIT binary patch delta 36 ocmdnTv5Rwq4b$XR%sKp-49N`n40#NC3}Dt|MdskmeM}n|0mlLgLjV8( delta 16 YcmdnRxsPLm4b$X#%t4#Em^Uy205cc`ZvX%Q diff --git a/gui.py b/gui.py index acc3c18..aa72ab9 100644 --- a/gui.py +++ b/gui.py @@ -47,6 +47,14 @@ class App: if int(dpg.get_value("opusframesize")) > 60: dpg.configure_item("opusframesize", default_value="60") + def changeprofileopus(self, sender, data): + if data == "xHE-Opus v2": + dpg.configure_item("opusstereomode", show=False) + dpg.configure_item("opusbitrate", min_value=2.5, max_value=510, min_clamped=True, max_clamped=True, step_fast=1, default_value=64) + else: + dpg.configure_item("opusstereomode", show=True) + dpg.configure_item("opusbitrate", min_value=5, max_value=1020, min_clamped=True, max_clamped=True, step_fast=1, default_value=64) + def selectdeoutputpath(self, sender, data): file_path = easygui.diropenbox() dpg.set_value("deoutpathshow", f"output: {file_path}") @@ -80,6 +88,8 @@ class App: dpg.configure_item("deplayconvert", show=False) def convert(self): + signaltype = str(dpg.get_value("opussignaltype")).lower() + profile = str(dpg.get_value("opusprofile")).strip().lower() stereomode = str(dpg.get_value("opusstereomode")).lower() if stereomode == "stereo l/r": stereomode = 1 @@ -88,20 +98,40 @@ class App: else: stereomode = 2 + if signaltype == "music": + signalauto = False + signalvoice = False + elif signaltype == "voice": + signalauto = False + signalvoice = True + else: + signalauto = True + signalvoice = False + try: total = 0 current = 0 filename = os.path.splitext(os.path.basename(self.inputfilepath))[0] dpg.set_value("convertstatus", "init encoder...") - encoder = libxheopus.DualOpusEncoder(dpg.get_value("opusapp"), 48000, dpg.get_value("opusversion")) + print(profile) + if profile == "xhe-opus v1": + encoder = libxheopus.DualOpusEncoder(dpg.get_value("opusapp"), 48000, dpg.get_value("opusversion")) + else: + encoder = libxheopus.PSOpusEncoder(dpg.get_value("opusapp"), 48000, dpg.get_value("opusversion")) + encoder.set_bitrate_mode(dpg.get_value("opusbitmode")) encoder.set_bandwidth(dpg.get_value("opusbandwidth")) encoder.set_bitrates(int(dpg.get_value("opusbitrate")*1000)) encoder.set_compression(dpg.get_value("opuscompression")) encoder.set_packet_loss(dpg.get_value("opuspacketloss")) - encoder.set_stereo_mode(stereomode, dpg.get_value("opusenajoint")) + + if profile != "xhe-opus v2": + encoder.set_stereo_mode(stereomode, dpg.get_value("opusenajoint")) + encoder.set_feature(dpg.get_value("opusenapred"), False, dpg.get_value("opusenadtx")) + encoder.enable_voice_mode(signalvoice, signalauto) + desired_frame_size = encoder.set_frame_size(int(dpg.get_value("opusframesize"))) dpg.set_value("convertstatus", "init writer...") @@ -168,7 +198,7 @@ class App: self.delen = metadata["footer"]["length"] def playaudiothread(self): - self.decoder = libxheopus.DualOpusDecoder() + self.decoder = libxheopus.xOpusDecoder() for data in self.derender.decode(self.decoder, True, self.depausepos): if self.deplay: @@ -229,7 +259,7 @@ class App: outwav.setsampwidth(2) # 2 bytes (16 bits) per sample outwav.setframerate(48000) - self.decoder = libxheopus.DualOpusDecoder() + self.decoder = libxheopus.xOpusDecoder() for data in self.derender.decode(self.decoder, True, self.depausepos): self.decurrentplay += 1 @@ -260,18 +290,20 @@ class App: dpg.add_text("output:", tag="outpathshow") dpg.add_button(label="Select Input File", callback=self.selectinputfile) dpg.add_button(label="Select Output Path", callback=self.selectoutputpath) + dpg.add_combo(["xHE-Opus v1", "xHE-Opus v2"], label="Profile", default_value="xHE-Opus v1", tag="opusprofile", callback=self.changeprofileopus) dpg.add_combo(["hev2", "exper", "stable", "old"], label="Version", default_value="hev2", tag="opusversion", callback=self.changeversionopus) dpg.add_combo(["120", "100", "80", "60", "40", "20", "10", "5"], label="Frame Size (ms)", tag="opusframesize", default_value="120") dpg.add_combo(["voip", "audio", "restricted_lowdelay"], label="Application", default_value="restricted_lowdelay", tag="opusapp") dpg.add_combo(["VBR", "CVBR", "CBR"], label="Bitrate Mode", default_value="CVBR", tag="opusbitmode") dpg.add_combo(["auto", "fullband", "superwideband", "wideband", "mediumband", "narrowband"], label="Bandwidth", tag="opusbandwidth", default_value="fullband") dpg.add_combo(["Stereo L/R", "Stereo Mid/Side"], label="Stereo Mode", tag="opusstereomode", default_value="Stereo L/R") + dpg.add_combo(["Auto", "Voice", "Music"], label="Signal Type", tag="opussignaltype", default_value="Auto") dpg.add_input_float(label="Bitrates", min_value=5, max_value=1020, min_clamped=True, max_clamped=True, step_fast=1, default_value=64, tag="opusbitrate") dpg.add_input_int(label="Compression Level", max_clamped=True, min_clamped=True, min_value=0, max_value=10, default_value=10, tag="opuscompression") dpg.add_input_int(label="Packet Loss", max_clamped=True, min_clamped=True, min_value=0, max_value=100, default_value=0, tag="opuspacketloss") dpg.add_checkbox(label="Prediction", tag="opusenapred") dpg.add_checkbox(label="DTX", tag="opusenadtx") - dpg.add_checkbox(label="Joint", tag="opusenajoint") + dpg.add_checkbox(label="Auto Mono (Mid/Side Encoding)", tag="opusenajoint") dpg.add_button(label="Convert", callback=self.startconvert) with dpg.window(label="converting", show=False, tag="convertingwindow", modal=True, no_resize=True, no_move=True, no_title_bar=True, width=320): diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3227ccc62a24f700533e292c5eec443438d645c GIT binary patch literal 214078 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
M4Fom_B6n)G2;lhAz(e*`&j59 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 literal 0 HcmV?d00001 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..2a5096c --- /dev/null +++ b/icon.svg @@ -0,0 +1,280 @@ + + + + + + + + + + diff --git a/libxheopus.py b/libxheopus.py index 8196f40..e2abc8b 100644 --- a/libxheopus.py +++ b/libxheopus.py @@ -4,6 +4,7 @@ import struct import pyogg import os import numpy as np +from scipy.signal import butter, filtfilt def float32_to_int16(data_float32): data_int16 = (data_float32 * 32767).astype(np.int16) @@ -51,7 +52,8 @@ class DualOpusEncoder: self.version = version self.samplerate = samplerate self.stereomode = 1 #0 = mono, 1 = stereo LR, 2 = stereo Mid/Side - self.enablejoint = False + self.audiomono = False + os.environ["pyogg_win_libopus_version"] = version importlib.reload(pyogg.opus) @@ -97,20 +99,25 @@ class DualOpusEncoder: """ narrowband: Narrowband typically refers to a limited range of frequencies suitable for voice communication. + mediumband (unsupported in libopus 1.3+): Mediumband extends the frequency range compared to narrowband, providing better audio quality. + wideband: Wideband offers an even broader frequency range, resulting in higher audio fidelity compared to narrowband and mediumband. + superwideband: Superwideband extends the frequency range beyond wideband, further enhancing audio quality. + fullband (default): Fullband provides the widest frequency range among the listed options, offering the highest audio quality. + auto: opus is working auto not force """ self.Lencoder.set_bandwidth(bandwidth) self.Rencoder.set_bandwidth(bandwidth) - def set_stereo_mode(self, mode=1, enablejoint=False): + def set_stereo_mode(self, mode=1, audiomono=False): """ 0 = mono 1 = stereo LR @@ -120,7 +127,7 @@ class DualOpusEncoder: mode = 1 self.stereomode = mode - self.enablejoint = enablejoint + self.audiomono = audiomono def set_frame_size(self, size=60): """ Set the desired frame duration (in milliseconds). @@ -162,6 +169,10 @@ class DualOpusEncoder: self.Rencoder.CTL(pyogg.opus.OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, int(phaseinvert)) self.Rencoder.CTL(pyogg.opus.OPUS_SET_DTX_REQUEST, int(DTX)) + def enable_voice_mode(self, enable=True, auto=False): + self.Lencoder.enable_voice_enhance(enable, auto) + self.Rencoder.enable_voice_enhance(enable, auto) + def encode(self, pcmbytes, directpcm=False): """input: pcm bytes accept float32/int16 only x74 is mono @@ -188,9 +199,9 @@ class DualOpusEncoder: intmono = float32_to_int16(mono) - Mencoded_packet = self.Lencoder.buffered_encode(memoryview(bytearray(intmono)), flush=True)[0][0].tobytes() + midencoded_packet = self.Lencoder.buffered_encode(memoryview(bytearray(intmono)), flush=True)[0][0].tobytes() - dual_encoded_packet = (Mencoded_packet + b'\\x64\\x74') + dual_encoded_packet = (midencoded_packet + b'\\x64\\x74') elif self.stereomode == 2: # stereo mid/side (Joint encoding) # convert to float32 @@ -214,7 +225,7 @@ class DualOpusEncoder: except: loudnessside = 0 - if (loudnessside) <= -50 and self.enablejoint: + if (loudnessside) <= -50 and self.audiomono: sideencoded_packet = b"\\xnl" else: sideencoded_packet = self.Rencoder.buffered_encode(memoryview(bytearray(intside)), flush=True)[0][0].tobytes() @@ -232,7 +243,194 @@ class DualOpusEncoder: return dual_encoded_packet -class DualOpusDecoder: +class PSOpusEncoder: + def __init__(self, app="audio", samplerate=48000, version="stable"): + """ + This version is xHE-Opus v2 (Parametric Stereo) + ----------------------------- version-------------------------- + hev2: libopus 1.5.1 (fre:ac) + exper: libopus 1.5.1 + stable: libopus 1.4 + old: libopus 1.3.1 + custom: custom opus path you can use "pyogg_win_libopus_custom_path" env to change opus version (windows only) + ------------------------- App---------------------------------- + + Set the encoding mode. + + This must be one of 'voip', 'audio', or 'restricted_lowdelay'. + + 'voip': Gives best quality at a given bitrate for voice + signals. It enhances the input signal by high-pass + filtering and emphasizing formants and + harmonics. Optionally it includes in-band forward error + correction to protect against packet loss. Use this mode + for typical VoIP applications. Because of the enhancement, + even at high bitrates the output may sound different from + the input. + + 'audio': Gives best quality at a given bitrate for most + non-voice signals like music. Use this mode for music and + mixed (music/voice) content, broadcast, and applications + requiring less than 15 ms of coding delay. + + 'restricted_lowdelay': configures low-delay mode that + disables the speech-optimized mode in exchange for + slightly reduced delay. This mode can only be set on an + newly initialized encoder because it changes the codec + delay. + """ + self.version = version + self.samplerate = samplerate + + os.environ["pyogg_win_libopus_version"] = version + importlib.reload(pyogg.opus) + + self.encoder = pyogg.OpusBufferedEncoder() + + self.encoder.set_application(app) + + self.encoder.set_sampling_frequency(samplerate) + + self.encoder.set_channels(1) + + self.set_frame_size() + self.set_compression() + self.set_feature() + self.set_bitrate_mode() + self.set_bitrates() + self.set_bandwidth() + self.set_packet_loss() + + def set_compression(self, level=10): + """complex 0-10 low-hires""" + self.encoder.set_compresion_complex(level) + + def set_bitrates(self, bitrates=64000): + """input birate unit: bps""" + if bitrates <= 2500: + bitrates = 2500 + + self.encoder.set_bitrates(bitrates) + + def set_bandwidth(self, bandwidth="fullband"): + """ + narrowband: + Narrowband typically refers to a limited range of frequencies suitable for voice communication. + + mediumband (unsupported in libopus 1.3+): + Mediumband extends the frequency range compared to narrowband, providing better audio quality. + + wideband: + Wideband offers an even broader frequency range, resulting in higher audio fidelity compared to narrowband and mediumband. + + superwideband: + Superwideband extends the frequency range beyond wideband, further enhancing audio quality. + + fullband (default): + Fullband provides the widest frequency range among the listed options, offering the highest audio quality. + + auto: opus is working auto not force + """ + self.encoder.set_bandwidth(bandwidth) + + def set_frame_size(self, size=60): + """ Set the desired frame duration (in milliseconds). + Valid options are 2.5, 5, 10, 20, 40, or 60ms. + Exclusive for HE opus v2 (freac opus) 80, 100 or 120ms. + + @return chunk size + """ + if self.version != "hev2" and size > 60: + raise ValueError("non hev2 can't use framesize > 60") + + self.encoder.set_frame_size(size) + + return int((size / 1000) * self.samplerate) + + def set_packet_loss(self, loss=0): + """input: % percent""" + if loss > 100: + raise ValueError("percent must <=100") + + self.encoder.set_packets_loss(loss) + + def set_bitrate_mode(self, mode="CVBR"): + """VBR, CVBR, CBR + VBR in 1.5.x replace by CVBR + """ + + self.encoder.set_bitrate_mode(mode) + + def set_feature(self, prediction=False, phaseinvert=False, DTX=False): + self.encoder.CTL(pyogg.opus.OPUS_SET_PREDICTION_DISABLED_REQUEST, int(prediction)) + self.encoder.CTL(pyogg.opus.OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, int(phaseinvert)) + self.encoder.CTL(pyogg.opus.OPUS_SET_DTX_REQUEST, int(DTX)) + + def enable_voice_mode(self, enable=True, auto=False): + self.encoder.enable_voice_enhance(enable, auto) + + def __parameterization(self, stereo_signal): + # Convert int16 to float32 for processing + stereo_signal = stereo_signal.astype(np.float32) / 32768.0 + + # Reshape stereo_signal into a 2D array with two channels + stereo_signal = stereo_signal.reshape((-1, 2)) + + # Calculate the magnitude spectrogram for each channel + mag_left = np.abs(np.fft.fft(stereo_signal[:, 0])) + mag_right = np.abs(np.fft.fft(stereo_signal[:, 1])) + + # Calculate the phase difference between the left and right channels + phase_diff = np.angle(stereo_signal[:, 0]) - np.angle(stereo_signal[:, 1]) + + # Compute other spatial features + # Calculate stereo width + stereo_width = np.mean(np.correlate(mag_left, mag_right, mode='full')) + + # Calculate phase coherence + phase_coherence = np.mean(np.cos(phase_diff)) + + # Calculate stereo panning + stereo_panning_left = np.mean(mag_left / (mag_left + mag_right)) + stereo_panning_right = np.mean(mag_right / (mag_left + mag_right)) + + pan = stereo_panning_right - stereo_panning_left + + # Return the derived parameters + return (int(stereo_width), phase_coherence, pan) + + def encode(self, pcmbytes, directpcm=False): + """input: pcm bytes accept float32/int16 only + x74 is mono + x75 is stereo LR + x76 is stereo mid/side + + xnl is no side audio + """ + if directpcm: + if pcmbytes.dtype == np.float32: + pcm = (pcmbytes * 32767).astype(np.int16) + elif pcmbytes.dtype == np.int16: + pcm = pcmbytes.astype(np.int16) + else: + raise TypeError("accept only int16/float32") + else: + pcm = np.frombuffer(pcmbytes, dtype=np.int16) + + pcmreshaped = pcm.reshape(-1, 2) + + mono_data = np.mean(pcmreshaped * 0.5, axis=1, dtype=np.int16) + + stereodata = self.__parameterization(pcmreshaped) + packedstereodata = struct.pack('iff', *stereodata) + + encoded_packet = self.encoder.buffered_encode(memoryview(bytearray(mono_data)), flush=True)[0][0].tobytes() + + encoded_packet = (encoded_packet + b'\\x21\\x75' + packedstereodata) + + return encoded_packet + +class xOpusDecoder: def __init__(self, sample_rate=48000): self.Ldecoder = pyogg.OpusDecoder() self.Rdecoder = pyogg.OpusDecoder() @@ -243,22 +441,189 @@ class DualOpusDecoder: self.Ldecoder.set_sampling_frequency(sample_rate) self.Rdecoder.set_sampling_frequency(sample_rate) + self.__prev_pan = 0.0 + + def __smooth(self, value, prev_value, alpha=0.1): + return alpha * value + (1 - alpha) * prev_value + + def __expand_and_pan(self, input_signal, pan_value, expansion_factor, gain): + """ + Apply stereo expansion and panning to an input audio signal. + + Parameters: + - input_signal: Input audio signal (numpy array of int16). + - expansion_factor: Factor to expand the stereo width (0 to 1). + - pan_value: Pan value (-1 to 1, where -1 is full left, 1 is full right). + - gain: Gain factor to adjust the volume. + + Returns: + - output_signal: Processed audio signal (stereo, numpy array of int16). + """ + + # Convert int16 to float32 for processing + input_signal_float = input_signal.astype(np.float32) / 32768.0 + + # Separate the channels + left_channel = input_signal_float[:, 0] + right_channel = input_signal_float[:, 1] + + # Apply panning + pan_left = (1 - pan_value) / 2 + pan_right = (1 + pan_value) / 2 + left_channel *= pan_left + right_channel *= pan_right + + # Apply stereo expansion + center = (left_channel + right_channel) / 2 + left_channel = center + (left_channel - center) * expansion_factor + right_channel = center + (right_channel - center) * expansion_factor + + # Apply gain + left_channel *= gain + right_channel *= gain + + # Ensure no clipping by normalizing if necessary + max_val = max(np.max(np.abs(left_channel)), np.max(np.abs(right_channel))) + if max_val > 1.0: + left_channel /= max_val + right_channel /= max_val + + # Merge the channels + output_signal = np.stack((left_channel, right_channel), axis=-1) + + return (output_signal * 32767).astype(np.int16) + + def __mix_stereo_signals(self, signal1, signal2, volume1=1.0, volume2=1.0): + # Ensure both signals have the same length + length = max(len(signal1), len(signal2)) + signal1 = np.pad(signal1, ((0, length - len(signal1)), (0, 0)), mode='constant') + signal2 = np.pad(signal2, ((0, length - len(signal2)), (0, 0)), mode='constant') + + # Convert signals to float + signal1 = signal1.astype(np.float32) + signal2 = signal2.astype(np.float32) + + # Adjust volume + signal1 *= volume1 + signal2 *= volume2 + + # Mix the signals + mixed_signal = signal1 + signal2 + + # Normalize the mixed signal to prevent clipping + max_amplitude = np.max(np.abs(mixed_signal)) + if max_amplitude > 32767: + mixed_signal = (mixed_signal / max_amplitude) * 32767 + + return mixed_signal.astype(np.int16) + + def __apply_smoothing_window(self, audio_data, window_size): + """ + Apply a smoothing window to the beginning and end of the audio data. + + Parameters: + - audio_data: 2D numpy array with shape (num_samples, 2) + - window_size: Size of the smoothing window in samples + + Returns: + - smoothed_audio_data: 2D numpy array with the smoothing window applied + """ + window = np.hanning(window_size * 2) + fade_in = window[:window_size] + fade_out = window[-window_size:] + + audio_data[:window_size, :] *= fade_in[:, np.newaxis] + audio_data[-window_size:, :] *= fade_out[:, np.newaxis] + + return audio_data + + def __stereo_widening_effect(self, data, delay_samples=10, gain=0.8, window_size=100): + audio_data = data.reshape(-1, 2) + + # Convert int16 to float32 for processing + audio_data = audio_data.astype(np.float32) + + # Apply delay to the right channel + right_channel = np.roll(audio_data[:, 1], delay_samples) + + # Apply gain to both channels + audio_data[:, 0] *= gain + right_channel *= gain + + # Combine channels back into stereo + widened_audio_data = np.stack((audio_data[:, 0], right_channel), axis=1) + + # Apply smoothing window to reduce clicks + widened_audio_data = self.__apply_smoothing_window(widened_audio_data, window_size) + + # Clip to avoid overflow + widened_audio_data = np.clip(widened_audio_data, -32768, 32767) + + # Convert float32 back to int16 + widened_audio_data = widened_audio_data.astype(np.int16) + + return widened_audio_data + + def __apply_phase_coherence_to_stereo(self, signal, phase_coherence): + # Convert phase coherence to phase shift in radians + phase_shift = np.arccos(phase_coherence) + # Apply phase shift to both channels + return self.__apply_phase_shift(signal, phase_shift) + + # Function to apply phase shift to one channel + def __apply_phase_shift(self, signal, phase_shift): + # Convert to complex + signal_complex = signal.astype(np.complex64) + # Apply phase shift + shifted_signal = signal_complex * np.exp(1j * phase_shift) + return shifted_signal.astype(np.int16) + + def __butter_lowpass_filter_stereo(self, data, cutoff, fs, order=5): + nyq = 0.5 * fs + normal_cutoff = cutoff / nyq + b, a = butter(order, normal_cutoff, btype='low', analog=False) + filtered_data = np.apply_along_axis(lambda x: filtfilt(b, a, x), axis=0, arr=data) + return filtered_data.astype(np.int16) + + def __synthstereo(self, mono_signal, stereodata): + pan = stereodata[2] + + # Smooth the pan value + pan = self.__smooth(pan, self.__prev_pan, alpha=0.25) + self.__prev_pan = pan + + stereo_exp = stereodata[0] / 10000 + + try: + delayed = self.__stereo_widening_effect(mono_signal, int(stereo_exp), 1, int(stereo_exp) * 2) + except: + delayed = mono_signal + + l1 = self.__expand_and_pan(mono_signal, pan, 1, 2) + + stereo_signal_shifted = self.__apply_phase_coherence_to_stereo(delayed, stereodata[1]) + + return self.__mix_stereo_signals(l1, stereo_signal_shifted, volume1=1, volume2=0.5).astype(np.int16) + def decode(self, dualopusbytes: bytes, outputformat=np.int16): # mode check if b"\\x64\\x74" in dualopusbytes: mode = 0 - dualopusbytespilted = dualopusbytes.split(b'\\x64\\x74') + xopusbytespilted = dualopusbytes.split(b'\\x64\\x74') elif b"\\x64\\x76" in dualopusbytes: mode = 2 - dualopusbytespilted = dualopusbytes.split(b'\\x64\\x76') + xopusbytespilted = dualopusbytes.split(b'\\x64\\x76') elif b"\\x64\\x75" in dualopusbytes: mode = 1 - dualopusbytespilted = dualopusbytes.split(b'\\x64\\x75') + xopusbytespilted = dualopusbytes.split(b'\\x64\\x75') + elif b"\\x21\\x75" in dualopusbytes: + mode = 3 # v2 + xopusbytespilted = dualopusbytes.split(b'\\x21\\x75') else: - raise TypeError("this is not dual opus") + raise TypeError("this is not xopus bytes") if mode == 0: # mono - Mencoded_packet = dualopusbytespilted[0] + Mencoded_packet = xopusbytespilted[0] decoded_left_channel_pcm = self.Ldecoder.decode(memoryview(bytearray(Mencoded_packet))) Mpcm = np.frombuffer(decoded_left_channel_pcm, dtype=np.int16) @@ -266,8 +631,8 @@ class DualOpusDecoder: elif mode == 2: # stereo mid/side (Joint encoding) - Mencoded_packet = dualopusbytespilted[0] - Sencoded_packet = dualopusbytespilted[1] + Mencoded_packet = xopusbytespilted[0] + Sencoded_packet = xopusbytespilted[1] decoded_mid_channel_pcm = self.Ldecoder.decode(memoryview(bytearray(Mencoded_packet))) Mpcm = np.frombuffer(decoded_mid_channel_pcm, dtype=np.int16) @@ -279,18 +644,34 @@ class DualOpusDecoder: Mpcm = int16_to_float32(Mpcm) Spcm = int16_to_float32(Spcm) - L = (Mpcm + Spcm) / 1.5 - R = (Mpcm - Spcm) / 1.5 + L = Mpcm + Spcm + R = Mpcm - Spcm stereo_signal = np.column_stack((L, R)) + + max_amplitude = np.max(np.abs(stereo_signal)) + if max_amplitude > 1.0: + stereo_signal /= max_amplitude + stereo_signal = float32_to_int16(stereo_signal) else: stereo_signal = np.column_stack((Mpcm, Mpcm)) + elif mode == 3: + Mencoded_packet = xopusbytespilted[0] + stereodatapacked = xopusbytespilted[1] + stereodata = struct.unpack('iff', stereodatapacked) + + mono_channel_pcm = self.Ldecoder.decode(memoryview(bytearray(Mencoded_packet))) + Mpcm = np.frombuffer(mono_channel_pcm, dtype=np.int16) + + stereo_audio = np.stack((Mpcm, Mpcm)).T.reshape(-1, 2) + + stereo_signal = self.__synthstereo(stereo_audio, stereodata) else: # stereo LR - Lencoded_packet = dualopusbytespilted[0] - Rencoded_packet = dualopusbytespilted[1] + Lencoded_packet = xopusbytespilted[0] + Rencoded_packet = xopusbytespilted[1] decoded_left_channel_pcm = self.Ldecoder.decode(memoryview(bytearray(Lencoded_packet))) decoded_right_channel_pcm = self.Rdecoder.decode(memoryview(bytearray(Rencoded_packet))) @@ -368,10 +749,13 @@ class FooterContainer: return loudness_avg, length class XopusWriter: - def __init__(self, file, encoder: DualOpusEncoder, metadata={}): + def __init__(self, file, encoder: DualOpusEncoder, metadata=None): self.file = file self.encoder = encoder + if metadata is None: + metadata = {} + systemmetadata = { "format": "Xopus", "audio": { @@ -445,7 +829,8 @@ class XopusReader: else: try: yield decoder.decode(data) - except: + except Exception as e: + #print(e) yield b"" else: decodedlist = [] diff --git a/player.py b/player.py index 5bf2ef7..0e1da48 100644 --- a/player.py +++ b/player.py @@ -1,5 +1,5 @@ import pyaudio -from libxheopus import DualOpusDecoder, XopusReader +from libxheopus import xOpusDecoder, XopusReader import argparse from tqdm import tqdm import wave @@ -16,7 +16,7 @@ progress = tqdm() # Initialize PyAudio p = pyaudio.PyAudio() -decoder = DualOpusDecoder() +decoder = xOpusDecoder() xopusdecoder = XopusReader(args.input) diff --git a/realtime.py b/realtime.py new file mode 100644 index 0000000..c0b5de1 --- /dev/null +++ b/realtime.py @@ -0,0 +1,79 @@ +import numpy as np +import pyaudio +import os +from libxheopus import DualOpusEncoder, xOpusDecoder + +encoder = DualOpusEncoder("restricted_lowdelay", 48000, "hev2") +encoder.set_bitrates(24000) +encoder.set_bitrate_mode("CVBR") +encoder.set_bandwidth("fullband") +encoder.set_compression(10) +desired_frame_size = encoder.set_frame_size(120) + +decoder = xOpusDecoder(48000) + +p = pyaudio.PyAudio() + +device_name_input = "Line 5 (Virtual Audio Cable)" +device_index_input = 0 +for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == device_name_input: + device_index_input = dev['index'] + break + +device_name_output = "Speakers (2- USB Audio DAC )" +device_index_output = 0 +for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == device_name_output: + device_index_output = dev['index'] + break + +streaminput = p.open(format=pyaudio.paInt16, channels=2, rate=48000, input=True, input_device_index=device_index_input) +streamoutput = p.open(format=pyaudio.paInt16, channels=2, rate=48000, output=True, output_device_index=device_index_output) + +print(desired_frame_size) + +try: + while True: + try: + pcm = np.frombuffer(streaminput.read(desired_frame_size, exception_on_overflow=False), dtype=np.int16) + + if len(pcm) == 0: + # If PCM is empty, break the loop + break + + encoded_packets = encoder.encode(pcm) + + print(len(pcm), "-encoded->", len(encoded_packets)) + + + # print(encoded_packet) + try: + decoded_pcm = decoder.decode(encoded_packets) + except Exception as e: + decoded_pcm = b"" + + + # Check if the decoded PCM is empty or not + if len(decoded_pcm) > 0: + pcm_to_write = np.frombuffer(decoded_pcm, dtype=np.int16) + + streamoutput.write(pcm_to_write.astype(np.int16).tobytes()) + else: + print("Decoded PCM is empty") + + except Exception as e: + print(e) + raise + +except KeyboardInterrupt: + print("Interrupted by user") +finally: + # Clean up PyAudio streams and terminate PyAudio + streaminput.stop_stream() + streaminput.close() + streamoutput.stop_stream() + streamoutput.close() + p.terminate() \ No newline at end of file