diff --git a/src/derp/mumble.py b/src/derp/mumble.py index 47eb22b..acb3d6e 100644 --- a/src/derp/mumble.py +++ b/src/derp/mumble.py @@ -165,6 +165,8 @@ class MumbleBot: self._connect_count: int = 0 self._sound_listeners: list = [] 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 = ( 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", "?") log.info("mumble: %s as %s on %s:%d (session=%s)", 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: asyncio.run_coroutine_threadsafe( self._notify_plugins_connected(), self._loop, @@ -277,16 +284,18 @@ class MumbleBot: def _on_sound_received(self, user, sound_chunk) -> None: """Callback from pymumble thread: voice audio received. - Updates the timestamp used by the music plugin's duck monitor. - When this callback is registered, pymumble passes decoded PCM - directly and does not queue it -- no memory buildup. + Ignores audio from our own bots entirely -- prevents self-ducking + and avoids STT transcribing bot TTS/music. """ + 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 self._last_voice_ts = time.monotonic() self.registry._voice_ts = self._last_voice_ts if prev == 0.0: - name = user["name"] if isinstance(user, dict) else "?" - log.info("mumble: first voice packet from %s", name) + log.info("mumble: first voice packet from %s", name or "?") for fn in self._sound_listeners: try: fn(user, sound_chunk) @@ -604,6 +613,16 @@ class MumbleBot: if self._mumble is None: 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 log.info("stream_audio: starting pipeline for %s (vol=%.0f%%, seek=%.1fs)", url, _get_vol() * 100, seek) @@ -822,6 +841,19 @@ class MumbleBot: stderr_out.decode(errors="replace")[:500]) if on_done is not None: 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: """Shorten a URL via FlaskPaste. Returns original on failure."""