From 8b6ba00d8c17b742ac01543d3d36df0c5bb0ecf5 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Mon, 7 Jul 2025 12:00:29 +0100 Subject: [PATCH] add of changes --- protocol_prototype/DryBox/UI/audio_player.py | 138 +++++++++--------- .../DryBox/UI/protocol_phone_client.py | 3 +- 2 files changed, 67 insertions(+), 74 deletions(-) diff --git a/protocol_prototype/DryBox/UI/audio_player.py b/protocol_prototype/DryBox/UI/audio_player.py index a905c63..a4d85ef 100644 --- a/protocol_prototype/DryBox/UI/audio_player.py +++ b/protocol_prototype/DryBox/UI/audio_player.py @@ -25,7 +25,6 @@ class AudioPlayer(QObject): self.audio = None self.streams = {} # client_id -> stream self.buffers = {} # client_id -> queue - self.threads = {} # client_id -> thread self.recording_buffers = {} # client_id -> list of audio data self.recording_enabled = {} # client_id -> bool self.playback_enabled = {} # client_id -> bool @@ -65,30 +64,57 @@ class AudioPlayer(QObject): return False try: - # Create audio stream with larger buffer to prevent underruns + # Create buffer for this client + self.buffers[client_id] = queue.Queue(maxsize=100) # Limit queue size + self.playback_enabled[client_id] = True + + # Create audio stream with callback for continuous playback + def audio_callback(in_data, frame_count, time_info, status): + if status: + self.debug(f"Playback status for client {client_id}: {status}") + + # Get audio data from buffer + audio_data = b'' + bytes_needed = frame_count * 2 # 16-bit samples + + # Try to get enough data for the requested frame count + while len(audio_data) < bytes_needed: + try: + chunk = self.buffers[client_id].get_nowait() + audio_data += chunk + except queue.Empty: + # No more data available, pad with silence + if len(audio_data) < bytes_needed: + silence = b'\x00' * (bytes_needed - len(audio_data)) + audio_data += silence + break + + # Trim to exact size if we got too much + if len(audio_data) > bytes_needed: + # Put extra back in queue + extra = audio_data[bytes_needed:] + try: + self.buffers[client_id].put_nowait(extra) + except queue.Full: + pass + audio_data = audio_data[:bytes_needed] + + return (audio_data, pyaudio.paContinue) + + # Create stream with callback stream = self.audio.open( format=pyaudio.paInt16, channels=self.channels, rate=self.sample_rate, output=True, - frames_per_buffer=self.chunk_size * 2, # Doubled buffer size - stream_callback=None + frames_per_buffer=640, # 80ms buffer for smoother playback + stream_callback=audio_callback ) self.streams[client_id] = stream - self.buffers[client_id] = queue.Queue() - self.playback_enabled[client_id] = True + stream.start_stream() - # Start playback thread - thread = threading.Thread( - target=self._playback_thread, - args=(client_id,), - daemon=True - ) - self.threads[client_id] = thread - thread.start() - - self.debug(f"Started playback for client {client_id}") + self.debug(f"Started callback-based playback for client {client_id}") self.playback_started.emit(client_id) return True @@ -103,12 +129,7 @@ class AudioPlayer(QObject): self.playback_enabled[client_id] = False - # Wait for thread to finish - if client_id in self.threads: - self.threads[client_id].join(timeout=1.0) - del self.threads[client_id] - - # Close stream + # Stop and close stream if client_id in self.streams: try: self.streams[client_id].stop_stream() @@ -119,6 +140,12 @@ class AudioPlayer(QObject): # Clear buffer if client_id in self.buffers: + # Clear any remaining data + while not self.buffers[client_id].empty(): + try: + self.buffers[client_id].get_nowait() + except: + break del self.buffers[client_id] self.debug(f"Stopped playback for client {client_id}") @@ -138,11 +165,23 @@ class AudioPlayer(QObject): self.debug(f"Client {client_id} audio frame #{self._frame_count[client_id]}: {len(pcm_data)} bytes") if client_id in self.buffers: - self.buffers[client_id].put(pcm_data) - if self._frame_count[client_id] == 1: - self.debug(f"Client {client_id} buffer started, queue size: {self.buffers[client_id].qsize()}") + try: + # Use put_nowait to avoid blocking + self.buffers[client_id].put_nowait(pcm_data) + if self._frame_count[client_id] == 1: + self.debug(f"Client {client_id} buffer started, queue size: {self.buffers[client_id].qsize()}") + except queue.Full: + # Buffer is full, drop oldest data to make room + try: + self.buffers[client_id].get_nowait() # Remove oldest + self.buffers[client_id].put_nowait(pcm_data) # Add newest + if self._frame_count[client_id] % 50 == 0: # Log occasionally + self.debug(f"Client {client_id} buffer overflow, dropping old data") + except: + pass else: - self.debug(f"Client {client_id} has no buffer (playback not started?)") + if self._frame_count[client_id] == 1: + self.debug(f"Client {client_id} has no buffer (playback not started?)") # Add to recording buffer if recording if self.recording_enabled.get(client_id, False): @@ -150,53 +189,6 @@ class AudioPlayer(QObject): self.recording_buffers[client_id] = [] self.recording_buffers[client_id].append(pcm_data) - def _playback_thread(self, client_id): - """Thread function for audio playback""" - stream = self.streams.get(client_id) - buffer = self.buffers.get(client_id) - - if not stream or not buffer: - return - - self.debug(f"Playback thread started for client {client_id}") - - # Buffer to accumulate data before playing - accumulated_data = b'' - min_buffer_size = self.chunk_size * 2 # Buffer at least 2 chunks before playing - - while self.playback_enabled.get(client_id, False): - try: - # Get audio data from buffer with timeout - audio_data = buffer.get(timeout=0.04) # 40ms timeout - accumulated_data += audio_data - - # Only play when we have enough data to prevent underruns - if len(accumulated_data) >= min_buffer_size: - # Only log first frame to avoid spam - if not hasattr(self, '_playback_logged'): - self._playback_logged = {} - if client_id not in self._playback_logged: - self._playback_logged[client_id] = False - - if not self._playback_logged[client_id]: - self.debug(f"Client {client_id} playback thread playing first frame: {len(accumulated_data)} bytes") - self._playback_logged[client_id] = True - - # Play accumulated audio - stream.write(accumulated_data[:min_buffer_size]) - accumulated_data = accumulated_data[min_buffer_size:] - - except queue.Empty: - # If we have some accumulated data, play it to avoid gaps - if len(accumulated_data) >= self.chunk_size: - stream.write(accumulated_data[:self.chunk_size]) - accumulated_data = accumulated_data[self.chunk_size:] - continue - except Exception as e: - self.debug(f"Playback error for client {client_id}: {e}") - break - - self.debug(f"Playback thread ended for client {client_id}") def start_recording(self, client_id): """Start recording received audio""" diff --git a/protocol_prototype/DryBox/UI/protocol_phone_client.py b/protocol_prototype/DryBox/UI/protocol_phone_client.py index fa8f862..629858d 100644 --- a/protocol_prototype/DryBox/UI/protocol_phone_client.py +++ b/protocol_prototype/DryBox/UI/protocol_phone_client.py @@ -11,7 +11,8 @@ from dissononce.dh.keypair import KeyPair from dissononce.dh.x25519.public import PublicKey import sys import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# Add path to access voice_codec from Prototype directory +sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'Prototype', 'Protocol_Alpha_0')) from voice_codec import Codec2Wrapper, FSKModem, Codec2Mode # ChaCha20 removed - using only Noise XK encryption