115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
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
|