import { AudioContext } from "three"; import { Application } from "./engine_application.js"; import { getParam } from "./engine_utils.js"; /** Verbose audio logging, opt-in via `?debugaudio` (same flag AudioSource uses). */ const debug = getParam("debugaudio"); /** * iOS Safari exposes a non-standard `"interrupted"` state in addition to the * spec'd `AudioContextState`s when the audio session is taken away (e.g. the * device is locked or another app starts playing audio). * @see https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari */ export type InterruptibleAudioContextState = AudioContextState | "interrupted"; /** * @internal * Pure policy deciding whether `AudioContext.resume()` should be attempted right now. * * The context needs resuming when it is `"suspended"` or (on iOS) `"interrupted"`. * But resuming is only allowed while the document is **visible**: on iOS Safari, * calling `resume()` while the page is backgrounded or the device is locked fails * with `InvalidStateError: Failed to start the audio device`, because the audio * hardware cannot be started in the background. In that case the resume must be * deferred until the page becomes visible again. * * @param state The current (possibly iOS-interrupted) audio context state. * @param visibility The current `document.visibilityState`. * @returns `true` if `resume()` should be called now, `false` if it should be deferred. */ export function shouldResumeAudioContext( state: InterruptibleAudioContextState, visibility: DocumentVisibilityState, ): boolean { // Resuming while hidden throws InvalidStateError on iOS — defer until visible. if (visibility !== "visible") return false; return state === "suspended" || state === "interrupted"; } /** Resume the shared AudioContext if it is suspended/interrupted and the page is visible. */ function recoverAudioContext() { // r183: getContext()'s return type is no longer assignable to the DOM AudioContext — cast it. const ctx = AudioContext.getContext() as unknown as globalThis.AudioContext; const state = ctx.state as InterruptibleAudioContextState; const visibility = typeof document !== "undefined" ? document.visibilityState : "visible"; if (!shouldResumeAudioContext(state, visibility)) return; ctx.resume() .then(() => { if (debug) console.log("AudioContext resumed successfully"); }) .catch(e => { if (debug) console.warn("Failed to resume AudioContext: " + e); }); } /** * @internal * Keep the shared AudioContext alive across suspends/interruptions (tab switch, device lock, audio * session interruption). On returning to the foreground we simply resume() the EXISTING context — * the same approach three.js uses, which recovers cleanly because AudioSource plays clips through a * MediaElementAudioSourceNode (an