feat: WebSocket команды — раздельная громкость mic/snd + запись аудио + обновлённый web-client
- satellite.py: разделён _runtime_volume на _runtime_mic_volume и _runtime_snd_volume - satellite.py: добавлены команды start_recording/stop_recording (WAV через base64) - satellite.py: метод _create_wav_from_buffer() для создания WAV из буфера - web-client: добавлен слайдер громкости микрофона (MIC+/MIC-) - web-client: добавлены кнопки записи и воспроизведения аудио - web-client: улучшенный лог событий и отладочные console.log - docs: websocket-server-plan.md сокращён с 1205 до ~80 строк Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
+105
-14
@@ -113,7 +113,8 @@ class SatelliteBase:
|
||||
self._unmute_microphone_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Runtime настройки (изменяемые через WebSocket команды)
|
||||
self._runtime_volume: float = 1.0
|
||||
self._runtime_mic_volume: float = 1.0 # Громкость микрофона
|
||||
self._runtime_snd_volume: float = 1.0 # Громкость воспроизведения
|
||||
self._runtime_led_brightness: int = 128
|
||||
|
||||
# WebSocket server
|
||||
@@ -125,6 +126,11 @@ class SatelliteBase:
|
||||
self._last_mic_level: float = 0.0
|
||||
self._audio_level_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Audio recording
|
||||
self._is_recording: bool = False
|
||||
self._recording_buffer: List[bytes] = []
|
||||
self._recording_start_time: Optional[float] = None
|
||||
|
||||
# Debug audio recording
|
||||
self.wake_audio_writer: Optional[DebugAudioWriter] = None
|
||||
self.stt_audio_writer: Optional[DebugAudioWriter] = None
|
||||
@@ -591,9 +597,9 @@ class SatelliteBase:
|
||||
audio_bytes, self.settings.mic.volume_multiplier
|
||||
)
|
||||
|
||||
# Применить runtime громкость (из WebSocket команд)
|
||||
if self._runtime_volume != 1.0:
|
||||
audio_bytes = multiply_volume(audio_bytes, self._runtime_volume)
|
||||
# Применить runtime громкость микрофона (из WebSocket команд)
|
||||
if self._runtime_mic_volume != 1.0:
|
||||
audio_bytes = multiply_volume(audio_bytes, self._runtime_mic_volume)
|
||||
|
||||
if self._mic_webrtc is not None:
|
||||
# Noise suppression and auto gain
|
||||
@@ -653,9 +659,13 @@ class SatelliteBase:
|
||||
_LOGGER.debug("Connected to snd service")
|
||||
|
||||
# Audio processing
|
||||
if self.settings.snd.needs_processing and AudioChunk.is_type(
|
||||
event.type
|
||||
):
|
||||
# Проверяем нужна ли обработка: базовый volume_multiplier или runtime громкость
|
||||
needs_processing = (
|
||||
self.settings.snd.needs_processing # volume_multiplier != 1.0
|
||||
or self._runtime_snd_volume != 1.0 # WebSocket громкость
|
||||
)
|
||||
|
||||
if needs_processing and AudioChunk.is_type(event.type):
|
||||
chunk = AudioChunk.from_event(event)
|
||||
audio_bytes = self._process_snd_audio(chunk.audio)
|
||||
event = AudioChunk(
|
||||
@@ -693,6 +703,11 @@ class SatelliteBase:
|
||||
audio_bytes, self.settings.snd.volume_multiplier
|
||||
)
|
||||
|
||||
# Применить runtime громкость воспроизведения (из WebSocket команд)
|
||||
if self._runtime_snd_volume != 1.0:
|
||||
_LOGGER.debug(f"Applying runtime volume: {self._runtime_snd_volume}")
|
||||
audio_bytes = multiply_volume(audio_bytes, self._runtime_snd_volume)
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def _play_wav(
|
||||
@@ -1149,7 +1164,8 @@ class SatelliteBase:
|
||||
Обработать команду от WebSocket клиента.
|
||||
|
||||
Поддерживаемые команды:
|
||||
- set_volume: Изменить громкость
|
||||
- set_volume: Изменить громкость воспроизведения (TTS, звуки)
|
||||
- set_mic_volume: Изменить чувствительность микрофона
|
||||
- mute: Заглушить микрофон
|
||||
- unmute: Включить микрофон
|
||||
- play_sound: Воспроизвести WAV файл
|
||||
@@ -1158,12 +1174,19 @@ class SatelliteBase:
|
||||
"""
|
||||
try:
|
||||
if command == "set_volume":
|
||||
# Изменить громкость (0.0 - 1.0)
|
||||
# Изменить громкость воспроизведения (0.0 - 1.0)
|
||||
# Поддерживаем оба названия: "level" и "volume" (для обратной совместимости)
|
||||
level = float(data.get("level") or data.get("volume", 1.0))
|
||||
level = max(0.0, min(1.0, level)) # Clamp 0-1
|
||||
self._runtime_volume = level
|
||||
_LOGGER.info(f"Setting volume to {level}")
|
||||
self._runtime_snd_volume = level
|
||||
_LOGGER.info(f"Setting playback volume to {level}")
|
||||
|
||||
elif command == "set_mic_volume":
|
||||
# Изменить чувствительность микрофона (0.0 - 2.0)
|
||||
level = float(data.get("level") or data.get("volume", 1.0))
|
||||
level = max(0.0, min(2.0, level)) # Clamp 0-2
|
||||
self._runtime_mic_volume = level
|
||||
_LOGGER.info(f"Setting microphone sensitivity to {level}")
|
||||
|
||||
elif command == "mute":
|
||||
_LOGGER.info("Muting microphone")
|
||||
@@ -1189,6 +1212,42 @@ class SatelliteBase:
|
||||
self._runtime_led_brightness = brightness
|
||||
_LOGGER.info(f"Setting LED brightness to {brightness}")
|
||||
|
||||
elif command == "start_recording":
|
||||
# Начать запись аудио с микрофона
|
||||
_LOGGER.info("Starting audio recording")
|
||||
self._is_recording = True
|
||||
self._recording_buffer = []
|
||||
self._recording_start_time = time.time()
|
||||
|
||||
elif command == "stop_recording":
|
||||
# Остановить запись и вернуть WAV файл
|
||||
if not self._is_recording:
|
||||
_LOGGER.warning("Recording not started")
|
||||
return
|
||||
|
||||
_LOGGER.info("Stopping audio recording")
|
||||
self._is_recording = False
|
||||
|
||||
# Создать WAV файл из буфера
|
||||
duration = time.time() - (self._recording_start_time or 0)
|
||||
wav_data = await self._create_wav_from_buffer()
|
||||
|
||||
# Отправить событие с данными записи
|
||||
import base64
|
||||
recording_event = Event(
|
||||
type="recording-ready",
|
||||
data={
|
||||
"duration": duration,
|
||||
"size": len(wav_data),
|
||||
"data": base64.b64encode(wav_data).decode('utf-8')
|
||||
}
|
||||
)
|
||||
await self._broadcast_to_websocket(recording_event)
|
||||
|
||||
# Очистить буфер
|
||||
self._recording_buffer = []
|
||||
_LOGGER.info(f"Recording saved: {duration:.1f}s, {len(wav_data)} bytes")
|
||||
|
||||
elif command == "trigger_event":
|
||||
# Отправить событие в Home Assistant
|
||||
event_type = data.get("event_type")
|
||||
@@ -1196,9 +1255,8 @@ class SatelliteBase:
|
||||
if event_type:
|
||||
_LOGGER.info(f"Triggering event: {event_type}")
|
||||
# Отправить событие в server
|
||||
from wyoming.event import Event
|
||||
event = Event(type=event_type, data=event_data)
|
||||
await self.event_to_server(event)
|
||||
trigger_event = Event(type=event_type, data=event_data)
|
||||
await self.event_to_server(trigger_event)
|
||||
else:
|
||||
_LOGGER.warning("trigger_event missing 'event_type'")
|
||||
|
||||
@@ -1250,6 +1308,35 @@ class SatelliteBase:
|
||||
if self._last_mic_level > 0.05:
|
||||
_LOGGER.debug(f"Audio level: {self._last_mic_level:.2f}")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Audio Recording
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def _create_wav_from_buffer(self) -> bytes:
|
||||
"""
|
||||
Создать WAV файл из буфера записи.
|
||||
|
||||
Returns:
|
||||
bytes: WAV файл в виде байтов
|
||||
"""
|
||||
import io
|
||||
|
||||
if not self._recording_buffer:
|
||||
return b''
|
||||
|
||||
# Объединить все чанки
|
||||
audio_data = b''.join(self._recording_buffer)
|
||||
|
||||
# Создать WAV файл в памяти
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wav_file:
|
||||
wav_file.setnchannels(self.settings.mic.channels)
|
||||
wav_file.setsampwidth(self.settings.mic.width)
|
||||
wav_file.setframerate(self.settings.mic.rate)
|
||||
wav_file.writeframes(audio_data)
|
||||
|
||||
return wav_buffer.getvalue()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Info
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1619,6 +1706,10 @@ class WakeStreamingSatellite(SatelliteBase):
|
||||
|
||||
self._last_mic_level = self._calculate_rms(audio_bytes)
|
||||
|
||||
# Сохранить в буфер записи (если запись активна)
|
||||
if self._is_recording:
|
||||
self._recording_buffer.append(audio_bytes)
|
||||
|
||||
# Debug audio recording
|
||||
if (self.wake_audio_writer is not None) or (self.stt_audio_writer is not None):
|
||||
if self.wake_audio_writer is not None:
|
||||
|
||||
Reference in New Issue
Block a user