import time import queue import numpy as np try: import sounddevice as sd except (ImportError, OSError): sd = None from ..schema.protocol import SpeakerRequest, SpeakerResponse def handle_command(cmd: SpeakerRequest, state: dict): if cmd is None or cmd.msg_type == "EXIT": state["stop"] = True return if cmd.msg_type == "STOP": state["current_data"] = np.zeros((0, 1), dtype=np.float32) state["started"] = False return if cmd.msg_type == "PAUSE": state["paused"] = True return if cmd.msg_type == "CONTINUE": state["paused"] = False return if cmd.msg_type == "AUDIO": if cmd.audio is not None and len(cmd.audio) > 0: state["current_data"] = np.concatenate( [state["current_data"], cmd.audio.reshape(-1, 1).astype(np.float32)], axis=0 ) def sync_playback_status(state: dict, result_queue): if result_queue is None: return if state.get("paused", False): target = "PAUSED" elif state.get("started", False): target = "PLAYING" else: target = "IDLE" if target == state["playback_state"]: return msg_map = {"PAUSED": "PAUSED", "PLAYING": "STARTED", "IDLE": "FINISHED"} result_queue.put(SpeakerResponse(msg_type=msg_map[target])) state["playback_state"] = target def fill_audio(outdata, frames, state: dict): if state.get("paused", False): outdata.fill(0) return if not state["started"]: if len(state["current_data"]) >= state["threshold"]: state["started"] = True else: outdata.fill(0) return avail = len(state["current_data"]) to_copy = min(avail, frames) if to_copy > 0: outdata[:to_copy] = state["current_data"][:to_copy] state["current_data"] = state["current_data"][to_copy:] if to_copy < frames: outdata[to_copy:].fill(0) state["started"] = False def speaker_worker_proc(play_queue, result_queue=None, sample_rate=24000): state = { "current_data": np.zeros((0, 1), dtype=np.float32), "started": False, "threshold": 1200, "stop": False, "paused": False, "playback_state": "IDLE" } def audio_callback(outdata, frames, time_info, status): while True: try: command = play_queue.get_nowait() handle_command(command, state) except queue.Empty: break fill_audio(outdata, frames, state) sync_playback_status(state, result_queue) if sd is None: # Headless mode: no audio device available if result_queue: result_queue.put(SpeakerResponse(msg_type="READY")) while not state.get("stop"): try: command = play_queue.get(timeout=0.5) handle_command(command, state) except queue.Empty: pass return try: with sd.OutputStream(samplerate=sample_rate, channels=1, callback=audio_callback, blocksize=2048): if result_queue: result_queue.put(SpeakerResponse(msg_type="READY")) while True: time.sleep(0.2) if state.get("stop"): break except KeyboardInterrupt: pass except Exception as e: print(f"⚠️ [SpeakerWorker] No audio device, running headless: {e}") if result_queue: result_queue.put(SpeakerResponse(msg_type="READY")) while not state.get("stop"): try: command = play_queue.get(timeout=0.5) handle_command(command, state) except Exception: pass