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 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 12:55:07 -04:00
parent 71ca0ecb5c
commit c31cf11767
+14 -9
View File
@@ -524,7 +524,6 @@ const voiceBlend = ref<VoiceBlendEntry[]>([
]);
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 {