mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 17:05:53 +08:00
feat(pm): live ATIS broadcast loop with METAR-slot refresh and multi-station support
- per-airport ATIS loop keyed by station with a virtual start epoch, so re-tuning resumes where the broadcast would be instead of restarting - refetch airport data at :23/:53 to follow VATSIM ATIS regeneration from real-world METAR publication, with faster retries while no ATIS is on the feed; prefetch audio when the info letter changes - support separate arrival/departure ATIS stations on different frequencies - cancel the deferred audio teardown on retune so a fresh broadcast is not killed by the previous stop()'s fade-out timer (atisAudioLoop) - comm log shows newest entries first Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,9 +62,19 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
let carrierGain: GainNode | null = null
|
||||
let atisSource: AudioBufferSourceNode | null = null
|
||||
let atisGain: GainNode | null = null
|
||||
// Pending deferred teardown from stop() — must be cancelled when a new
|
||||
// start comes in before it fires, otherwise it kills the fresh sources.
|
||||
let stopTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const state: AtisLoopState = { phase: 'idle' }
|
||||
|
||||
const cancelScheduledStop = () => {
|
||||
if (stopTimer) {
|
||||
clearTimeout(stopTimer)
|
||||
stopTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const ensureCtx = (): AudioContext => {
|
||||
if (ctx && ctx.state !== 'closed') {
|
||||
// Some browsers auto-suspend the context — resume on demand.
|
||||
@@ -133,6 +143,11 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
}
|
||||
|
||||
const startLoading = () => {
|
||||
cancelScheduledStop()
|
||||
// Cut any previous broadcast immediately — when switching between ATIS
|
||||
// stations only the carrier noise should be audible until the new
|
||||
// audio is ready, never the old station bleeding through.
|
||||
stopAtis()
|
||||
const c = ensureCtx()
|
||||
if (carrierSource) {
|
||||
// Already running. Ramp back up to loud in case we were in bed mode.
|
||||
@@ -142,7 +157,7 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
carrierGain.gain.setValueAtTime(carrierGain.gain.value, now)
|
||||
carrierGain.gain.linearRampToValueAtTime(CARRIER_GAIN_LOUD, now + CARRIER_FADE_S)
|
||||
}
|
||||
state.phase = state.phase === 'playing' ? 'playing' : 'loading'
|
||||
state.phase = 'loading'
|
||||
exposeDebug()
|
||||
return
|
||||
}
|
||||
@@ -178,6 +193,7 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
mime?: string
|
||||
epochMs: number
|
||||
}) => {
|
||||
cancelScheduledStop()
|
||||
const c = ensureCtx()
|
||||
let audioBuffer: AudioBuffer
|
||||
try {
|
||||
@@ -230,6 +246,7 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
cancelScheduledStop()
|
||||
// Quick fade out the carrier then stop both sources
|
||||
if (ctx && carrierGain) {
|
||||
const now = ctx.currentTime
|
||||
@@ -239,9 +256,11 @@ export function createAtisAudioLoop(): AtisAudioLoop {
|
||||
carrierGain.gain.linearRampToValueAtTime(0, now + 0.15)
|
||||
} catch {}
|
||||
}
|
||||
// Schedule stops slightly in the future so the fade completes audibly
|
||||
// Schedule stops slightly in the future so the fade completes audibly.
|
||||
// Kept in stopTimer so a retune can cancel it before it fires.
|
||||
const stopDelay = 200
|
||||
setTimeout(() => {
|
||||
stopTimer = setTimeout(() => {
|
||||
stopTimer = null
|
||||
stopAtis()
|
||||
stopCarrier()
|
||||
state.phase = 'idle'
|
||||
|
||||
Reference in New Issue
Block a user