added multi sound-channel
This commit is contained in:
parent
434abad282
commit
bb71f893e5
@ -32,7 +32,8 @@ def create_app():
|
|||||||
|
|
||||||
# pygame initialisieren (einmalig)
|
# pygame initialisieren (einmalig)
|
||||||
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
|
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")
|
app.logger.info("pygame initialisiert")
|
||||||
|
|
||||||
# Blueprints registrieren
|
# Blueprints registrieren
|
||||||
|
|||||||
@ -12,6 +12,22 @@ from app.bluetooth.manager import MouldKing, advertiser, tracer
|
|||||||
from app.utils.helpers import load_default_sounds
|
from app.utils.helpers import load_default_sounds
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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__)
|
api_bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
@ -198,6 +214,8 @@ def api_play_sound():
|
|||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
sound_id = data.get('sound_id')
|
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:
|
if not sound_id:
|
||||||
return jsonify({"success": False, "message": "Kein sound_id angegeben"}), 400
|
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}")
|
logger.error(f"Sound-Datei nicht gefunden: {file_path}")
|
||||||
return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404
|
return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404
|
||||||
|
|
||||||
# Abspielen (non-blocking, kein eigener Thread nötig)
|
# Abspielen (serialisiert, aber mehrere Channels erlaubt)
|
||||||
try:
|
with audio_lock:
|
||||||
pygame.mixer.music.load(file_path)
|
_ensure_mixer()
|
||||||
pygame.mixer.music.play()
|
|
||||||
except Exception as e:
|
# Sound laden (Cache)
|
||||||
logger.exception(f"Fehler beim Abspielen von {file_path}")
|
snd = _load_sound(file_path)
|
||||||
return jsonify({"success": False, "message": str(e)}), 500
|
|
||||||
|
# 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})")
|
logger.info(f"Spiele Sound: {sound_entry['name']} ({sound_id})")
|
||||||
return jsonify({"success": True, "message": f"Spiele: {sound_entry['name']}"})
|
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'])
|
@api_bp.route('/stop_sound', methods=['POST'])
|
||||||
def api_stop_sound():
|
def api_stop_sound():
|
||||||
try:
|
try:
|
||||||
pygame.mixer.music.stop()
|
data = request.get_json(silent=True) or {}
|
||||||
logger.info("Aktueller Sound gestoppt")
|
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})
|
return jsonify({"success": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Stop-Sound-Fehler: {e}")
|
logger.error(f"Stop-Sound-Fehler: {e}")
|
||||||
@ -255,7 +314,17 @@ def api_set_volume():
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
vol = float(data.get('volume', 0.8))
|
vol = float(data.get('volume', 0.8))
|
||||||
vol = max(0.0, min(1.0, vol))
|
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")
|
logger.info(f"Volume auf {vol} gesetzt")
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -10,8 +10,10 @@
|
|||||||
{
|
{
|
||||||
"id": "wind_western1",
|
"id": "wind_western1",
|
||||||
"file": "freesound_community-wind-western-64661.mp3",
|
"file": "freesound_community-wind-western-64661.mp3",
|
||||||
"name": "Wind Western",
|
"name": "Wind",
|
||||||
"description": "einfache Windgeräusche"
|
"description": "einfache Windgeräusche",
|
||||||
|
"loop": true,
|
||||||
|
"channel": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -60,6 +60,8 @@ function initSoundboard() {
|
|||||||
playButtons.forEach(btn => {
|
playButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const soundId = btn.dataset.soundId;
|
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
|
// Alten Play-Button zurücksetzen
|
||||||
if (currentSoundButton && currentSoundButton !== btn) {
|
if (currentSoundButton && currentSoundButton !== btn) {
|
||||||
@ -77,7 +79,7 @@ function initSoundboard() {
|
|||||||
const res = await fetch('/api/play_sound', {
|
const res = await fetch('/api/play_sound', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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();
|
const data = await res.json();
|
||||||
@ -111,10 +113,12 @@ function initSoundboard() {
|
|||||||
// ── Stop-Button für aktuellen Sound ────────────────────────────────────────
|
// ── Stop-Button für aktuellen Sound ────────────────────────────────────────
|
||||||
document.querySelectorAll('.stop-sound-btn').forEach(btn => {
|
document.querySelectorAll('.stop-sound-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
|
const channel = btn.dataset.channel || null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/stop_sound', {
|
const res = await fetch('/api/stop_sound', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
,body: JSON.stringify({ channel })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@ -118,13 +118,17 @@
|
|||||||
{% for sound in config.sounds %}
|
{% for sound in config.sounds %}
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button class="btn btn-outline-primary flex-grow-1 play-sound-btn"
|
<button class="btn btn-outline-primary flex-grow-1 play-sound-btn"
|
||||||
data-sound-id="{{ sound.id }}">
|
data-sound-id="{{ sound.id }}"
|
||||||
|
data-channel="{{ sound.channel | default('') }}"
|
||||||
|
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||||
{{ sound.name }}
|
{{ sound.name }}
|
||||||
{% if sound.description %}
|
{% if sound.description %}
|
||||||
<small class="d-block text-muted">{{ sound.description }}</small>
|
<small class="d-block text-muted">{{ sound.description }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
|
<button class="btn btn-outline-danger stop-sound-btn"
|
||||||
|
data-channel="{{ sound.channel | default('') }}"
|
||||||
|
title="Aktuellen Sound stoppen">
|
||||||
<i class="bi bi-stop-fill"></i>
|
<i class="bi bi-stop-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -139,13 +143,17 @@
|
|||||||
{% for sound in global_sounds %}
|
{% for sound in global_sounds %}
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button class="btn btn-outline-secondary flex-grow-1 play-sound-btn"
|
<button class="btn btn-outline-secondary flex-grow-1 play-sound-btn"
|
||||||
data-sound-id="{{ sound.id }}">
|
data-sound-id="{{ sound.id }}"
|
||||||
|
data-channel="{{ sound.channel | default('') }}"
|
||||||
|
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||||
{{ sound.name }}
|
{{ sound.name }}
|
||||||
{% if sound.description %}
|
{% if sound.description %}
|
||||||
<small class="d-block text-muted">{{ sound.description }}</small>
|
<small class="d-block text-muted">{{ sound.description }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
|
<button class="btn btn-outline-danger stop-sound-btn"
|
||||||
|
data-channel="{{ sound.channel | default('') }}"
|
||||||
|
title="Aktuellen Sound stoppen">
|
||||||
<i class="bi bi-stop-fill"></i>
|
<i class="bi bi-stop-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user