wangzhibo
2026-10-08 220e4fab27b7f419b3e89e2f2d28ae789aac09d7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import logging
from typing import Protocol
 
import numpy as np
 
from ..config import RuntimeConfig
 
logger = logging.getLogger(__name__)
 
 
class AudioListener(Protocol):
    def on_audio_data(self, audio_data: np.ndarray) -> None:
        ...
 
 
class MicrophoneListener:
 
    def __init__(self, config: RuntimeConfig) -> None:
        self.config = config
        self._stream = None
        self._running = False
        self._listeners: list[AudioListener] = []
        self._sample_rate = self.config.audio.sample_rate
        self._channels = self.config.audio.channels
        self._device = self.config.audio.input_device
        self._block_duration_ms = 30
        self._block_size = int(self._sample_rate * (self._block_duration_ms / 1000))
 
    def add_audio_listener(self, listener: AudioListener) -> None:
        if listener not in self._listeners:
            self._listeners.append(listener)
 
    def remove_audio_listener(self, listener: AudioListener) -> None:
        if listener in self._listeners:
            self._listeners.remove(listener)
 
    def start(self) -> None:
        try:
            import sounddevice as sd
        except ImportError as exc:
            raise RuntimeError(
                "Missing dependency: sounddevice. Install runtime dependencies before starting microphone listener."
            ) from exc
 
        self._stream = sd.InputStream(
            device=self._device,
            samplerate=self._sample_rate,
            channels=self._channels,
            dtype="int16",
            blocksize=self._block_size,
            callback=self._input_callback,
            latency="low",
        )
        self._stream.start()
        self._running = True
 
        logger.info("microphone listener started")
        logger.info("microphone sample rate: %s", self._sample_rate)
        logger.info("microphone channels: %s", self._channels)
        logger.info("microphone device: %s", self._device if self._device is not None else "default")
        logger.info("microphone block size: %s", self._block_size)
 
    def stop(self) -> None:
        if not self._running and self._stream is None:
            return
 
        self._running = False
        if self._stream is not None:
            try:
                self._stream.stop()
            finally:
                self._stream.close()
                self._stream = None
        logger.info("microphone listener stopped")
 
    def _input_callback(self, indata, frames, time_info, status) -> None:
        _ = frames, time_info
        if status and "overflow" not in str(status).lower():
            logger.warning("microphone status: %s", status)
 
        audio = np.copy(indata)
        if audio.ndim > 1:
            audio = audio[:, 0]
        else:
            audio = audio.reshape(-1)
 
        for listener in list(self._listeners):
            try:
                listener.on_audio_data(audio)
            except Exception:
                logger.exception("audio listener callback failed")