From 050b3aa17afdfeb4a2fd58c5bc411a9f1de7e0e0 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 4 Jun 2024 04:46:47 +0200 Subject: [PATCH] Improved audio playback on Linux --- sbapp/main.py | 7 +++- sbapp/plyer/platforms/linux/audio.py | 29 ++++++++++---- sbapp/sideband/audioproc.py | 60 +++++++++++++++++++--------- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/sbapp/main.py b/sbapp/main.py index eb803f2..feb5cfa 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1534,8 +1534,11 @@ class SidebandApp(MDApp): elif audio_field[0] >= LXMF.AM_CODEC2_700C and audio_field[0] <= LXMF.AM_CODEC2_3200: temp_path = self.sideband.rec_cache+"/msg.ogg" from sideband.audioproc import samples_to_ogg, decode_codec2 - if samples_to_ogg(decode_codec2(audio_field[1], audio_field[0]), temp_path): - RNS.log("Wrote wav file to: "+temp_path) + samples = decode_codec2(audio_field[1], audio_field[0]) + if samples_to_ogg(samples, temp_path): + RNS.log("Wrote OGG file to: "+temp_path, RNS.LOG_DEBUG) + else: + RNS.log("OGG write failed", RNS.LOG_DEBUG) else: raise NotImplementedError(audio_field[0]) diff --git a/sbapp/plyer/platforms/linux/audio.py b/sbapp/plyer/platforms/linux/audio.py index 18ba9f2..3509f33 100644 --- a/sbapp/plyer/platforms/linux/audio.py +++ b/sbapp/plyer/platforms/linux/audio.py @@ -1,7 +1,8 @@ import time import threading +import RNS from sbapp.plyer.facades.audio import Audio -from kivy.core.audio import SoundLoader +from ffpyplayer.player import MediaPlayer class LinuxAudio(Audio): @@ -18,8 +19,13 @@ class LinuxAudio(Audio): self.is_playing = False def _check_playback(self): - while self.sound != None and self.sound.state == "play": + run = True + while run and self.sound != None and not self.sound.get_pause(): time.sleep(0.25) + if self.duration: + pts = self.sound.get_pts() + if pts > self.duration: + run = False self.is_playing = False @@ -32,23 +38,30 @@ class LinuxAudio(Audio): pass def _stop(self): - if self.sound != None and self.sound.state == "play": - self.sound.stop() + if self.sound != None: + self.sound.set_pause(True) + self.sound.seek(0, relative=False) self.is_playing = False def _play(self): - if self.sound == None or self._loaded_path != self._file_path: - self.sound = SoundLoader.load(self._file_path) + self.sound = MediaPlayer(self._file_path) + self.metadata = self.sound.get_metadata() + self.duration = self.metadata["duration"] + if self.duration == None: + time.sleep(0.15) + self.metadata = self.sound.get_metadata() + self.duration = self.metadata["duration"] + RNS.log(str(self.metadata)) + + self._loaded_path = self._file_path self.is_playing = True - self.sound.play() self._check_thread = threading.Thread(target=self._check_playback, daemon=True) self._check_thread.start() def reload(self): self._loaded_path = None - self.sound = None def playing(self): return self.is_playing diff --git a/sbapp/sideband/audioproc.py b/sbapp/sideband/audioproc.py index 740bf30..1993057 100644 --- a/sbapp/sideband/audioproc.py +++ b/sbapp/sideband/audioproc.py @@ -43,28 +43,48 @@ def samples_from_ogg(file_path=None): return audio.get_array_of_samples() def samples_to_ogg(samples=None, file_path=None): - if file_path != None and samples != None: - pcm_data = io.BytesIO(samples) - channels = 1; samples_per_second = 8000; bytes_per_sample = 2 + try: + if file_path != None and samples != None: + pcm_data = io.BytesIO(samples) - opus_buffered_encoder = pyogg.OpusBufferedEncoder() - opus_buffered_encoder.set_application("audio") - opus_buffered_encoder.set_sampling_frequency(samples_per_second) - opus_buffered_encoder.set_channels(channels) - opus_buffered_encoder.set_frame_size(20) # milliseconds - ogg_opus_writer = pyogg.OggOpusWriter(file_path, opus_buffered_encoder) + RNS.log(f"Samples: {len(samples)}") + RNS.log(f"Type : {type(samples)}") - frame_duration = 0.1 - frame_size = int(frame_duration * samples_per_second) - bytes_per_frame = frame_size*bytes_per_sample - - while True: - pcm = pcm_data.read(bytes_per_frame) - if len(pcm) == 0: - break - ogg_opus_writer.write(memoryview(bytearray(pcm))) + channels = 1; samples_per_second = 8000; bytes_per_sample = 2 - ogg_opus_writer.close() + opus_buffered_encoder = pyogg.OpusBufferedEncoder() + opus_buffered_encoder.set_application("audio") + opus_buffered_encoder.set_sampling_frequency(samples_per_second) + opus_buffered_encoder.set_channels(channels) + opus_buffered_encoder.set_frame_size(20) # milliseconds + ogg_opus_writer = pyogg.OggOpusWriter(file_path, opus_buffered_encoder) + + frame_duration = 0.020 + frame_size = int(frame_duration * samples_per_second) + bytes_per_frame = frame_size*bytes_per_sample + + read_bytes = 0 + written_bytes = 0 + while True: + pcm = pcm_data.read(bytes_per_frame) + if len(pcm) == 0: + break + else: + read_bytes += len(pcm) + + ogg_opus_writer.write(memoryview(bytearray(pcm))) + written_bytes += len(pcm) + + ogg_opus_writer.close() + + RNS.log(f"Read {read_bytes} bytes") + RNS.log(f"Wrote {written_bytes} bytes") + + return True + + except Exception as e: + RNS.trace_exception(e) + return False def samples_to_wav(samples=None, file_path=None): if samples != None and file_path != None: @@ -87,7 +107,9 @@ def encode_codec2(samples, mode): SPF = c2.samples_per_frame() PACKET_SIZE = SPF * 2 # 16-bit samples STRUCT_FORMAT = '{}h'.format(SPF) + F_FRAMES = len(samples)/SPF N_FRAMES = math.floor(len(samples)/SPF) + # TODO: Add padding to align to whole frames frames = np.array(samples[0:N_FRAMES*SPF], dtype=np.int16) encoded = b""