Replace deprecated audioop with NumPy and add silent join sounds
- Replace audioop module (removed in Python 3.13) with NumPy - Implement _apply_volume() method using NumPy for volume adjustment - Update RMS calculation in ducking_sound_received() to use NumPy - Add numpy to requirements.txt - Add silent flag to CachedItemWrapper for join sounds - Join sounds now play without channel announcements - Regular playlist items still respect announce_current_music config - Update CLAUDE.md to document changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
40
bragi.py
40
bragi.py
@@ -15,7 +15,7 @@ import sys
|
||||
import math
|
||||
import signal
|
||||
import configparser
|
||||
import audioop
|
||||
import numpy as np
|
||||
import subprocess as sp
|
||||
import argparse
|
||||
import os.path
|
||||
@@ -427,7 +427,7 @@ class MumbleBot:
|
||||
if sound_config.startswith('http://') or sound_config.startswith('https://'):
|
||||
# It's a URL
|
||||
self.log.info(f'bot: Playing join sound URL for {username}: {sound_config}')
|
||||
music_wrapper = get_cached_wrapper_from_scrap(type='url', url=sound_config, user='system')
|
||||
music_wrapper = get_cached_wrapper_from_scrap(type='url', url=sound_config, user='system', silent=True)
|
||||
self.log.info(f'bot: Successfully created music wrapper for {username}')
|
||||
else:
|
||||
# It's a file path - search database for first match
|
||||
@@ -440,7 +440,7 @@ class MumbleBot:
|
||||
|
||||
# Use first match
|
||||
self.log.info(f'bot: Playing join sound file for {username}: {matches[0]["path"]}')
|
||||
music_wrapper = get_cached_wrapper_from_dict(matches[0], 'system')
|
||||
music_wrapper = get_cached_wrapper_from_dict(matches[0], 'system', silent=True)
|
||||
self.log.info(f'bot: Successfully created music wrapper for {username}')
|
||||
|
||||
# Add to playlist
|
||||
@@ -509,7 +509,8 @@ class MumbleBot:
|
||||
|
||||
self.log.info("bot: play music " + music_wrapper.format_debug_string())
|
||||
|
||||
if var.config.getboolean('bot', 'announce_current_music'):
|
||||
# Only announce if configured and not a silent item (e.g., join sounds)
|
||||
if var.config.getboolean('bot', 'announce_current_music') and not music_wrapper.silent:
|
||||
self.send_channel_msg(music_wrapper.format_current_playing())
|
||||
|
||||
if var.config.getboolean('debug', 'ffmpeg'):
|
||||
@@ -628,13 +629,13 @@ class MumbleBot:
|
||||
|
||||
if not self.on_interrupting and len(raw_music) == self.pcm_buffer_size:
|
||||
self.mumble.sound_output.add_sound(
|
||||
audioop.mul(raw_music, 2, self.volume_helper.real_volume))
|
||||
self._apply_volume(raw_music, self.volume_helper.real_volume))
|
||||
elif self.read_pcm_size == 0:
|
||||
self.mumble.sound_output.add_sound(
|
||||
audioop.mul(self._fadeout(raw_music, self.stereo, fadein=True), 2, self.volume_helper.real_volume))
|
||||
self._apply_volume(self._fadeout(raw_music, self.stereo, fadein=True), self.volume_helper.real_volume))
|
||||
elif self.on_interrupting or len(raw_music) < self.pcm_buffer_size:
|
||||
self.mumble.sound_output.add_sound(
|
||||
audioop.mul(self._fadeout(raw_music, self.stereo, fadein=False), 2, self.volume_helper.real_volume))
|
||||
self._apply_volume(self._fadeout(raw_music, self.stereo, fadein=False), self.volume_helper.real_volume))
|
||||
self._cleanup_ffmpeg_process()
|
||||
time.sleep(0.1)
|
||||
self.on_interrupting = False
|
||||
@@ -728,7 +729,9 @@ class MumbleBot:
|
||||
self.last_volume_cycle_time = time.time()
|
||||
|
||||
def ducking_sound_received(self, user, sound):
|
||||
rms = audioop.rms(sound.pcm, 2)
|
||||
# Calculate RMS (root mean square) for volume ducking
|
||||
audio_array = np.frombuffer(sound.pcm, dtype=np.int16)
|
||||
rms = int(np.sqrt(np.mean(audio_array.astype(np.float64) ** 2)))
|
||||
self._max_rms = max(rms, self._max_rms)
|
||||
if self._display_rms:
|
||||
if rms < self.ducking_threshold:
|
||||
@@ -743,6 +746,27 @@ class MumbleBot:
|
||||
self.on_ducking = True
|
||||
self.ducking_release = time.time() + 1 # ducking release after 1s
|
||||
|
||||
def _apply_volume(self, pcm_data, volume):
|
||||
"""Apply volume adjustment to 16-bit PCM audio data.
|
||||
|
||||
Replaces deprecated audioop.mul() with modern NumPy implementation.
|
||||
|
||||
Args:
|
||||
pcm_data: bytes containing 16-bit signed PCM samples
|
||||
volume: float multiplier (0.0 to 1.0+)
|
||||
|
||||
Returns:
|
||||
bytes with volume-adjusted audio
|
||||
"""
|
||||
# Convert bytes to numpy array of 16-bit signed integers
|
||||
audio_array = np.frombuffer(pcm_data, dtype=np.int16)
|
||||
|
||||
# Apply volume (multiply and clip to prevent overflow)
|
||||
adjusted = np.clip(audio_array * volume, -32768, 32767).astype(np.int16)
|
||||
|
||||
# Convert back to bytes
|
||||
return adjusted.tobytes()
|
||||
|
||||
def _fadeout(self, _pcm_data, stereo=False, fadein=False):
|
||||
pcm_data = bytearray(_pcm_data)
|
||||
if stereo:
|
||||
|
||||
@@ -139,11 +139,12 @@ class MusicCache(dict):
|
||||
|
||||
|
||||
class CachedItemWrapper:
|
||||
def __init__(self, lib, id, type, user):
|
||||
def __init__(self, lib, id, type, user, silent=False):
|
||||
self.lib = lib
|
||||
self.id = id
|
||||
self.user = user
|
||||
self.type = type
|
||||
self.silent = silent # If True, don't announce when playing (for join sounds)
|
||||
self.log = logging.getLogger("bot")
|
||||
self.version = 0
|
||||
|
||||
@@ -216,10 +217,10 @@ class CachedItemWrapper:
|
||||
|
||||
|
||||
# Remember!!! Get wrapper functions will automatically add items into the cache!
|
||||
def get_cached_wrapper(item, user):
|
||||
def get_cached_wrapper(item, user, silent=False):
|
||||
if item:
|
||||
var.cache[item.id] = item
|
||||
return CachedItemWrapper(var.cache, item.id, item.type, user)
|
||||
return CachedItemWrapper(var.cache, item.id, item.type, user, silent=silent)
|
||||
return None
|
||||
|
||||
def get_cached_wrappers(items, user):
|
||||
@@ -234,12 +235,13 @@ def get_cached_wrapper_from_scrap(**kwargs):
|
||||
item = var.cache.get_item(**kwargs)
|
||||
if 'user' not in kwargs:
|
||||
raise KeyError("Which user added this song?")
|
||||
return CachedItemWrapper(var.cache, item.id, kwargs['type'], kwargs['user'])
|
||||
silent = kwargs.get('silent', False)
|
||||
return CachedItemWrapper(var.cache, item.id, kwargs['type'], kwargs['user'], silent=silent)
|
||||
|
||||
def get_cached_wrapper_from_dict(dict_from_db, user):
|
||||
def get_cached_wrapper_from_dict(dict_from_db, user, silent=False):
|
||||
if dict_from_db:
|
||||
item = dict_to_item(dict_from_db)
|
||||
return get_cached_wrapper(item, user)
|
||||
return get_cached_wrapper(item, user, silent=silent)
|
||||
return None
|
||||
|
||||
def get_cached_wrappers_from_dicts(dicts_from_db, user):
|
||||
@@ -250,10 +252,10 @@ def get_cached_wrappers_from_dicts(dicts_from_db, user):
|
||||
|
||||
return items
|
||||
|
||||
def get_cached_wrapper_by_id(id, user):
|
||||
def get_cached_wrapper_by_id(id, user, silent=False):
|
||||
item = var.cache.get_item_by_id(id)
|
||||
if item:
|
||||
return CachedItemWrapper(var.cache, item.id, item.type, user)
|
||||
return CachedItemWrapper(var.cache, item.id, item.type, user, silent=silent)
|
||||
|
||||
def get_cached_wrappers_by_tags(tags, user):
|
||||
items = var.cache.get_items_by_tags(tags)
|
||||
|
||||
@@ -5,4 +5,5 @@ mutagen
|
||||
requests
|
||||
packaging
|
||||
pyradios
|
||||
opuslib==3.0.1
|
||||
opuslib==3.0.1
|
||||
numpy
|
||||
Reference in New Issue
Block a user