feat: ack tone, duck-before-TTS, instant ducking on voice/unmute
- Add ascending two-tone chime (880Hz/1320Hz) before TTS playback as audible acknowledgment that the voice trigger was recognized - Signal music ducking 1.5s before TTS starts so music is already lowered when audio begins playing - Snap duck volume to floor instantly on voice packet or user unmute via pymumble callback, eliminating the 1s poll delay - Register USERUPDATED callback to preemptively duck when a user unmutes (they're about to speak) - Strip leading punctuation from trigger remainder (Whisper artifacts)
This commit is contained in:
@@ -11,6 +11,8 @@ import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
@@ -83,6 +85,50 @@ def _pcm_to_wav(pcm: bytes) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# -- Acknowledge tone --------------------------------------------------------
|
||||
|
||||
_ACK_FREQ = (880, 1320) # A5 -> E6 ascending
|
||||
_ACK_NOTE_DUR = 0.15 # seconds per note
|
||||
_ACK_AMP = 12000 # gentle amplitude
|
||||
_ACK_FRAME = 960 # 20ms at 48kHz, matches Mumble native
|
||||
|
||||
|
||||
async def _ack_tone(bot) -> None:
|
||||
"""Play a short two-tone ascending chime via pymumble sound_output."""
|
||||
mu = getattr(bot, "_mumble", None)
|
||||
if mu is None:
|
||||
return
|
||||
so = mu.sound_output
|
||||
if so is None:
|
||||
return
|
||||
|
||||
# Unmute if self-muted (stream_audio handles re-mute later)
|
||||
if getattr(bot, "_self_mute", False):
|
||||
if bot._mute_task and not bot._mute_task.done():
|
||||
bot._mute_task.cancel()
|
||||
bot._mute_task = None
|
||||
try:
|
||||
mu.users.myself.unmute()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
frames_per_note = int(_ACK_NOTE_DUR / 0.02) # 0.02s per frame
|
||||
for freq in _ACK_FREQ:
|
||||
for i in range(frames_per_note):
|
||||
samples = []
|
||||
for j in range(_ACK_FRAME):
|
||||
t = (i * _ACK_FRAME + j) / _SAMPLE_RATE
|
||||
samples.append(int(_ACK_AMP * math.sin(2 * math.pi * freq * t)))
|
||||
pcm = struct.pack(f"<{_ACK_FRAME}h", *samples)
|
||||
so.add_sound(pcm)
|
||||
while so.get_buffer_size() > 0.5:
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
# Wait for tone to finish
|
||||
while so.get_buffer_size() > 0:
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
|
||||
# -- STT: Sound listener (pymumble thread) ----------------------------------
|
||||
|
||||
|
||||
@@ -170,7 +216,7 @@ async def _flush_monitor(bot):
|
||||
|
||||
trigger = ps["trigger"]
|
||||
if trigger and text.lower().startswith(trigger.lower()):
|
||||
remainder = text[len(trigger):].strip()
|
||||
remainder = text[len(trigger):].strip().lstrip(",.;:!?")
|
||||
if remainder:
|
||||
log.info("voice: trigger from %s: %s", name, remainder)
|
||||
bot._spawn(
|
||||
@@ -243,8 +289,10 @@ async def _tts_play(bot, text: str):
|
||||
if wav_path is None:
|
||||
return
|
||||
try:
|
||||
# Signal music plugin to duck while TTS is playing
|
||||
# Signal music plugin to duck, wait for it to take effect
|
||||
bot.registry._tts_active = True
|
||||
await asyncio.sleep(1.5)
|
||||
await _ack_tone(bot)
|
||||
done = asyncio.Event()
|
||||
await bot.stream_audio(str(wav_path), volume=1.0, on_done=done)
|
||||
await done.wait()
|
||||
|
||||
Reference in New Issue
Block a user