This commit is contained in:
parent
8b6ba00d8c
commit
d3d14919a8
@ -32,6 +32,7 @@ class AudioPlayer(QObject):
|
|||||||
self.channels = 1
|
self.channels = 1
|
||||||
self.chunk_size = 320 # 40ms at 8kHz
|
self.chunk_size = 320 # 40ms at 8kHz
|
||||||
self.debug_callback = None
|
self.debug_callback = None
|
||||||
|
self.actual_sample_rate = 8000 # Will be updated if needed
|
||||||
|
|
||||||
if PYAUDIO_AVAILABLE:
|
if PYAUDIO_AVAILABLE:
|
||||||
try:
|
try:
|
||||||
@ -68,58 +69,91 @@ class AudioPlayer(QObject):
|
|||||||
self.buffers[client_id] = queue.Queue(maxsize=100) # Limit queue size
|
self.buffers[client_id] = queue.Queue(maxsize=100) # Limit queue size
|
||||||
self.playback_enabled[client_id] = True
|
self.playback_enabled[client_id] = True
|
||||||
|
|
||||||
# Create audio stream with callback for continuous playback
|
# Try different sample rates if 8000 Hz fails
|
||||||
def audio_callback(in_data, frame_count, time_info, status):
|
sample_rates = [8000, 16000, 44100, 48000]
|
||||||
if status:
|
stream = None
|
||||||
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
|
for rate in sample_rates:
|
||||||
stream = self.audio.open(
|
try:
|
||||||
format=pyaudio.paInt16,
|
# Adjust buffer size based on sample rate
|
||||||
channels=self.channels,
|
buffer_frames = int(640 * rate / 8000) # Scale buffer size
|
||||||
rate=self.sample_rate,
|
|
||||||
output=True,
|
# Create audio stream with callback for continuous playback
|
||||||
frames_per_buffer=640, # 80ms buffer for smoother playback
|
def audio_callback(in_data, frame_count, time_info, status):
|
||||||
stream_callback=audio_callback
|
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
|
self.streams[client_id] = stream
|
||||||
stream.start_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)
|
self.playback_started.emit(client_id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.debug(f"Failed to start playback for client {client_id}: {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
|
return False
|
||||||
|
|
||||||
def stop_playback(self, client_id):
|
def stop_playback(self, client_id):
|
||||||
@ -230,7 +264,7 @@ class AudioPlayer(QObject):
|
|||||||
with wave.open(save_path, 'wb') as wav_file:
|
with wave.open(save_path, 'wb') as wav_file:
|
||||||
wav_file.setnchannels(self.channels)
|
wav_file.setnchannels(self.channels)
|
||||||
wav_file.setsampwidth(2) # 16-bit
|
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)
|
wav_file.writeframes(combined_audio)
|
||||||
|
|
||||||
self.debug(f"Saved recording for client {client_id} to {save_path}")
|
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}")
|
self.debug(f"Failed to save recording for client {client_id}: {e}")
|
||||||
return None
|
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):
|
def cleanup(self):
|
||||||
"""Clean up audio resources"""
|
"""Clean up audio resources"""
|
||||||
# Stop all playback
|
# Stop all playback
|
||||||
|
Loading…
Reference in New Issue
Block a user