feat: добавить Audio Level monitoring — RMS громкости микрофона в WebSocket
- settings.py: audio_level_enabled, audio_level_interval_ms
- satellite.py: _calculate_rms() — вычисление RMS (0.0-1.0)
- satellite.py: _send_audio_level() — фоновая задача отправки
- satellite.py: _last_mic_level, _audio_level_task атрибуты
- satellite.py: интеграция в _start() / _stop()
- satellite.py: вычисление RMS в event_from_mic() для всех классов
- __main__.py: CLI --audio-level-enabled, --audio-level-interval-ms
Событие в WebSocket (10 раз в секунду):
{type: audio-level, data: {level: 0.42}}
This commit is contained in:
@@ -117,6 +117,10 @@ class SatelliteBase:
|
||||
self._websocket_clients: Set[websockets.server.WebSocketServerProtocol] = set() # type: ignore
|
||||
self._websocket_lock = asyncio.Lock()
|
||||
|
||||
# Audio level monitoring
|
||||
self._last_mic_level: float = 0.0
|
||||
self._audio_level_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Debug audio recording
|
||||
self.wake_audio_writer: Optional[DebugAudioWriter] = None
|
||||
self.stt_audio_writer: Optional[DebugAudioWriter] = None
|
||||
@@ -256,6 +260,12 @@ class SatelliteBase:
|
||||
if self.settings.event.websocket_enabled:
|
||||
await self.start_websocket_server()
|
||||
|
||||
# Запустить задачу audio level (новый)
|
||||
if self.settings.event.audio_level_enabled:
|
||||
self._audio_level_task = asyncio.create_task(
|
||||
self._send_audio_level(), name="audio_level"
|
||||
)
|
||||
|
||||
self.state = State.STARTED
|
||||
await self.started()
|
||||
|
||||
@@ -271,6 +281,11 @@ class SatelliteBase:
|
||||
if self._websocket_server:
|
||||
await self.stop_websocket_server()
|
||||
|
||||
# Остановить задачу audio level (новый)
|
||||
if self._audio_level_task:
|
||||
self._audio_level_task.cancel()
|
||||
self._audio_level_task = None
|
||||
|
||||
await self._disconnect_from_services()
|
||||
self._disable_ping()
|
||||
self.state = State.STOPPED
|
||||
@@ -1176,6 +1191,44 @@ class SatelliteBase:
|
||||
except Exception as exc:
|
||||
_LOGGER.error(f"Error handling command {command}: {exc}")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Audio Level Monitoring
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _calculate_rms(self, audio_bytes: bytes) -> float:
|
||||
"""
|
||||
Вычислить RMS громкость (0.0 - 1.0).
|
||||
|
||||
Использует тот же алгоритм что и WebRtcVadEnergy из vad.py
|
||||
"""
|
||||
pcm = array.array("h")
|
||||
pcm.frombytes(audio_bytes)
|
||||
if not pcm:
|
||||
return 0.0
|
||||
rms = math.sqrt(sum(int(s) * int(s) for s in pcm) / len(pcm))
|
||||
return rms / 32767.0 # Нормализация для 16-bit
|
||||
|
||||
async def _send_audio_level(self) -> None:
|
||||
"""
|
||||
Периодически отправлять audio-level события в WebSocket.
|
||||
"""
|
||||
from wyoming.event import Event as WyomingEvent
|
||||
|
||||
interval = self.settings.event.audio_level_interval_ms / 1000.0
|
||||
|
||||
while self.is_running:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
if (
|
||||
self.settings.event.audio_level_enabled
|
||||
and self._websocket_clients
|
||||
):
|
||||
event = WyomingEvent(
|
||||
"audio-level",
|
||||
{"level": self._last_mic_level}
|
||||
).event()
|
||||
await self._broadcast_to_websocket(event)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Info
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1235,15 +1288,18 @@ class AlwaysStreamingSatellite(SatelliteBase):
|
||||
return
|
||||
|
||||
if AudioChunk.is_type(event.type):
|
||||
# Вычислить RMS для аудио чанка
|
||||
if audio_bytes is None:
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
self._last_mic_level = self._calculate_rms(audio_bytes)
|
||||
|
||||
# Forward to server
|
||||
await self.event_to_server(event)
|
||||
|
||||
# Debug audio recording
|
||||
if self.stt_audio_writer is not None:
|
||||
if audio_bytes is None:
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
self.stt_audio_writer.write(audio_bytes)
|
||||
|
||||
|
||||
@@ -1327,13 +1383,15 @@ class VadStreamingSatellite(SatelliteBase):
|
||||
# Only unpack chunk once
|
||||
chunk: Optional[AudioChunk] = None
|
||||
|
||||
# Вычислить RMS для аудио чанка
|
||||
if audio_bytes is None:
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
self._last_mic_level = self._calculate_rms(audio_bytes)
|
||||
|
||||
# Debug audio recording
|
||||
if self.stt_audio_writer is not None:
|
||||
if audio_bytes is None:
|
||||
# Need to unpack
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
self.stt_audio_writer.write(audio_bytes)
|
||||
|
||||
if (
|
||||
@@ -1533,12 +1591,15 @@ class WakeStreamingSatellite(SatelliteBase):
|
||||
):
|
||||
return
|
||||
|
||||
# Вычислить RMS для аудио чанка
|
||||
if audio_bytes is None:
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
self._last_mic_level = self._calculate_rms(audio_bytes)
|
||||
|
||||
# Debug audio recording
|
||||
if (self.wake_audio_writer is not None) or (self.stt_audio_writer is not None):
|
||||
if audio_bytes is None:
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = chunk.audio
|
||||
|
||||
if self.wake_audio_writer is not None:
|
||||
self.wake_audio_writer.write(audio_bytes)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user