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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user