Skip to content

Audio

A game declares its cues once, from InitApi.audio, and plays them by name from UpdateApi.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.

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.

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 an update receives is seconds.

readonly audio: {
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 update, so what a frame sounds is decided by the same function that advanced the simulation.

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.

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 pointer or key event it sees and emits audio:unlocked at that moment. unlocked becomes true on that gesture in every browser, including one that then offers no audio context.

Audio reports itself through the engine’s event broadcaster, subscribed with api.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 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. Subscribing before engine.initialize captures the cues a game plays from its own initialization onward.

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
No audio context is availableplay and loop emit their events and nothing sounds
The audio graph throws during synthesis or a loopThe event is emitted and the frame continues

CueSpec and AudioState are exported as types from @clockwyrks/simple-2d.