From c31cf11767f4fd919c20112305aee7e638377f60 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 31 Mar 2026 12:55:07 -0400 Subject: [PATCH] fix: use AudioContext for voice previews to bypass browser autoplay policy new Audio().play() after an async await loses the user gesture context and is silently blocked. Creating AudioContext synchronously before the fetch preserves the permission, then decode/play through it after the await. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/views/SettingsView.vue | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index edc02d7..dd1b408 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -524,7 +524,6 @@ const voiceBlend = ref([ ]); const blendPreviewText = ref("Hello! I'm your assistant. How can I help you today?"); const blendPreviewing = ref(false); -let _blendAudioUrl = ""; function addBlendSlot() { voiceBlend.value.push({ voice: "af_heart", weight: 1.0 }); @@ -537,6 +536,8 @@ function removeBlendSlot(idx: number) { async function previewBlend() { if (blendPreviewing.value || !blendPreviewText.value.trim()) return; blendPreviewing.value = true; + // Create AudioContext synchronously in user-gesture context to bypass autoplay policy + const ctx = new AudioContext(); try { const blob = await synthesiseSpeech( blendPreviewText.value, @@ -544,10 +545,11 @@ async function previewBlend() { voiceTtsSpeed.value, voiceBlend.value, ); - if (_blendAudioUrl) URL.revokeObjectURL(_blendAudioUrl); - _blendAudioUrl = URL.createObjectURL(blob); - const audio = new Audio(_blendAudioUrl); - audio.play(); + const buf = await ctx.decodeAudioData(await blob.arrayBuffer()); + const src = ctx.createBufferSource(); + src.buffer = buf; + src.connect(ctx.destination); + src.start(0); } catch { toastStore.show("Preview failed — TTS may not be ready", "error"); } finally { @@ -572,20 +574,23 @@ async function loadVoiceTab() { } const voicePreviewing = ref(false); -let _voicePreviewUrl = ""; async function previewVoice() { if (voicePreviewing.value) return; voicePreviewing.value = true; + // Create AudioContext synchronously in user-gesture context to bypass autoplay policy + const ctx = new AudioContext(); try { const blob = await synthesiseSpeech( "Hello! I'm your assistant. How can I help you today?", voiceTtsVoice.value, voiceTtsSpeed.value, ); - if (_voicePreviewUrl) URL.revokeObjectURL(_voicePreviewUrl); - _voicePreviewUrl = URL.createObjectURL(blob); - new Audio(_voicePreviewUrl).play(); + const buf = await ctx.decodeAudioData(await blob.arrayBuffer()); + const src = ctx.createBufferSource(); + src.buffer = buf; + src.connect(ctx.destination); + src.start(0); } catch { toastStore.show("Preview failed — TTS may not be ready", "error"); } finally {