fix: make !volume apply immediately during playback

stream_audio now accepts a callable for volume, re-read on each PCM
frame instead of capturing a static float at track start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-21 23:20:17 +01:00
parent eae36aa1f9
commit 67b2dc827d
4 changed files with 16 additions and 10 deletions

View File

@@ -556,8 +556,8 @@ HTML stripped on receive, escaped on send. IRC-only commands are no-ops.
!volume 75 # Set volume (0-100, default 50) !volume 75 # Set volume (0-100, default 50)
``` ```
Requires: `yt-dlp`, `ffmpeg`, `libopus.so.0` on the host. Requires: `yt-dlp`, `ffmpeg`, `libopus` on the host.
Max 50 tracks in queue. Volume applies on next track. Max 50 tracks in queue. Volume changes take effect immediately.
Mumble-only: `!play` replies with error on other adapters, others silently no-op. Mumble-only: `!play` replies with error on other adapters, others silently no-op.
## Plugin Template ## Plugin Template

View File

@@ -1576,7 +1576,7 @@ and voice transmission.
``` ```
- Queue holds up to 50 tracks - Queue holds up to 50 tracks
- Volume applies from the next track (default: 50%) - Volume takes effect immediately during playback (default: 50%)
- Title resolved via `yt-dlp --get-title` before playback - Title resolved via `yt-dlp --get-title` before playback
- Audio pipeline: `yt-dlp | ffmpeg` subprocess, PCM fed to pymumble - Audio pipeline: `yt-dlp | ffmpeg` subprocess, PCM fed to pymumble
- Commands are Mumble-only; `!play` on other adapters replies with an error, - Commands are Mumble-only; `!play` on other adapters replies with an error,

View File

@@ -78,10 +78,11 @@ async def _play_loop(bot) -> None:
done = asyncio.Event() done = asyncio.Event()
ps["done_event"] = done ps["done_event"] = done
volume = ps["volume"] / 100.0
try: try:
await bot.stream_audio( await bot.stream_audio(
track.url, volume=volume, on_done=done, track.url,
volume=lambda: ps["volume"] / 100.0,
on_done=done,
) )
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
@@ -269,7 +270,7 @@ async def cmd_volume(bot, message):
Usage: Usage:
!volume Show current volume !volume Show current volume
!volume <0-100> Set volume (applies on next track) !volume <0-100> Set volume (takes effect immediately)
""" """
if not _is_mumble(bot): if not _is_mumble(bot):
return return

View File

@@ -421,7 +421,7 @@ class MumbleBot:
self, self,
url: str, url: str,
*, *,
volume: float = 0.5, volume=0.5,
on_done=None, on_done=None,
) -> None: ) -> None:
"""Stream audio from URL through yt-dlp|ffmpeg to voice channel. """Stream audio from URL through yt-dlp|ffmpeg to voice channel.
@@ -432,12 +432,16 @@ class MumbleBot:
Feeds raw PCM to pymumble's sound_output which handles Opus Feeds raw PCM to pymumble's sound_output which handles Opus
encoding, packetization, and timing. encoding, packetization, and timing.
``volume`` may be a float (static) or a callable returning float
(dynamic, re-read each frame).
""" """
if self._mumble is None: if self._mumble is None:
return return
_get_vol = volume if callable(volume) else lambda: volume
log.info("stream_audio: starting pipeline for %s (vol=%.0f%%)", log.info("stream_audio: starting pipeline for %s (vol=%.0f%%)",
url, volume * 100) url, _get_vol() * 100)
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"sh", "-c", "sh", "-c",
@@ -457,8 +461,9 @@ class MumbleBot:
if len(pcm) < _FRAME_BYTES: if len(pcm) < _FRAME_BYTES:
pcm += b"\x00" * (_FRAME_BYTES - len(pcm)) pcm += b"\x00" * (_FRAME_BYTES - len(pcm))
if volume != 1.0: vol = _get_vol()
pcm = _scale_pcm(pcm, volume) if vol != 1.0:
pcm = _scale_pcm(pcm, vol)
self._mumble.sound_output.add_sound(pcm) self._mumble.sound_output.add_sound(pcm)
frames += 1 frames += 1