Mute microphone during awake wav
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user