Skip to content

Commit

Permalink
Mute microphone during awake wav
Browse files Browse the repository at this point in the history
  • Loading branch information
synesthesiam committed Jan 16, 2024
1 parent ebc686d commit 73604f9
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 34 deletions.
6 changes: 3 additions & 3 deletions examples/2mic_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from wyoming.event import Event
from wyoming.satellite import (
RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected,
SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
)
from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.vad import VoiceStarted
Expand Down Expand Up @@ -115,7 +115,7 @@ async def handle_event(self, event: Event) -> bool:
self.color(_BLACK)
elif SatelliteConnected.is_type(event.type):
# Flash
for _ in range(5):
for _ in range(3):
self.color(_GREEN)
await asyncio.sleep(0.3)
self.color(_BLACK)
Expand Down
6 changes: 3 additions & 3 deletions examples/4mic_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from wyoming.event import Event
from wyoming.satellite import (
RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected,
SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
)
from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.vad import VoiceStarted
Expand Down Expand Up @@ -115,7 +115,7 @@ async def handle_event(self, event: Event) -> bool:
self.color(_BLACK)
elif SatelliteConnected.is_type(event.type):
# Flash
for _ in range(5):
for _ in range(3):
self.color(_GREEN)
await asyncio.sleep(0.3)
self.color(_BLACK)
Expand Down
9 changes: 5 additions & 4 deletions installer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,13 @@ def apply_settings(settings: Settings) -> None:
error("installing audio enhancements")
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(
"Installing event requirements...",
[
pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))
],
[pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))],
)
if not result:
error("installing event requirements")
Expand Down
2 changes: 1 addition & 1 deletion installer/satellite.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Satellite settings."""
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


Expand Down
83 changes: 60 additions & 23 deletions wyoming_satellite/satellite.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import logging
import math
import time
import wave
from enum import Enum, auto
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 wyoming.asr import Transcript
Expand All @@ -14,19 +15,19 @@
from wyoming.error import Error
from wyoming.event import Event, async_write_event
from wyoming.mic import MicProcessAsyncClient
from wyoming.ping import Ping, Pong
from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import (
RunSatellite,
StreamingStarted,
StreamingStopped,
SatelliteConnected,
SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
)
from wyoming.snd import SndProcessAsyncClient
from wyoming.tts import Synthesize
from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detect, Detection, WakeProcessAsyncClient
from wyoming.ping import Ping, Pong

from .settings import SatelliteSettings
from .utils import DebugAudioWriter, multiply_volume, run_event_command, wav_to_events
Expand Down Expand Up @@ -73,6 +74,9 @@ def __init__(self, settings: SatelliteSettings) -> None:
self._pong_received_event = asyncio.Event()
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
self.wake_audio_writer: Optional[DebugAudioWriter] = None
self.stt_audio_writer: Optional[DebugAudioWriter] = None
Expand Down Expand Up @@ -545,17 +549,41 @@ def _process_snd_audio(self, audio_bytes: bytes) -> 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."""
if (not wav_path) or (not self.settings.snd.enabled):
return

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)
try:
if mute_microphone:
with wave.open(wav_path, "rb") as wav_file:
seconds_to_mute = wav_file.getnframes() / wav_file.getframerate()

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
Expand Down Expand Up @@ -719,7 +747,10 @@ async def trigger_detect(self) -> None:
async def trigger_detection(self, detection: Detection) -> None:
"""Called when wake word is detected."""
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:
"""Called when speech-to-text text is received."""
Expand Down Expand Up @@ -827,22 +858,23 @@ async def event_from_server(self, event: Event) -> None:
# Start debug recording
if self.stt_audio_writer is not None:
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
if self.stt_audio_writer is not None:
self.stt_audio_writer.stop()

# We're always streaming
_LOGGER.info("Streaming audio")
if Transcript.is_type(event.type):
# We're always streaming
_LOGGER.info("Streaming audio")

# Re-trigger streaming start even though we technically don't stop
# so the event service can reset LEDs, etc.
await self.trigger_streaming_start()
# Re-trigger streaming start even though we technically don't stop
# so the event service can reset LEDs, etc.
await self.trigger_streaming_start()

async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None
) -> None:
if not self.is_streaming:
if (not self.is_streaming) or self.microphone_muted:
return

if AudioChunk.is_type(event.type):
Expand Down Expand Up @@ -898,15 +930,17 @@ async def event_from_server(self, event: Event) -> None:
# Start debug recording
if self.stt_audio_writer is not None:
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
if self.stt_audio_writer is not None:
self.stt_audio_writer.stop()

async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None
) -> None:
if not AudioChunk.is_type(event.type):
if (not AudioChunk.is_type(event.type)) or self.microphone_muted:
return

# Only unpack chunk once
Expand Down Expand Up @@ -1031,11 +1065,14 @@ async def event_from_server(self, event: Event) -> None:
# Only check event types once
is_run_satellite = False
is_transcript = False
is_error = False

if RunSatellite.is_type(event.type):
is_run_satellite = True
elif Transcript.is_type(event.type):
is_transcript = True
elif Error.is_type(event.type):
is_error = True

if is_transcript:
# Stop streaming before event_from_server is called because it will
Expand All @@ -1048,7 +1085,7 @@ async def event_from_server(self, event: Event) -> None:

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
self.is_streaming = False
await self.trigger_streaming_stop()
Expand All @@ -1074,7 +1111,7 @@ async def trigger_server_disonnected(self) -> None:
async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None
) -> None:
if not AudioChunk.is_type(event.type):
if (not AudioChunk.is_type(event.type)) or self.microphone_muted:
return

# Debug audio recording
Expand Down
6 changes: 6 additions & 0 deletions wyoming_satellite/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ class MicSettings(ServiceSettings):
samples_per_chunk: int = 1024
"""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
def needs_webrtc(self) -> bool:
"""True if webrtc audio enhancements are needed."""
Expand Down

0 comments on commit 73604f9

Please sign in to comment.