This commit is contained in:
parent
8b6ba00d8c
commit
d3d14919a8
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user