diff --git a/public/src/tts.js b/public/src/tts.js index 87877d5..00dfa15 100644 --- a/public/src/tts.js +++ b/public/src/tts.js @@ -131,10 +131,48 @@ function splitTextIntoChunks(text) { return chunks; } +// Shared AudioContext for decoding-only operations (silence detection). +// Lazily created so we don't hit iOS Safari's "must come from gesture" restriction +// unless we actually need it. +let _decodeCtx = null; +function getDecodeCtx() { + if (!_decodeCtx) { + const Ctx = window.AudioContext || window.webkitAudioContext; + if (!Ctx) return null; + _decodeCtx = new Ctx(); + } + return _decodeCtx; +} + +// Detect where speech actually begins in an audio blob. Scans the first 1.5s of +// decoded PCM for the first sample whose absolute amplitude crosses `threshold`, +// then returns that time minus a 20ms safety margin so we don't clip the first +// phoneme. Returns 0 on any decode failure (degrade gracefully). +async function detectSpeechStart(blob, threshold = 0.05, safetyMs = 20) { + try { + const ctx = getDecodeCtx(); + if (!ctx) return 0; + const arrayBuffer = await blob.arrayBuffer(); + const audioBuffer = await ctx.decodeAudioData(arrayBuffer.slice(0)); + const data = audioBuffer.getChannelData(0); + const sampleRate = audioBuffer.sampleRate; + const scanSamples = Math.min(data.length, Math.floor(sampleRate * 1.5)); + for (let i = 0; i < scanSamples; i++) { + if (Math.abs(data[i]) > threshold) { + return Math.max(0, i / sampleRate - safetyMs / 1000); + } + } + return 0; + } catch (e) { + console.warn("detectSpeechStart failed:", e); + return 0; + } +} + // Pipelined playlist player: fetches all chunks in parallel, plays each as soon // as ready, chains via 'ended'. Exposes Audio-compatible API so downstream UI -// (pause/play, download, progress) works unchanged. Bonus: fixes Safari MP3-concat -// bug by playing each chunk as its own