fix(voice): TTS играет через AudioContext (фикс для iPad Safari)
All checks were successful
Deploy / deploy (push) Successful in 1m45s

iOS Safari блокирует <audio>.play() даже после silent-WAV unlock —
каждый new Audio() считается новым элементом без gesture.

Решение: при тапе кнопки в VoiceController создаём общий
AudioContext (под user-gesture) и пробуждаем его. VoiceOverlay
теперь играет TTS через этот ctx (decodeAudioData + BufferSource).
HTMLAudioElement остаётся fallback'ом если ctx недоступен.

decodeAudioData в Safari исторически callback, в Chrome — Promise:
используем оба варианта.
This commit is contained in:
Cosmo
2026-04-27 11:17:41 +00:00
parent 6c3992bb4e
commit 6083597065
2 changed files with 52 additions and 11 deletions

View File

@@ -178,19 +178,21 @@ export default function VoiceController() {
if (state !== 'idle' && state !== 'error') return
setState('loading')
// 0. «Audio unlock» — Android Chrome не даёт <audio>.play() без user-gesture.
// Wake-word срабатывает сам, поэтому позже play() будет тихо отвергнут.
// Проигрываем 1мс silent WAV прямо сейчас (user тапнул кнопку → есть gesture).
// 0. «Audio unlock» — iOS Safari / Android Chrome не дают воспроизводить
// звук без user-gesture. Wake-word срабатывает сам, поэтому позже TTS
// тихо отвергнется. Создаём общий AudioContext прямо сейчас (тап = gesture)
// и сохраняем в window — VoiceOverlay будет играть через него.
try {
const silent = new Audio(
'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
)
silent.volume = 0
await silent.play()
silent.pause()
console.log('[voice] audio unlock ok')
const w = window as any
if (!w.__voicePlaybackCtx) {
const Ctx = w.AudioContext || w.webkitAudioContext
if (Ctx) w.__voicePlaybackCtx = new Ctx()
}
const ctx: AudioContext | undefined = w.__voicePlaybackCtx
if (ctx && ctx.state === 'suspended') await ctx.resume()
console.log('[voice] playback AudioContext state=', ctx?.state)
} catch (e: any) {
console.warn('[voice] audio unlock failed:', e?.message)
console.warn('[voice] AudioContext init failed:', e?.message)
}
// 1. Запрос разрешения на микрофон отдельно