From d3d14919a89995a381d53c5cd88db608bd8e0385 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Mon, 7 Jul 2025 14:05:07 +0100 Subject: [PATCH] add fix for samples --- protocol_prototype/DryBox/UI/audio_player.py | 154 +++++++++++++------ 1 file changed, 111 insertions(+), 43 deletions(-) diff --git a/protocol_prototype/DryBox/UI/audio_player.py b/protocol_prototype/DryBox/UI/audio_player.py index a4d85ef..a9c1cfd 100644 --- a/protocol_prototype/DryBox/UI/audio_player.py +++ b/protocol_prototype/DryBox/UI/audio_player.py @@ -32,6 +32,7 @@ class AudioPlayer(QObject): self.channels = 1 self.chunk_size = 320 # 40ms at 8kHz self.debug_callback = None + self.actual_sample_rate = 8000 # Will be updated if needed if PYAUDIO_AVAILABLE: try: @@ -68,58 +69,91 @@ class AudioPlayer(QObject): 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) + # Try different sample rates if 8000 Hz fails + sample_rates = [8000, 16000, 44100, 48000] + stream = None - # Create stream with callback - stream = self.audio.open( - format=pyaudio.paInt16, - channels=self.channels, - rate=self.sample_rate, - output=True, - frames_per_buffer=640, # 80ms buffer for smoother playback - stream_callback=audio_callback - ) + for rate in sample_rates: + try: + # Adjust buffer size based on sample rate + buffer_frames = int(640 * rate / 8000) # Scale buffer size + + # 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() + + # Resample if needed + if self.actual_sample_rate != self.sample_rate: + chunk = self._resample_audio(chunk, self.sample_rate, self.actual_sample_rate) + + 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) + + # Try to create stream with current sample rate + stream = self.audio.open( + format=pyaudio.paInt16, + channels=self.channels, + rate=rate, + output=True, + frames_per_buffer=buffer_frames, + stream_callback=audio_callback + ) + + self.actual_sample_rate = rate + if rate != self.sample_rate: + self.debug(f"Using sample rate {rate} Hz (resampling from {self.sample_rate} Hz)") + + break # Success! + + except Exception as e: + if rate == sample_rates[-1]: # Last attempt + raise e + else: + self.debug(f"Sample rate {rate} Hz failed, trying next...") + continue + + if not stream: + raise Exception("Could not create audio stream with any sample rate") self.streams[client_id] = stream stream.start_stream() - self.debug(f"Started callback-based playback for client {client_id}") + self.debug(f"Started callback-based playback for client {client_id} at {self.actual_sample_rate} Hz") self.playback_started.emit(client_id) return True except Exception as e: self.debug(f"Failed to start playback for client {client_id}: {e}") + self.playback_enabled[client_id] = False + if client_id in self.buffers: + del self.buffers[client_id] return False def stop_playback(self, client_id): @@ -230,7 +264,7 @@ class AudioPlayer(QObject): with wave.open(save_path, 'wb') as wav_file: wav_file.setnchannels(self.channels) wav_file.setsampwidth(2) # 16-bit - wav_file.setframerate(self.sample_rate) + wav_file.setframerate(self.sample_rate) # Always save at original 8kHz wav_file.writeframes(combined_audio) self.debug(f"Saved recording for client {client_id} to {save_path}") @@ -245,6 +279,40 @@ class AudioPlayer(QObject): self.debug(f"Failed to save recording for client {client_id}: {e}") return None + def _resample_audio(self, audio_data, from_rate, to_rate): + """Simple linear resampling of audio data""" + if from_rate == to_rate: + return audio_data + + import struct + + # Convert bytes to samples + samples = struct.unpack(f'{len(audio_data)//2}h', audio_data) + + # Calculate resampling ratio + ratio = to_rate / from_rate + new_length = int(len(samples) * ratio) + + # Simple linear interpolation + resampled = [] + for i in range(new_length): + # Find position in original samples + pos = i / ratio + idx = int(pos) + frac = pos - idx + + if idx < len(samples) - 1: + # Linear interpolation between samples + sample = int(samples[idx] * (1 - frac) + samples[idx + 1] * frac) + else: + # Use last sample + sample = samples[-1] if samples else 0 + + resampled.append(sample) + + # Convert back to bytes + return struct.pack(f'{len(resampled)}h', *resampled) + def cleanup(self): """Clean up audio resources""" # Stop all playback