This commit is contained in:
parent
c4610fbcb9
commit
8b6ba00d8c
@ -25,7 +25,6 @@ class AudioPlayer(QObject):
|
|||||||
self.audio = None
|
self.audio = None
|
||||||
self.streams = {} # client_id -> stream
|
self.streams = {} # client_id -> stream
|
||||||
self.buffers = {} # client_id -> queue
|
self.buffers = {} # client_id -> queue
|
||||||
self.threads = {} # client_id -> thread
|
|
||||||
self.recording_buffers = {} # client_id -> list of audio data
|
self.recording_buffers = {} # client_id -> list of audio data
|
||||||
self.recording_enabled = {} # client_id -> bool
|
self.recording_enabled = {} # client_id -> bool
|
||||||
self.playback_enabled = {} # client_id -> bool
|
self.playback_enabled = {} # client_id -> bool
|
||||||
@ -65,30 +64,57 @@ class AudioPlayer(QObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
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(
|
stream = self.audio.open(
|
||||||
format=pyaudio.paInt16,
|
format=pyaudio.paInt16,
|
||||||
channels=self.channels,
|
channels=self.channels,
|
||||||
rate=self.sample_rate,
|
rate=self.sample_rate,
|
||||||
output=True,
|
output=True,
|
||||||
frames_per_buffer=self.chunk_size * 2, # Doubled buffer size
|
frames_per_buffer=640, # 80ms buffer for smoother playback
|
||||||
stream_callback=None
|
stream_callback=audio_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
self.streams[client_id] = stream
|
self.streams[client_id] = stream
|
||||||
self.buffers[client_id] = queue.Queue()
|
stream.start_stream()
|
||||||
self.playback_enabled[client_id] = True
|
|
||||||
|
|
||||||
# Start playback thread
|
self.debug(f"Started callback-based playback for client {client_id}")
|
||||||
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.playback_started.emit(client_id)
|
self.playback_started.emit(client_id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -103,12 +129,7 @@ class AudioPlayer(QObject):
|
|||||||
|
|
||||||
self.playback_enabled[client_id] = False
|
self.playback_enabled[client_id] = False
|
||||||
|
|
||||||
# Wait for thread to finish
|
# Stop and close stream
|
||||||
if client_id in self.threads:
|
|
||||||
self.threads[client_id].join(timeout=1.0)
|
|
||||||
del self.threads[client_id]
|
|
||||||
|
|
||||||
# Close stream
|
|
||||||
if client_id in self.streams:
|
if client_id in self.streams:
|
||||||
try:
|
try:
|
||||||
self.streams[client_id].stop_stream()
|
self.streams[client_id].stop_stream()
|
||||||
@ -119,6 +140,12 @@ class AudioPlayer(QObject):
|
|||||||
|
|
||||||
# Clear buffer
|
# Clear buffer
|
||||||
if client_id in self.buffers:
|
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]
|
del self.buffers[client_id]
|
||||||
|
|
||||||
self.debug(f"Stopped playback for client {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")
|
self.debug(f"Client {client_id} audio frame #{self._frame_count[client_id]}: {len(pcm_data)} bytes")
|
||||||
|
|
||||||
if client_id in self.buffers:
|
if client_id in self.buffers:
|
||||||
self.buffers[client_id].put(pcm_data)
|
try:
|
||||||
if self._frame_count[client_id] == 1:
|
# Use put_nowait to avoid blocking
|
||||||
self.debug(f"Client {client_id} buffer started, queue size: {self.buffers[client_id].qsize()}")
|
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:
|
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
|
# Add to recording buffer if recording
|
||||||
if self.recording_enabled.get(client_id, False):
|
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] = []
|
||||||
self.recording_buffers[client_id].append(pcm_data)
|
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):
|
def start_recording(self, client_id):
|
||||||
"""Start recording received audio"""
|
"""Start recording received audio"""
|
||||||
|
@ -11,7 +11,8 @@ from dissononce.dh.keypair import KeyPair
|
|||||||
from dissononce.dh.x25519.public import PublicKey
|
from dissononce.dh.x25519.public import PublicKey
|
||||||
import sys
|
import sys
|
||||||
import os
|
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
|
from voice_codec import Codec2Wrapper, FSKModem, Codec2Mode
|
||||||
# ChaCha20 removed - using only Noise XK encryption
|
# ChaCha20 removed - using only Noise XK encryption
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user