From e4f0ea719950d8b7f27cfe51fb2111a744388d37 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 26 Jun 2026 10:55:15 +0200 Subject: [PATCH] fix(web): Web-Audio-Kontext bei Nutzergeste freischalten (iOS-Chunk-Wiedergabe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unlockTTS() schaltete bisher nur SpeechSynthesis frei. Der Web-Audio-Kontext für die satzweise Chunk-Wiedergabe (Server-TTS-Modi) wurde erst lazy im WS-Callback erzeugt — auf iOS startet ein so erzeugter AudioContext "suspended" und resume() außerhalb einer Geste wird abgelehnt -> stumme/blockierte Chunk-Wiedergabe. Fix: unlockTTS() legt den AudioContext jetzt in der Nutzergeste an, ruft resume() und spielt einen 1-Sample-Stille-Buffer (Standard-iOS-Unlock). Beide Freischaltungen (Geräte-TTS + Web-Audio) laufen unabhängig, auch wenn SpeechSynthesis fehlt. Verifiziert (Desktop-Chrome): Kontext wird in der Geste erzeugt, ist "running" und der Stille-Buffer läuft, bevor der erste Chunk eintrifft; Chunk-Wiedergabe weiterhin gapless. Echtes iOS Safari hier nicht testbar. Co-Authored-By: Claude Opus 4.8 --- app/web/app.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/web/app.js b/app/web/app.js index c575b65..7371de8 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -151,11 +151,29 @@ function deviceSpeakBtn(text, lang, btn) { speechSynthesis.speak(u); } -// iOS: speechSynthesis darf nur nach einer Nutzergeste starten -> beim ersten Tap freischalten. +// iOS: sowohl SpeechSynthesis (Geräte-TTS) als auch der Web-Audio-Kontext (Server-Chunks) +// dürfen nur nach einer Nutzergeste starten -> beim ersten Tap beide freischalten. let _ttsUnlocked = false; +let _audioUnlocked = false; function unlockTTS() { - if (_ttsUnlocked || !TTS_SUPPORTED) return; - try { speechSynthesis.speak(new SpeechSynthesisUtterance("")); _ttsUnlocked = true; } catch (e) {} + // Geräte-TTS per Leer-Utterance freischalten. + if (TTS_SUPPORTED && !_ttsUnlocked) { + try { speechSynthesis.speak(new SpeechSynthesisUtterance("")); _ttsUnlocked = true; } catch (e) {} + } + // Web-Audio-Kontext jetzt (in der Geste) anlegen + per Stille-Buffer aufwecken, damit + // die satzweise Chunk-Wiedergabe in den Server-Modi auch auf iOS läuft (sonst bleibt ein + // erst im WS-Callback erzeugter AudioContext dort "suspended"). + if (!_audioUnlocked) { + try { + audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)(); + if (audioCtx.state === "suspended") audioCtx.resume(); + const src = audioCtx.createBufferSource(); + src.buffer = audioCtx.createBuffer(1, 1, 22050); // 1 Sample Stille + src.connect(audioCtx.destination); + src.start(0); + _audioUnlocked = true; + } catch (e) {} + } } const PLAYBACK_KEY = "va-playback"; // geräte-lokal: "device" wenn Geräte-TTS gewählt