Mute microphone during awake wav

This commit is contained in:
Michael Hansen
2024-01-16 17:06:30 -06:00
parent ebc686d9ad
commit 73604f9a71
6 changed files with 78 additions and 34 deletions
+3 -3
View File
@@ -14,10 +14,10 @@ from wyoming.asr import Transcript
from wyoming.event import Event from wyoming.event import Event
from wyoming.satellite import ( from wyoming.satellite import (
RunSatellite, RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected, SatelliteConnected,
SatelliteDisconnected, SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
) )
from wyoming.server import AsyncEventHandler, AsyncServer from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.vad import VoiceStarted from wyoming.vad import VoiceStarted
@@ -115,7 +115,7 @@ class LEDsEventHandler(AsyncEventHandler):
self.color(_BLACK) self.color(_BLACK)
elif SatelliteConnected.is_type(event.type): elif SatelliteConnected.is_type(event.type):
# Flash # Flash
for _ in range(5): for _ in range(3):
self.color(_GREEN) self.color(_GREEN)
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
self.color(_BLACK) self.color(_BLACK)
+3 -3
View File
@@ -14,10 +14,10 @@ from wyoming.asr import Transcript
from wyoming.event import Event from wyoming.event import Event
from wyoming.satellite import ( from wyoming.satellite import (
RunSatellite, RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected, SatelliteConnected,
SatelliteDisconnected, SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
) )
from wyoming.server import AsyncEventHandler, AsyncServer from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.vad import VoiceStarted from wyoming.vad import VoiceStarted
@@ -115,7 +115,7 @@ class LEDsEventHandler(AsyncEventHandler):
self.color(_BLACK) self.color(_BLACK)
elif SatelliteConnected.is_type(event.type): elif SatelliteConnected.is_type(event.type):
# Flash # Flash
for _ in range(5): for _ in range(3):
self.color(_GREEN) self.color(_GREEN)
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
self.color(_BLACK) self.color(_BLACK)
+5 -4
View File
@@ -119,12 +119,13 @@ def apply_settings(settings: Settings) -> None:
error("installing audio enhancements") error("installing audio enhancements")
return return
if settings.satellite.event_service_command and (("2mic" in settings.satellite.event_service_command) or ("4mic" in settings.satellite.event_service_command)): if settings.satellite.event_service_command and (
("2mic" in settings.satellite.event_service_command)
or ("4mic" in settings.satellite.event_service_command)
):
result = run_with_gauge( result = run_with_gauge(
"Installing event requirements...", "Installing event requirements...",
[ [pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))],
pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))
],
) )
if not result: if not result:
error("installing event requirements") error("installing event requirements")
+1 -1
View File
@@ -1,7 +1,7 @@
"""Satellite settings.""" """Satellite settings."""
from typing import Optional from typing import Optional
from .const import SatelliteType, Settings, PROGRAM_DIR from .const import PROGRAM_DIR, SatelliteType, Settings
from .whiptail import error, inputbox, menu, passwordbox, radiolist, run_with_gauge from .whiptail import error, inputbox, menu, passwordbox, radiolist, run_with_gauge
+60 -23
View File
@@ -3,9 +3,10 @@ import asyncio
import logging import logging
import math import math
import time import time
import wave
from enum import Enum, auto from enum import Enum, auto
from pathlib import Path from pathlib import Path
from typing import Callable, Dict, Optional, Set, Union, Final from typing import Callable, Dict, Final, Optional, Set, Union
from pyring_buffer import RingBuffer from pyring_buffer import RingBuffer
from wyoming.asr import Transcript from wyoming.asr import Transcript
@@ -14,19 +15,19 @@ from wyoming.client import AsyncClient
from wyoming.error import Error from wyoming.error import Error
from wyoming.event import Event, async_write_event from wyoming.event import Event, async_write_event
from wyoming.mic import MicProcessAsyncClient from wyoming.mic import MicProcessAsyncClient
from wyoming.ping import Ping, Pong
from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import ( from wyoming.satellite import (
RunSatellite, RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected, SatelliteConnected,
SatelliteDisconnected, SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
) )
from wyoming.snd import SndProcessAsyncClient from wyoming.snd import SndProcessAsyncClient
from wyoming.tts import Synthesize from wyoming.tts import Synthesize
from wyoming.vad import VoiceStarted, VoiceStopped from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detect, Detection, WakeProcessAsyncClient from wyoming.wake import Detect, Detection, WakeProcessAsyncClient
from wyoming.ping import Ping, Pong
from .settings import SatelliteSettings from .settings import SatelliteSettings
from .utils import DebugAudioWriter, multiply_volume, run_event_command, wav_to_events from .utils import DebugAudioWriter, multiply_volume, run_event_command, wav_to_events
@@ -73,6 +74,9 @@ class SatelliteBase:
self._pong_received_event = asyncio.Event() self._pong_received_event = asyncio.Event()
self._ping_server_task = asyncio.create_task(self._ping_server(), name="ping") self._ping_server_task = asyncio.create_task(self._ping_server(), name="ping")
self.microphone_muted = False
self._unmute_microphone_task: Optional[asyncio.Task] = None
# Debug audio recording # Debug audio recording
self.wake_audio_writer: Optional[DebugAudioWriter] = None self.wake_audio_writer: Optional[DebugAudioWriter] = None
self.stt_audio_writer: Optional[DebugAudioWriter] = None self.stt_audio_writer: Optional[DebugAudioWriter] = None
@@ -545,17 +549,41 @@ class SatelliteBase:
return audio_bytes return audio_bytes
async def _play_wav(self, wav_path: Optional[Union[str, Path]]) -> None: async def _play_wav(
self, wav_path: Optional[Union[str, Path]], mute_microphone: bool = False
) -> None:
"""Send WAV as events to sound service.""" """Send WAV as events to sound service."""
if (not wav_path) or (not self.settings.snd.enabled): if (not wav_path) or (not self.settings.snd.enabled):
return return
for event in wav_to_events( try:
wav_path, if mute_microphone:
samples_per_chunk=self.settings.snd.samples_per_chunk, with wave.open(wav_path, "rb") as wav_file:
volume_multiplier=self.settings.snd.volume_multiplier, seconds_to_mute = wav_file.getnframes() / wav_file.getframerate()
):
await self.event_to_snd(event) seconds_to_mute += self.settings.mic.seconds_to_mute_after_awake_wav
_LOGGER.debug("Muting microphone for %s second(s)", seconds_to_mute)
self.microphone_muted = True
self._unmute_microphone_task = asyncio.create_task(
self._unmute_microphone_after(seconds_to_mute)
)
for event in wav_to_events(
wav_path,
samples_per_chunk=self.settings.snd.samples_per_chunk,
volume_multiplier=self.settings.snd.volume_multiplier,
):
await self.event_to_snd(event)
except Exception:
# Unmute in case of an error
self.microphone_muted = False
raise
async def _unmute_microphone_after(self, seconds: float) -> None:
await asyncio.sleep(seconds)
self.microphone_muted = False
_LOGGER.debug("Unmuted microphone")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Wake # Wake
@@ -719,7 +747,10 @@ class SatelliteBase:
async def trigger_detection(self, detection: Detection) -> None: async def trigger_detection(self, detection: Detection) -> None:
"""Called when wake word is detected.""" """Called when wake word is detected."""
await run_event_command(self.settings.event.detection, detection.name) await run_event_command(self.settings.event.detection, detection.name)
await self._play_wav(self.settings.snd.awake_wav) await self._play_wav(
self.settings.snd.awake_wav,
mute_microphone=self.settings.mic.mute_during_awake_wav,
)
async def trigger_transcript(self, transcript: Transcript) -> None: async def trigger_transcript(self, transcript: Transcript) -> None:
"""Called when speech-to-text text is received.""" """Called when speech-to-text text is received."""
@@ -827,22 +858,23 @@ class AlwaysStreamingSatellite(SatelliteBase):
# Start debug recording # Start debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
self.stt_audio_writer.start() self.stt_audio_writer.start()
elif Transcript.is_type(event.type): elif Transcript.is_type(event.type) or Error.is_type(event.type):
# Stop debug recording # Stop debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
self.stt_audio_writer.stop() self.stt_audio_writer.stop()
# We're always streaming if Transcript.is_type(event.type):
_LOGGER.info("Streaming audio") # We're always streaming
_LOGGER.info("Streaming audio")
# Re-trigger streaming start even though we technically don't stop # Re-trigger streaming start even though we technically don't stop
# so the event service can reset LEDs, etc. # so the event service can reset LEDs, etc.
await self.trigger_streaming_start() await self.trigger_streaming_start()
async def event_from_mic( async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None self, event: Event, audio_bytes: Optional[bytes] = None
) -> None: ) -> None:
if not self.is_streaming: if (not self.is_streaming) or self.microphone_muted:
return return
if AudioChunk.is_type(event.type): if AudioChunk.is_type(event.type):
@@ -898,7 +930,9 @@ class VadStreamingSatellite(SatelliteBase):
# Start debug recording # Start debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
self.stt_audio_writer.start() self.stt_audio_writer.start()
elif Transcript.is_type(event.type): elif Transcript.is_type(event.type) or Error.is_type(event.type):
self.is_streaming = False
# Stop debug recording # Stop debug recording
if self.stt_audio_writer is not None: if self.stt_audio_writer is not None:
self.stt_audio_writer.stop() self.stt_audio_writer.stop()
@@ -906,7 +940,7 @@ class VadStreamingSatellite(SatelliteBase):
async def event_from_mic( async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None self, event: Event, audio_bytes: Optional[bytes] = None
) -> None: ) -> None:
if not AudioChunk.is_type(event.type): if (not AudioChunk.is_type(event.type)) or self.microphone_muted:
return return
# Only unpack chunk once # Only unpack chunk once
@@ -1031,11 +1065,14 @@ class WakeStreamingSatellite(SatelliteBase):
# Only check event types once # Only check event types once
is_run_satellite = False is_run_satellite = False
is_transcript = False is_transcript = False
is_error = False
if RunSatellite.is_type(event.type): if RunSatellite.is_type(event.type):
is_run_satellite = True is_run_satellite = True
elif Transcript.is_type(event.type): elif Transcript.is_type(event.type):
is_transcript = True is_transcript = True
elif Error.is_type(event.type):
is_error = True
if is_transcript: if is_transcript:
# Stop streaming before event_from_server is called because it will # Stop streaming before event_from_server is called because it will
@@ -1048,7 +1085,7 @@ class WakeStreamingSatellite(SatelliteBase):
await super().event_from_server(event) await super().event_from_server(event)
if is_run_satellite or is_transcript: if is_run_satellite or is_transcript or is_error:
# Stop streaming and go back to wake word detection # Stop streaming and go back to wake word detection
self.is_streaming = False self.is_streaming = False
await self.trigger_streaming_stop() await self.trigger_streaming_stop()
@@ -1074,7 +1111,7 @@ class WakeStreamingSatellite(SatelliteBase):
async def event_from_mic( async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None self, event: Event, audio_bytes: Optional[bytes] = None
) -> None: ) -> None:
if not AudioChunk.is_type(event.type): if (not AudioChunk.is_type(event.type)) or self.microphone_muted:
return return
# Debug audio recording # Debug audio recording
+6
View File
@@ -49,6 +49,12 @@ class MicSettings(ServiceSettings):
samples_per_chunk: int = 1024 samples_per_chunk: int = 1024
"""Samples to read at a time from mic command""" """Samples to read at a time from mic command"""
mute_during_awake_wav: bool = True
"""True if microphone audio should be muted while the awake WAV is playing."""
seconds_to_mute_after_awake_wav: float = 0.5
"""Extra second(s) of microphone audio to mute after awake WAV has finished playing."""
@property @property
def needs_webrtc(self) -> bool: def needs_webrtc(self) -> bool:
"""True if webrtc audio enhancements are needed.""" """True if webrtc audio enhancements are needed."""