## WebRTC Auto Gain & Noise Suppression This PR adds support for WebRTC Auto Gain and Noise Suppression as previously implemented in the Wyoming Satellite project. ### What's New Two new entities have been created a number slider for gain and another one for noise suppression. The processing happens inside of the `webrtc.py` file and is implemented in the `__main__.py` file's `process_audio` function. The slider for gain has a limit of 0-31 and noise suppression has a limit of 0-4. The Auto Gain and Noise Suppression persist through restarts and are saved as preferences. The values are dynamically chnged without needing a restart. CLI flags called `--mic-auto-gain` and `--mic-noise-suppression` have also been added. > When both values are 0, WebRTC processing is bypassed entirely with no performance overhead. Closes #25 ### Testing When saving the audio recorded by LVA there is a noticeable increase. The following two files have been recorded with similar audio being played through them with these results (Gain-31 and NS-4): | Metric | With AGC+NS | Without | |---|---|---| | Max amplitude | 0.329 | 0.054 | | RMS amplitude | 0.008905 | 0.002862 | | Volume adjustment needed | 3.034x | 13.479x | | Rough frequency | 916 Hz | 705 Hz |
161 lines
5.1 KiB
Python
161 lines
5.1 KiB
Python
"""Shared models."""
|
|
|
|
import json
|
|
import logging
|
|
from dataclasses import asdict, dataclass, field
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from queue import Queue
|
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union
|
|
|
|
if TYPE_CHECKING:
|
|
from pymicro_wakeword import MicroWakeWord
|
|
from pyopen_wakeword import OpenWakeWord
|
|
|
|
from .entity import (
|
|
ESPHomeEntity,
|
|
MediaPlayerEntity,
|
|
MicSettingEntity,
|
|
MuteSwitchEntity,
|
|
ThinkingSoundEntity,
|
|
)
|
|
from .mpv_player import MpvMediaPlayer
|
|
from .satellite import VoiceSatelliteProtocol
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class WakeWordType(str, Enum):
|
|
MICRO_WAKE_WORD = "micro"
|
|
OPEN_WAKE_WORD = "openWakeWord"
|
|
|
|
|
|
@dataclass
|
|
class AvailableWakeWord:
|
|
id: str
|
|
type: WakeWordType
|
|
wake_word: str
|
|
trained_languages: List[str]
|
|
wake_word_path: Path
|
|
|
|
def load(self) -> "Union[MicroWakeWord, OpenWakeWord]":
|
|
if self.type == WakeWordType.MICRO_WAKE_WORD:
|
|
from pymicro_wakeword import MicroWakeWord
|
|
|
|
return MicroWakeWord.from_config(config_path=self.wake_word_path)
|
|
|
|
if self.type == WakeWordType.OPEN_WAKE_WORD:
|
|
from pyopen_wakeword import OpenWakeWord
|
|
|
|
oww_model = OpenWakeWord.from_model(model_path=self.wake_word_path)
|
|
setattr(oww_model, "wake_word", self.wake_word)
|
|
|
|
return oww_model
|
|
|
|
raise ValueError(f"Unexpected wake word type: {self.type}")
|
|
|
|
|
|
@dataclass
|
|
class Preferences:
|
|
active_wake_words: List[str] = field(default_factory=list)
|
|
volume: Optional[float] = None
|
|
thinking_sound: int = 0 # 0 = disabled, 1 = enabled
|
|
mic_auto_gain: int = 0
|
|
mic_noise_suppression: int = 0
|
|
|
|
|
|
@dataclass
|
|
class ServerState:
|
|
name: str
|
|
friendly_name: str
|
|
mac_address: str
|
|
ip_address: str
|
|
network_interface: str
|
|
version: str
|
|
esphome_version: str
|
|
audio_queue: "Queue[Optional[bytes]]"
|
|
entities: "List[ESPHomeEntity]"
|
|
available_wake_words: "Dict[str, AvailableWakeWord]"
|
|
wake_words: "Dict[str, Union[MicroWakeWord, OpenWakeWord]]"
|
|
active_wake_words: Set[str]
|
|
stop_word: "MicroWakeWord"
|
|
music_player: "MpvMediaPlayer"
|
|
tts_player: "MpvMediaPlayer"
|
|
wakeup_sound: str
|
|
processing_sound: str
|
|
timer_finished_sound: str
|
|
mute_sound: str
|
|
unmute_sound: str
|
|
preferences: Preferences
|
|
preferences_path: Path
|
|
download_dir: Path
|
|
|
|
media_player_entity: "Optional[MediaPlayerEntity]" = None
|
|
satellite: "Optional[VoiceSatelliteProtocol]" = None
|
|
mute_switch_entity: "Optional[MuteSwitchEntity]" = None
|
|
thinking_sound_entity: "Optional[ThinkingSoundEntity]" = None
|
|
mic_gain_entity: "Optional[MicSettingEntity]" = None
|
|
mic_noise_suppression_entity: "Optional[MicSettingEntity]" = None
|
|
wake_words_changed: bool = False
|
|
refractory_seconds: float = 2.0
|
|
thinking_sound_enabled: bool = False
|
|
output_only: bool = False
|
|
muted: bool = False
|
|
connected: bool = False
|
|
volume: float = 1.0
|
|
mic_auto_gain: int = 0
|
|
mic_noise_suppression: int = 0
|
|
timer_max_ring_seconds: float = 900.0
|
|
|
|
def save_preferences(self) -> None:
|
|
"""Save preferences as JSON."""
|
|
_LOGGER.debug("Saving preferences: %s", self.preferences_path)
|
|
self.preferences_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(self.preferences_path, "w", encoding="utf-8") as preferences_file:
|
|
json.dump(
|
|
asdict(self.preferences),
|
|
preferences_file,
|
|
ensure_ascii=False,
|
|
indent=4,
|
|
)
|
|
|
|
def persist_volume(self, volume: float) -> None:
|
|
"""Persist the normalized media volume (0.0 - 1.0)."""
|
|
clamped_volume = max(0.0, min(1.0, volume))
|
|
_LOGGER.debug(
|
|
"persist_volume called: new=%s, current=%s, prefs=%s",
|
|
clamped_volume,
|
|
self.volume,
|
|
self.preferences.volume,
|
|
)
|
|
|
|
if abs(self.volume - clamped_volume) < 0.0001 and self.preferences.volume is not None and abs(self.preferences.volume - clamped_volume) < 0.0001:
|
|
_LOGGER.debug("Skipping save - volume unchanged")
|
|
return
|
|
|
|
self.volume = clamped_volume
|
|
self.preferences.volume = clamped_volume
|
|
_LOGGER.info("Saving volume %s to %s", clamped_volume, self.preferences_path)
|
|
self.save_preferences()
|
|
_LOGGER.info("Volume saved successfully")
|
|
|
|
def persist_mic_gain(self, gain: float) -> None:
|
|
"""Persist the microphone auto gain value."""
|
|
gain_int = int(gain)
|
|
if self.mic_auto_gain == gain_int and self.preferences.mic_auto_gain == gain_int:
|
|
return
|
|
|
|
self.mic_auto_gain = gain_int
|
|
self.preferences.mic_auto_gain = gain_int
|
|
self.save_preferences()
|
|
|
|
def persist_mic_noise(self, noise: float) -> None:
|
|
"""Persist the microphone noise suppression value."""
|
|
noise_int = int(noise)
|
|
if self.mic_noise_suppression == noise_int and self.preferences.mic_noise_suppression == noise_int:
|
|
return
|
|
|
|
self.mic_noise_suppression = noise_int
|
|
self.preferences.mic_noise_suppression = noise_int
|
|
self.save_preferences()
|