0a913045a8
Without this, await audio.play() resolves immediately after source.start(), so the playQueue chains the next sentence before the current one finishes, causing overlapping / interrupted playback.
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
import { ref, readonly } from 'vue'
|
|
|
|
const VOLUME_KEY = 'fa_voice_volume'
|
|
|
|
// Shared volume across all instances — persisted to localStorage per device
|
|
const _volume = ref<number>(parseFloat(localStorage.getItem(VOLUME_KEY) ?? '1.0'))
|
|
|
|
export function useVoiceAudio() {
|
|
const playing = ref(false)
|
|
const isSupported = typeof AudioContext !== 'undefined' || typeof (window as unknown as Record<string, unknown>).webkitAudioContext !== 'undefined'
|
|
|
|
let audioCtx: AudioContext | null = null
|
|
let currentSource: AudioBufferSourceNode | null = null
|
|
|
|
function _getCtx(): AudioContext {
|
|
if (!audioCtx || audioCtx.state === 'closed') {
|
|
const Ctx = window.AudioContext ?? (window as unknown as Record<string, typeof AudioContext>).webkitAudioContext
|
|
audioCtx = new Ctx()
|
|
}
|
|
return audioCtx
|
|
}
|
|
|
|
async function play(blob: Blob): Promise<void> {
|
|
stop()
|
|
const ctx = _getCtx()
|
|
if (ctx.state === 'suspended') await ctx.resume()
|
|
|
|
const arrayBuffer = await blob.arrayBuffer()
|
|
const audioBuffer = await ctx.decodeAudioData(arrayBuffer)
|
|
|
|
const source = ctx.createBufferSource()
|
|
source.buffer = audioBuffer
|
|
|
|
const gain = ctx.createGain()
|
|
gain.gain.value = _volume.value
|
|
source.connect(gain)
|
|
gain.connect(ctx.destination)
|
|
|
|
currentSource = source
|
|
playing.value = true
|
|
|
|
return new Promise<void>((resolve) => {
|
|
source.onended = () => {
|
|
playing.value = false
|
|
currentSource = null
|
|
resolve()
|
|
}
|
|
source.start(0)
|
|
})
|
|
}
|
|
|
|
function stop(): void {
|
|
if (currentSource) {
|
|
try { currentSource.stop() } catch { /* already stopped */ }
|
|
currentSource = null
|
|
}
|
|
playing.value = false
|
|
}
|
|
|
|
return {
|
|
playing: readonly(playing),
|
|
volume: _volume,
|
|
isSupported,
|
|
play,
|
|
stop,
|
|
}
|
|
}
|
|
|
|
export function setVoiceVolume(v: number) {
|
|
_volume.value = Math.max(0, Math.min(1, v))
|
|
localStorage.setItem(VOLUME_KEY, String(_volume.value))
|
|
}
|