diff --git a/app/__init__.py b/app/__init__.py index a211020..bc28b1e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -32,7 +32,8 @@ def create_app(): # pygame initialisieren (einmalig) pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) - pygame.mixer.music.set_volume(0.8) # Standard-Lautstärke + pygame.mixer.set_num_channels(16) # genug parallele Kanäle für BG + Effekte + pygame.mixer.music.set_volume(0.8) # Standard-Lautstärke (für Legacy-Fälle) app.logger.info("pygame initialisiert") # Blueprints registrieren diff --git a/app/routes/api.py b/app/routes/api.py index 6666aa9..f115950 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -12,6 +12,22 @@ from app.bluetooth.manager import MouldKing, advertiser, tracer from app.utils.helpers import load_default_sounds logger = logging.getLogger(__name__) +audio_lock = threading.Lock() + +# Sound-Cache: path -> pygame.mixer.Sound +loaded_sounds = {} + +def _ensure_mixer(): + if not pygame.mixer.get_init(): + pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) + pygame.mixer.set_num_channels(16) + +def _load_sound(file_path): + if file_path in loaded_sounds: + return loaded_sounds[file_path] + snd = pygame.mixer.Sound(file_path) + loaded_sounds[file_path] = snd + return snd api_bp = Blueprint('api', __name__) @@ -198,6 +214,8 @@ def api_play_sound(): try: data = request.get_json() sound_id = data.get('sound_id') + channel_req = data.get('channel') # optional: spezifischer Channel + loop_req = data.get('loop', 0) # optional: -1 oder true für Endlosschleife if not sound_id: return jsonify({"success": False, "message": "Kein sound_id angegeben"}), 400 @@ -220,13 +238,36 @@ def api_play_sound(): logger.error(f"Sound-Datei nicht gefunden: {file_path}") return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404 - # Abspielen (non-blocking, kein eigener Thread nötig) - try: - pygame.mixer.music.load(file_path) - pygame.mixer.music.play() - except Exception as e: - logger.exception(f"Fehler beim Abspielen von {file_path}") - return jsonify({"success": False, "message": str(e)}), 500 + # Abspielen (serialisiert, aber mehrere Channels erlaubt) + with audio_lock: + _ensure_mixer() + + # Sound laden (Cache) + snd = _load_sound(file_path) + + # Ziel-Channel bestimmen + ch = None + if channel_req not in [None, ""]: + try: + ch_id = int(channel_req) + ch = pygame.mixer.Channel(ch_id) + except Exception as e: + logger.error(f"Ungültiger channel-Wert '{channel_req}': {e}") + return jsonify({"success": False, "message": "Ungültiger Channel"}), 400 + else: + ch = pygame.mixer.find_channel(True) # zwingend freien nehmen + + if ch is None: + return jsonify({"success": False, "message": "Kein freier Audio-Channel verfügbar"}), 503 + + # Loop-Wert interpretieren + loops = -1 if loop_req in [True, "true", "True", -1, "loop", "1", 1] else 0 + + try: + ch.play(snd, loops=loops) + except Exception as e: + logger.exception(f"Fehler beim Abspielen von {file_path}") + return jsonify({"success": False, "message": str(e)}), 500 logger.info(f"Spiele Sound: {sound_entry['name']} ({sound_id})") return jsonify({"success": True, "message": f"Spiele: {sound_entry['name']}"}) @@ -240,8 +281,26 @@ def api_play_sound(): @api_bp.route('/stop_sound', methods=['POST']) def api_stop_sound(): try: - pygame.mixer.music.stop() - logger.info("Aktueller Sound gestoppt") + data = request.get_json(silent=True) or {} + channel_req = data.get('channel') + + with audio_lock: + _ensure_mixer() + if channel_req not in [None, ""]: + try: + ch_id = int(channel_req) + pygame.mixer.Channel(ch_id).stop() + except Exception as e: + logger.error(f"Stop-Sound-Fehler (channel {channel_req}): {e}") + return jsonify({"success": False, "message": "Ungültiger Channel"}), 400 + else: + # Alle Kanäle stoppen + for ch_id in range(pygame.mixer.get_num_channels()): + pygame.mixer.Channel(ch_id).stop() + # Zur Sicherheit auch music stoppen + pygame.mixer.music.stop() + + logger.info("Sound(s) gestoppt") return jsonify({"success": True}) except Exception as e: logger.error(f"Stop-Sound-Fehler: {e}") @@ -255,7 +314,17 @@ def api_set_volume(): data = request.get_json() vol = float(data.get('volume', 0.8)) vol = max(0.0, min(1.0, vol)) - pygame.mixer.music.set_volume(vol) + channel_req = data.get('channel') + with audio_lock: + _ensure_mixer() + if channel_req not in [None, ""]: + ch_id = int(channel_req) + pygame.mixer.Channel(ch_id).set_volume(vol) + else: + # global: alle Kanäle + music + for ch_id in range(pygame.mixer.get_num_channels()): + pygame.mixer.Channel(ch_id).set_volume(vol) + pygame.mixer.music.set_volume(vol) logger.info(f"Volume auf {vol} gesetzt") return jsonify({"success": True}) except Exception as e: diff --git a/configs/default_sounds.json b/configs/default_sounds.json index 7b0e993..926ddf8 100644 --- a/configs/default_sounds.json +++ b/configs/default_sounds.json @@ -10,8 +10,10 @@ { "id": "wind_western1", "file": "freesound_community-wind-western-64661.mp3", - "name": "Wind Western", - "description": "einfache Windgeräusche" + "name": "Wind", + "description": "einfache Windgeräusche", + "loop": true, + "channel": 0 } ] } \ No newline at end of file diff --git a/static/js/ui-soundboard.js b/static/js/ui-soundboard.js index d775f4b..4071ad0 100644 --- a/static/js/ui-soundboard.js +++ b/static/js/ui-soundboard.js @@ -60,6 +60,8 @@ function initSoundboard() { playButtons.forEach(btn => { btn.addEventListener('click', async () => { const soundId = btn.dataset.soundId; + const channel = btn.dataset.channel || null; + const loopFlag = btn.dataset.loop === '1' || btn.dataset.loop === 'true'; // Alten Play-Button zurücksetzen if (currentSoundButton && currentSoundButton !== btn) { @@ -77,7 +79,7 @@ function initSoundboard() { const res = await fetch('/api/play_sound', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sound_id: soundId }) + body: JSON.stringify({ sound_id: soundId, channel, loop: loopFlag }) }); const data = await res.json(); @@ -111,10 +113,12 @@ function initSoundboard() { // ── Stop-Button für aktuellen Sound ──────────────────────────────────────── document.querySelectorAll('.stop-sound-btn').forEach(btn => { btn.addEventListener('click', async () => { + const channel = btn.dataset.channel || null; try { const res = await fetch('/api/stop_sound', { method: 'POST', headers: { 'Content-Type': 'application/json' } + ,body: JSON.stringify({ channel }) }); const data = await res.json(); diff --git a/templates/control.html b/templates/control.html index 1810452..4a1e0df 100644 --- a/templates/control.html +++ b/templates/control.html @@ -118,13 +118,17 @@ {% for sound in config.sounds %}