mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-11 12:05:33 +08:00
refactor(live-atc): extract TTS/radio-effects speech to useRadioSpeech
Moves the audio-context/Pizzicato setup, TTS fetch+playback pipeline, controller/readback speech scheduling, and the radio-check handling (RADIO_CHECK_RE/SAY_AGAIN_RE/FAREWELL_RE, answerRadioCheck, performRadioCheck, readability quick-select) out of live-atc.vue. The interrupt primitives (stopCurrentSpeech/enqueueSpeech and the audio/abort bookkeeping they tear down) live in a separate useSpeechInterrupt because useFrequencyPresets needs stopCurrentSpeech while useRadioSpeech needs frequency state — one composable owning both would be circular. useSpeechInterrupt is constructed first and threaded into both. signalStrength/speechSpeed/radioCheckLoading/radioEffectsEnabled/ readbackEnabled stay page-level refs and are passed in, because useAtisPlayback already consumes signalStrength before useRadioSpeech can be constructed (it needs handlePilotTransmission). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
71
app/composables/useSpeechInterrupt.ts
Normal file
71
app/composables/useSpeechInterrupt.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Low-level speech interruption/queue primitives, split out from
|
||||
* useRadioSpeech so it has zero dependency on frequency/airport state.
|
||||
* useFrequencyPresets needs stopCurrentSpeech (tuning away cuts in-flight
|
||||
* ATC speech) while useRadioSpeech needs frequency state — instantiating
|
||||
* both from one composable would be circular, so this piece is
|
||||
* constructed first and threaded into both.
|
||||
*/
|
||||
export function useSpeechInterrupt() {
|
||||
let speechQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
// Tracks objects that must be torn down when frequency changes mid-speech.
|
||||
let _currentAudio: HTMLAudioElement | null = null
|
||||
let _currentPizzicatoSound: { stop: () => void } | null = null
|
||||
// All in-flight TTS requests (playback is serialized, but generation runs in
|
||||
// parallel so a reply's TTS doesn't wait for earlier speech to finish playing).
|
||||
const _pendingSpeechAborts = new Set<AbortController>()
|
||||
// Incremented on each stopCurrentSpeech() call so enqueued tasks can detect
|
||||
// they belong to a cancelled generation and exit early.
|
||||
let _speechGeneration = 0
|
||||
|
||||
const getSpeechGeneration = () => _speechGeneration
|
||||
|
||||
/** Immediately stop any in-flight TTS request and audio playback. */
|
||||
const stopCurrentSpeech = () => {
|
||||
_speechGeneration++
|
||||
for (const abort of _pendingSpeechAborts) {
|
||||
abort.abort()
|
||||
}
|
||||
_pendingSpeechAborts.clear()
|
||||
if (_currentPizzicatoSound) {
|
||||
try { _currentPizzicatoSound.stop() } catch (_) { /* ignore */ }
|
||||
_currentPizzicatoSound = null
|
||||
}
|
||||
if (_currentAudio) {
|
||||
_currentAudio.pause()
|
||||
_currentAudio.src = ''
|
||||
_currentAudio = null
|
||||
}
|
||||
// Drop queued tasks by resolving the queue immediately.
|
||||
speechQueue = Promise.resolve()
|
||||
}
|
||||
|
||||
const enqueueSpeech = (task: () => Promise<void>) => {
|
||||
const generation = _speechGeneration
|
||||
speechQueue = speechQueue
|
||||
.then(() => {
|
||||
if (generation !== _speechGeneration) return // cancelled — skip
|
||||
return task()
|
||||
})
|
||||
.catch(err => {
|
||||
if (err?.name !== 'AbortError') console.error('Speech queue error:', err)
|
||||
})
|
||||
return speechQueue
|
||||
}
|
||||
|
||||
const setCurrentAudio = (audio: HTMLAudioElement | null) => { _currentAudio = audio }
|
||||
const setCurrentPizzicatoSound = (sound: { stop: () => void } | null) => { _currentPizzicatoSound = sound }
|
||||
const addPendingAbort = (abort: AbortController) => { _pendingSpeechAborts.add(abort) }
|
||||
const deletePendingAbort = (abort: AbortController) => { _pendingSpeechAborts.delete(abort) }
|
||||
|
||||
return {
|
||||
stopCurrentSpeech,
|
||||
enqueueSpeech,
|
||||
getSpeechGeneration,
|
||||
setCurrentAudio,
|
||||
setCurrentPizzicatoSound,
|
||||
addPendingAbort,
|
||||
deletePendingAbort,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user