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