Add the ability to control microphone volume (#287)

Control microphone volume via UI in device page. Set to the maximum by default and saved in preferences to restore upon restarts.
This commit is contained in:
Omar Amin authored and GitHub committed 2026-04-10 01:33:34 +02:00
1 parent 4ee7bb04bc
commit c284806b73
3 files changed
+43 -1

No files matched your search

+3 -1
View File
@@ -375,6 +375,7 @@ async def main() -> None:
output_only=args.output_only, output_only=args.output_only,
download_dir=args.download_dir, download_dir=args.download_dir,
volume=initial_volume, volume=initial_volume,
mic_volume=preferences.mic_volume,
mic_auto_gain=preferences.mic_auto_gain, mic_auto_gain=preferences.mic_auto_gain,
mic_noise_suppression=preferences.mic_noise_suppression, mic_noise_suppression=preferences.mic_noise_suppression,
timer_max_ring_seconds=args.timer_max_ring_seconds, timer_max_ring_seconds=args.timer_max_ring_seconds,
@@ -455,7 +456,8 @@ def process_audio(state: ServerState, mic, block_size: int):
while True: while True:
audio_chunk_array = mic_in.record(block_size).reshape(-1) audio_chunk_array = mic_in.record(block_size).reshape(-1)
# little-endian 16-bit signed # little-endian 16-bit signed
audio_chunk = (np.clip(audio_chunk_array, -1.0, 1.0) * 32767.0).astype("<i2").tobytes() mic_vol_scalar = max(0.1, min(1.0, state.mic_volume / 100.0))
audio_chunk = (np.clip(audio_chunk_array * mic_vol_scalar, -1.0, 1.0) * 32767.0).astype("<i2").tobytes()
if state.satellite is None: if state.satellite is None:
continue continue
+14
View File
@@ -62,6 +62,7 @@ class Preferences:
thinking_sound: int = 0 # 0 = disabled, 1 = enabled thinking_sound: int = 0 # 0 = disabled, 1 = enabled
mic_auto_gain: int = 0 mic_auto_gain: int = 0
mic_noise_suppression: int = 0 mic_noise_suppression: int = 0
mic_volume: int = 100 # 1–100, default maximum
@dataclass @dataclass
@@ -96,6 +97,7 @@ class ServerState:
thinking_sound_entity: "Optional[ThinkingSoundEntity]" = None thinking_sound_entity: "Optional[ThinkingSoundEntity]" = None
mic_gain_entity: "Optional[MicSettingEntity]" = None mic_gain_entity: "Optional[MicSettingEntity]" = None
mic_noise_suppression_entity: "Optional[MicSettingEntity]" = None mic_noise_suppression_entity: "Optional[MicSettingEntity]" = None
mic_volume_entity: "Optional[MicSettingEntity]" = None
wake_words_changed: bool = False wake_words_changed: bool = False
refractory_seconds: float = 2.0 refractory_seconds: float = 2.0
thinking_sound_enabled: bool = False thinking_sound_enabled: bool = False
@@ -105,6 +107,7 @@ class ServerState:
volume: float = 1.0 volume: float = 1.0
mic_auto_gain: int = 0 mic_auto_gain: int = 0
mic_noise_suppression: int = 0 mic_noise_suppression: int = 0
mic_volume: int = 100 # 1–100, default maximum
timer_max_ring_seconds: float = 900.0 timer_max_ring_seconds: float = 900.0
def save_preferences(self) -> None: def save_preferences(self) -> None:
@@ -158,3 +161,14 @@ class ServerState:
self.mic_noise_suppression = noise_int self.mic_noise_suppression = noise_int
self.preferences.mic_noise_suppression = noise_int self.preferences.mic_noise_suppression = noise_int
self.save_preferences() self.save_preferences()
def persist_mic_volume(self, volume: float) -> None:
"""Persist the microphone input volume (0–100)."""
volume_int = max(1, min(100, int(round(volume))))
if self.mic_volume == volume_int and self.preferences.mic_volume == volume_int:
return
self.mic_volume = volume_int
self.preferences.mic_volume = volume_int
_LOGGER.info("Saving mic_volume %s to %s", volume_int, self.preferences_path)
self.save_preferences()
+26
View File
@@ -213,6 +213,29 @@ class VoiceSatelliteProtocol(APIServer):
self.state.mic_noise_suppression_entity.update_set_value(_set_noise_label) self.state.mic_noise_suppression_entity.update_set_value(_set_noise_label)
self.state.mic_noise_suppression_entity.sync_with_state() self.state.mic_noise_suppression_entity.sync_with_state()
# Mic Volume
if self.state.mic_volume_entity is None:
self.state.mic_volume_entity = MicSettingEntity(
server=self,
key=len(self.state.entities),
name="Mic Volume",
object_id="mic_volume",
min_value=1.0,
max_value=100.0,
get_value=lambda: float(self.state.mic_volume),
set_value=lambda val: self.state.persist_mic_volume(float(val)),
icon="mdi:microphone-settings",
)
self.state.entities.append(self.state.mic_volume_entity)
elif self.state.mic_volume_entity not in self.state.entities:
self.state.entities.append(self.state.mic_volume_entity)
self.state.mic_volume_entity.server = self
self.state.mic_volume_entity.update_get_value(lambda: float(self.state.mic_volume))
self.state.mic_volume_entity.update_set_value(lambda val: self.state.persist_mic_volume(float(val)))
# ---- Instance variables ----
self._is_streaming_audio = False self._is_streaming_audio = False
self._tts_url: Optional[str] = None self._tts_url: Optional[str] = None
self._tts_played = False self._tts_played = False
@@ -605,6 +628,9 @@ class VoiceSatelliteProtocol(APIServer):
if self.state.mic_noise_suppression_entity is not None: if self.state.mic_noise_suppression_entity is not None:
self.state.mic_noise_suppression_entity.sync_with_state() self.state.mic_noise_suppression_entity.sync_with_state()
if self.state.mic_volume_entity is not None:
self.state.mic_volume_entity.sync_with_state()
_LOGGER.info("Disconnected from Home Assistant; waiting for reconnection") _LOGGER.info("Disconnected from Home Assistant; waiting for reconnection")
def process_packet(self, msg_type: int, packet_data: bytes) -> None: def process_packet(self, msg_type: int, packet_data: bytes) -> None: