fix(mi-voice): await AudioContext.resume before source.start

Suspended contexts silently queued audio that never played. Await resume,
split connect() chain to avoid undefined-return on older browsers, and
re-check state after source setup in case first resume lost the gesture.
This commit is contained in:
Jeff Emmett 2026-04-16 15:30:16 -04:00
parent 858711e783
commit 71782b1cf1
1 changed files with 8 additions and 4 deletions

View File

@ -88,12 +88,12 @@ export class MiVoiceBridge {
// ── Bridge TTS ── // ── Bridge TTS ──
#ensureAudioCtx(): AudioContext { async #ensureAudioCtx(): Promise<AudioContext> {
if (!this.#audioCtx || this.#audioCtx.state === "closed") { if (!this.#audioCtx || this.#audioCtx.state === "closed") {
this.#audioCtx = new AudioContext(); this.#audioCtx = new AudioContext();
} }
if (this.#audioCtx.state === "suspended") { if (this.#audioCtx.state === "suspended") {
this.#audioCtx.resume(); try { await this.#audioCtx.resume(); } catch { /* gesture may be required */ }
} }
return this.#audioCtx; return this.#audioCtx;
} }
@ -158,14 +158,18 @@ export class MiVoiceBridge {
} catch { /* ignore bad header */ } } catch { /* ignore bad header */ }
const mp3Bytes = buf.slice(4 + headerLen); const mp3Bytes = buf.slice(4 + headerLen);
const ctx = this.#ensureAudioCtx(); const ctx = await this.#ensureAudioCtx();
const audioBuffer = await ctx.decodeAudioData(mp3Bytes.slice(0)); // slice to copy const audioBuffer = await ctx.decodeAudioData(mp3Bytes.slice(0)); // slice to copy
const source = ctx.createBufferSource(); const source = ctx.createBufferSource();
source.buffer = audioBuffer; source.buffer = audioBuffer;
const gain = ctx.createGain(); const gain = ctx.createGain();
gain.gain.value = gainValue; gain.gain.value = gainValue;
source.connect(gain).connect(ctx.destination); source.connect(gain);
gain.connect(ctx.destination);
this.#currentSource = source; this.#currentSource = source;
if (ctx.state === "suspended") {
try { await ctx.resume(); } catch { /* ignore */ }
}
source.onended = () => { source.onended = () => {
this.#currentSource = null; this.#currentSource = null;