fix: ignore bot audio in sound callback, self-mute support
- _on_sound_received skips audio from our own bots entirely, preventing self-ducking and STT transcribing bot TTS/music - New self_mute config: mute on connect, unmute before stream_audio, re-mute 3s after audio finishes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,8 @@ class MumbleBot:
|
|||||||
self._connect_count: int = 0
|
self._connect_count: int = 0
|
||||||
self._sound_listeners: list = []
|
self._sound_listeners: list = []
|
||||||
self._receive_sound: bool = mu_cfg.get("receive_sound", True)
|
self._receive_sound: bool = mu_cfg.get("receive_sound", True)
|
||||||
|
self._self_mute: bool = mu_cfg.get("self_mute", False)
|
||||||
|
self._mute_task: asyncio.Task | None = None
|
||||||
self._only_plugins: set[str] | None = (
|
self._only_plugins: set[str] | None = (
|
||||||
set(mu_cfg["only_plugins"]) if "only_plugins" in mu_cfg else None
|
set(mu_cfg["only_plugins"]) if "only_plugins" in mu_cfg else None
|
||||||
)
|
)
|
||||||
@@ -225,6 +227,11 @@ class MumbleBot:
|
|||||||
session = getattr(self._mumble.users, "myself_session", "?")
|
session = getattr(self._mumble.users, "myself_session", "?")
|
||||||
log.info("mumble: %s as %s on %s:%d (session=%s)",
|
log.info("mumble: %s as %s on %s:%d (session=%s)",
|
||||||
kind, self._username, self._host, self._port, session)
|
kind, self._username, self._host, self._port, session)
|
||||||
|
if self._self_mute:
|
||||||
|
try:
|
||||||
|
self._mumble.users.myself.mute()
|
||||||
|
except Exception:
|
||||||
|
log.exception("mumble: failed to self-mute on connect")
|
||||||
if self._loop:
|
if self._loop:
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self._notify_plugins_connected(), self._loop,
|
self._notify_plugins_connected(), self._loop,
|
||||||
@@ -277,16 +284,18 @@ class MumbleBot:
|
|||||||
def _on_sound_received(self, user, sound_chunk) -> None:
|
def _on_sound_received(self, user, sound_chunk) -> None:
|
||||||
"""Callback from pymumble thread: voice audio received.
|
"""Callback from pymumble thread: voice audio received.
|
||||||
|
|
||||||
Updates the timestamp used by the music plugin's duck monitor.
|
Ignores audio from our own bots entirely -- prevents self-ducking
|
||||||
When this callback is registered, pymumble passes decoded PCM
|
and avoids STT transcribing bot TTS/music.
|
||||||
directly and does not queue it -- no memory buildup.
|
|
||||||
"""
|
"""
|
||||||
|
name = user["name"] if isinstance(user, dict) else None
|
||||||
|
bots = getattr(self.registry, "_bots", {})
|
||||||
|
if name and name in bots:
|
||||||
|
return
|
||||||
prev = self._last_voice_ts
|
prev = self._last_voice_ts
|
||||||
self._last_voice_ts = time.monotonic()
|
self._last_voice_ts = time.monotonic()
|
||||||
self.registry._voice_ts = self._last_voice_ts
|
self.registry._voice_ts = self._last_voice_ts
|
||||||
if prev == 0.0:
|
if prev == 0.0:
|
||||||
name = user["name"] if isinstance(user, dict) else "?"
|
log.info("mumble: first voice packet from %s", name or "?")
|
||||||
log.info("mumble: first voice packet from %s", name)
|
|
||||||
for fn in self._sound_listeners:
|
for fn in self._sound_listeners:
|
||||||
try:
|
try:
|
||||||
fn(user, sound_chunk)
|
fn(user, sound_chunk)
|
||||||
@@ -604,6 +613,16 @@ class MumbleBot:
|
|||||||
if self._mumble is None:
|
if self._mumble is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Unmute before streaming if self_mute is enabled
|
||||||
|
if self._self_mute:
|
||||||
|
if self._mute_task and not self._mute_task.done():
|
||||||
|
self._mute_task.cancel()
|
||||||
|
self._mute_task = None
|
||||||
|
try:
|
||||||
|
self._mumble.users.myself.unmute()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
_get_vol = volume if callable(volume) else lambda: volume
|
_get_vol = volume if callable(volume) else lambda: volume
|
||||||
log.info("stream_audio: starting pipeline for %s (vol=%.0f%%, seek=%.1fs)",
|
log.info("stream_audio: starting pipeline for %s (vol=%.0f%%, seek=%.1fs)",
|
||||||
url, _get_vol() * 100, seek)
|
url, _get_vol() * 100, seek)
|
||||||
@@ -822,6 +841,19 @@ class MumbleBot:
|
|||||||
stderr_out.decode(errors="replace")[:500])
|
stderr_out.decode(errors="replace")[:500])
|
||||||
if on_done is not None:
|
if on_done is not None:
|
||||||
on_done.set()
|
on_done.set()
|
||||||
|
# Re-mute after audio finishes
|
||||||
|
if self._self_mute:
|
||||||
|
self._mute_task = self._spawn(
|
||||||
|
self._delayed_mute(3.0), name="self-mute",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _delayed_mute(self, delay: float) -> None:
|
||||||
|
"""Re-mute after a delay (lets the audio buffer drain fully)."""
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
try:
|
||||||
|
self._mumble.users.myself.mute()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async def shorten_url(self, url: str) -> str:
|
async def shorten_url(self, url: str) -> str:
|
||||||
"""Shorten a URL via FlaskPaste. Returns original on failure."""
|
"""Shorten a URL via FlaskPaste. Returns original on failure."""
|
||||||
|
|||||||
Reference in New Issue
Block a user