Skip to content

Audio

A game declares its cues from InitApi.audio or a level’s LoadApi.audio, and plays them by name from world.audio. A cue is either synthesized from a CueSpec or backed by an audio file, and both play through the same call.

readonly audio: {
define(cue: string, spec: CueSpec): void;
load(cue: string, path: string): Promise<void>;
};
MemberBehavior
defineBinds cue to a synthesized spec.
loadFetches and decodes the audio at path and binds the result to cue. Resolves once the cue is playable.

A cue name carries one source. Declaring a name that already exists replaces what it plays, whichever of the two declared it.

Cue definitions belong to the engine rather than the world, so they survive a level transition. A cue declared during initialization is playable in every level that follows.

load resolves path through the asset loader, so it follows the same asset root and the same path rules, and it emits the same asset:loaded and asset:failed events. The audio the asset-generation tools produce is loaded this way.

A level declares the cues it alone needs from its load, which the engine awaits before the world is built.

readonly audio: {
load(cue: string, path: string): Promise<void>;
};

LoadApi.audio carries load. Synthesized cues are defined once, from the game instance’s initialize, where they outlive every transition.

interface CueSpec {
wave?: "sine" | "square" | "sawtooth" | "triangle";
freq: number;
freqTo?: number;
gain?: number;
durationMs: number;
}
FieldUnitDefaultMeaning
wave—"sine"The oscillator waveform.
freqhertzrequiredThe starting frequency. A loop holds it.
freqTohertzfreqThe frequency swept to linearly across the duration. A loop ignores it.
gain0–10.2The peak gain the envelope decays from. A loop holds it.
durationMsmillisecondsrequiredHow long the cue sounds. A loop ignores it.

durationMs is milliseconds, and the delta time a tick receives is seconds.

interface WorldAudio {
play(cue: string): void;
loop(cue: string): void;
stop(cue: string): void;
looping(cue: string): boolean;
setMuted(muted: boolean): void;
muted(): boolean;
}
MemberBehavior
playEmits cue:played and, when audible, sounds the cue. Returns immediately.
loopStarts the cue looping if it is not already: emits cue:looped once and, when audible, sounds the cue continuously until stopped. Does nothing for a cue already looping.
stopStops the cue’s loop if it is looping and emits cue:stopped. Does nothing for a cue that is not looping.
loopingWhether the cue is looping. false for an undeclared cue.
setMutedSets the mute bit. A muted cue still emits its event, and every running loop follows the bit live.
mutedThe mute bit.

Playback belongs to a tick, so what a frame sounds is decided by the same code that advanced the simulation. An actor, a component, a controller, and a game mode all reach the bus through the world they belong to.

A file-backed cue loops its decoded buffer seamlessly. A synthesized cue holds its wave at freq at its gain until stopped, with no sweep and no decay. A cue is either looping or not; loop and stop each act once per transition and emit once per transition.

Mute is live: setMuted(true) silences every running loop and setMuted(false) restores each one’s gain, without stopping or restarting it. A loop started before the unlock is looping from that call, with its event emitted, and begins to sound when the gesture opens the context.

Loops belong to the engine with the cue definitions, so a loop started in one level keeps running across a transition until a tick stops it. Redeclaring a looping cue, through define or load under the same name, stops the loop and emits cue:stopped. engine.destroy() stops every loop.

interface AudioState {
muted: boolean;
unlocked: boolean;
}
FieldMeaning
mutedWhether the bus is muted.
unlockedWhether a user gesture has opened the audio context.

The engine opens the audio context on the first pointerdown or keydown event it sees and emits audio:unlocked at that moment.

Audio reports itself through the engine’s event broadcaster, subscribed with events.on(name, handler).

"cue:played": { cue: string; t: number; gain: number };
"cue:looped": { cue: string; t: number; gain: number };
"cue:stopped": { cue: string; t: number };
"audio:unlocked": Record<string, never>;
FieldMeaning
cueThe name that was played, started looping, or stopped.
tThe frame loop’s accumulated simulated time in milliseconds at that moment.
gainThe gain it played or started looping at.

A play or a loop on a muted bus reports gain: 0. On an unmuted bus it reports the spec’s gain for a synthesized cue and 1 for a file-backed cue. cue:looped is emitted once per loop, when it starts, and cue:stopped once, when it ends.

Handlers run synchronously at the moment of the call, so a subscriber sees the frame a cue belongs to. The broadcaster lives on the engine, so a subscription made before engine.initialize captures the cues a game plays from its start level onward and across every transition.

ConditionResult
play, loop, or stop names a cue that was never declaredThrows, naming the cue
looping names a cue that was never declaredReturns false
load is given a path the asset loader refusesRejects with the resolve error, and the cue stays undeclared
load cannot fetch or decode the audioRejects with the cause, and the cue stays undeclared
load rejects inside the instance’s initialize or the start level’s loadengine.initialize rejects with the cause

CueSpec, AudioState, and WorldAudio are exported as types from @clockwyrks/structured-2d.