@gameable/audio
Interfaces
AudioBuses
The bus gain nodes, keyed by name.
Properties
master
ts
readonly master: GainNode;Sums sfx, music and voice; connected to context.destination.
music
ts
readonly music: GainNode;Music beds.
sfx
ts
readonly sfx: GainNode;One-shot gameplay sounds.
voice
ts
readonly voice: GainNode;Dialogue and say lines.
AudioEngine
The audio service: everything the host needs to run a frame of sound.
Properties
buses
ts
readonly buses: AudioBuses;The bus gain nodes. Adjust bus.gain.value for mixer sliders.
context
ts
readonly context: AudioContext;The context the graph lives on.
ended
ts
readonly ended: number[];Handles of sounds that ended naturally or were stolen since the host last cleared this array. AudioEngine.update appends to it; the host turns each entry into a sound-ended event and then clears it with ended.length = 0.
A voice the game stopped itself — AudioEngine.stop, or a AudioEngine.play reusing a live handle — is not listed: the game already knows, and reporting it would claim a completion that never happened. Capped at 256 entries so a host that never drains it cannot grow the array without bound.
music
ts
readonly music: MusicPlayer;The music player: streamed tracks, playlists, stems and the beat clock.
Methods
decode()
ts
decode(id, data): Promise<AudioBuffer>;Decode encoded audio bytes, memoised by id.
Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Manifest asset id, the cache key. |
data | ArrayBuffer | Encoded bytes. |
Returns
Promise<AudioBuffer>
The decoded buffer. Repeat calls with the same id share a promise.
dispose()
ts
dispose(): void;Stop everything, drop the graph, and close a context this engine created.
Returns
void
play()
ts
play(args): void;Start a voice, silently dropping requests rejected by concurrency limits.
Parameters
| Parameter | Type | Description |
|---|---|---|
args | PlayArgs | What to play, where and how loud. |
Returns
void
resume()
ts
resume(): Promise<void>;Resume a context the browser suspended. Safe to call repeatedly.
Returns
Promise<void>
Resolves once the context is running.
setBusFx()
ts
setBusFx(
bus,
fx,
fadeMs?
): void;Set a bus's effects: a preset plus optional overrides. The chain is built on first use and only re-parameterised after that.
Parameters
| Parameter | Type | Description |
|---|---|---|
bus | BusName | The bus. |
fx | BusFx | Preset and overrides. |
fadeMs? | number | Ramp from the current setting, in milliseconds. |
Returns
void
setListener()
ts
setListener(pos, rotQuat): void;Move the listener.
Parameters
| Parameter | Type | Description |
|---|---|---|
pos | Vec3 | World position. |
rotQuat | Quat | Orientation; forward is local -Z, up is local +Y. |
Returns
void
setOcclusion()
ts
setOcclusion(
id,
amount,
fadeMs?
): void;How occluded a positioned voice is: 0 clear, 1 behind a wall. Lowers its low-pass cutoff and its gain.
Parameters
| Parameter | Type | Description |
|---|---|---|
id | number | The sound handle. Unknown and non-positioned ids are ignored. |
amount | number | 0..1. |
fadeMs? | number | Ramp length, in milliseconds. |
Returns
void
setVolume()
ts
setVolume(id, v): void;Change a playing voice's gain.
Parameters
| Parameter | Type | Description |
|---|---|---|
id | number | The sound handle. Unknown ids are ignored. |
v | number | Linear gain, 0..1. |
Returns
void
stats()
ts
stats(): object;Current admitted voices (including reserved), reserved voices, and lifetime drop/steal totals. Fade tails are excluded. The returned snapshot is safe for callers to retain.
Returns
object
dropped
ts
dropped: number;reserved
ts
reserved: number;stolen
ts
stolen: number;voices
ts
voices: number;Example
ts
import { createAudioEngine } from 'gameable/audio';
const engine = createAudioEngine();
console.log(engine.stats().dropped);stop()
ts
stop(id, fadeMs?): void;Stop a voice, optionally fading it out. Nothing is added to AudioEngine.ended: the caller asked for this, so it is not a completion.
Parameters
| Parameter | Type | Description |
|---|---|---|
id | number | The handle passed to AudioEngine.play. Unknown ids are ignored. |
fadeMs? | number | Fade-out before the stop, in milliseconds. 0 (the default) cuts. |
Returns
void
update()
ts
update(): void;Retire every voice whose buffer has run out and drain them into ended.
Timing comes from context.currentTime, the clock the voices themselves run on, not from the caller's frame delta: a long frame, a background tab or a timeScale change cannot drift the two apart. A suspended context does not advance, and neither does playback, so nothing ends early.
Returns
void
AudioEngineOptions
Options for createAudioEngine.
Extends
ConcurrencyOptions
Extended by
Properties
concurrency?
ts
optional concurrency?: Record<string, {
max: number;
retriggerMs?: number;
rule?: "stopOldest" | "preventNew" | "stopQuietest" | "stopFarthest";
}>;Per-group limits; missing groups use max 4, stopOldest, and a 30 ms retrigger delay.
Inherited from
ts
ConcurrencyOptions.concurrencycontext?
ts
optional context?: AudioContext;An existing context to drive. When omitted a new AudioContext is constructed, which requires a DOM environment. Pass one in tests.
distanceLowpass?
ts
optional distanceLowpass?: false | Partial<DistanceLowpass>;The low-pass every positioned voice gets with distance, or false for none. Defaults to DEFAULT_DISTANCE_LOWPASS (8 m, 80 m, 2.5 kHz).
masterVolume?
ts
optional masterVolume?: number;Linear gain on the master bus, 0..1. Defaults to 1.
maxVoices?
ts
optional maxVoices?: number;Maximum admitted non-reserved voices. Defaults to 32; fade tails are excluded.
Inherited from
ts
ConcurrencyOptions.maxVoicesmusic?
ts
optional music?: MusicPlayerOptions;Options for the music player (engine.music).
reservedBuses?
ts
optional reservedBuses?: string[];Protected buses, excluded from maxVoices. Defaults to music and dialogue (voice).
Inherited from
ts
ConcurrencyOptions.reservedBusesAudioModule
The 'audio' module. Its service is the AudioEngine it owns.
Extends
Properties
id
ts
readonly id: string;Unique id. Also the service id when init returns a service.
Inherited from
order?
ts
readonly optional order?: number;Sort key. Lower runs first; equal keys keep registration order.
Rough convention: input -100, gameplay -50, physics 0, rendering helpers 200. Gameplay runs before physics so that the commands a guest tick emits are simulated by the step that follows rather than the next one.
Inherited from
service
ts
readonly service: AudioEngine | null;The audio engine this module owns, or null before init and after dispose.
The engine is built by init, not by audio: constructing an AudioContext is a side effect on the page — browsers count them, and an unlocked one logs a warning — so a module that is never registered must never make one. init returns the engine, so the registry publishes it under 'audio' and engine.get('audio') hands back this same object.
unlock
ts
readonly unlock: AudioUnlock | null;The phone unlock, once init has armed it: unlock() from a gesture handler of your own, running for whether sound can play now.
Methods
beginFrame()?
ts
optional beginFrame(): void;Run before the frame's fixed steps. Input capture lives here.
Returns
void
Inherited from
decodeAsset()
ts
decodeAsset(idOrHandle): Promise<AudioBuffer>;Decode the bytes behind an asset id or handle, memoised.
This is the bridge from a play-sound command — which carries an asset handle, never a URL — to the AudioBuffer AudioEngine.play wants. Declared type: 'audio' entries load on demand as an ArrayBuffer before decoding. Concurrent requests share one load and decode.
Parameters
| Parameter | Type | Description |
|---|---|---|
idOrHandle | string | number | Manifest asset id, or the handle the guest minted for it. |
Returns
Promise<AudioBuffer>
The decoded buffer; rejects when the asset is missing or not audio.
decodedAsset()
ts
decodedAsset(idOrHandle): AudioBuffer | undefined;The already-decoded buffer for an asset, without awaiting anything.
This is what lets the host play a repeated sound — a footstep, a shot — inside the frame that asked for it instead of a microtask later. A miss means "not decoded yet"; fall back to AudioModule.decodeAsset.
Handles and ids share one cache entry: a handle is normalised through the asset registry first, so decodedAsset(7) and decodedAsset('sfx.shot') hit the same buffer.
Parameters
| Parameter | Type | Description |
|---|---|---|
idOrHandle | string | number | Manifest asset id, or the handle the guest minted for it. |
Returns
AudioBuffer | undefined
The decoded buffer, or undefined when no decode has finished.
dispose()
ts
dispose(): void;Release everything this module took.
Returns
void
Inherited from
endFrame()?
ts
optional endFrame(): void;Run after rendering. Input's end-of-frame bookkeeping lives here.
Returns
void
Inherited from
fixedUpdate()?
ts
optional fixedUpdate(dt): void;Run once per fixed step.
Parameters
| Parameter | Type | Description |
|---|---|---|
dt | number | Always ctx.config.fixedDt, whatever time.timeScale is; time scale changes how many steps a frame runs, not their length. |
Returns
void
Inherited from
init()
ts
init(ctx): AudioEngine;Build the engine and publish it as the 'audio' service.
Parameters
| Parameter | Type | Description |
|---|---|---|
ctx | HostContext | The host surface; only ctx.assets is read. |
Returns
The audio engine.
Overrides
update()
ts
update(dt, alpha): void;Pump the engine: retire finished voices and fill service.ended. A no-op before init.
Always present on this module, so callers need no optional call.
Parameters
| Parameter | Type | Description |
|---|---|---|
dt | number | Wall-clock seconds since the previous frame; unused, because the engine times voices on the audio context's own clock. |
alpha | number | Interpolation factor; unused by audio. |
Returns
void
Overrides
AudioModuleOptions
Options for audio.
Extends
Properties
concurrency?
ts
optional concurrency?: Record<string, {
max: number;
retriggerMs?: number;
rule?: "stopOldest" | "preventNew" | "stopQuietest" | "stopFarthest";
}>;Per-group limits; missing groups use max 4, stopOldest, and a 30 ms retrigger delay.
Inherited from
AudioEngineOptions.concurrency
context?
ts
optional context?: AudioContext;An existing context to drive. When omitted a new AudioContext is constructed, which requires a DOM environment. Pass one in tests.
Inherited from
distanceLowpass?
ts
optional distanceLowpass?: false | Partial<DistanceLowpass>;The low-pass every positioned voice gets with distance, or false for none. Defaults to DEFAULT_DISTANCE_LOWPASS (8 m, 80 m, 2.5 kHz).
Inherited from
AudioEngineOptions.distanceLowpass
masterVolume?
ts
optional masterVolume?: number;Linear gain on the master bus, 0..1. Defaults to 1.
Inherited from
AudioEngineOptions.masterVolume
maxVoices?
ts
optional maxVoices?: number;Maximum admitted non-reserved voices. Defaults to 32; fade tails are excluded.
Inherited from
music?
ts
optional music?: MusicPlayerOptions;Options for the music player (engine.music).
Inherited from
order?
ts
optional order?: number;Position in the module order. Lower runs first. Defaults to 30.
reservedBuses?
ts
optional reservedBuses?: string[];Protected buses, excluded from maxVoices. Defaults to music and dialogue (voice).
Inherited from
AudioEngineOptions.reservedBuses
unlock?
ts
optional unlock?: Omit<AudioUnlockOptions, "target">;The phone unlock's settings (unlock.ts): play as media under the silent switch, the silent loop for older phones, a change listener. Defaults on.
unlockTarget?
ts
optional unlockTarget?: EventTarget;Where the unlock listeners are installed. Defaults to the global object; pass the canvas to scope it, or a stub in tests.
AudioUnlock
The unlock.
Properties
running
ts
readonly running: boolean;The context can make sound right now.
Methods
dispose()
ts
dispose(): void;Remove the listeners and stop the silent loop.
Returns
void
unlock()
ts
unlock(): void;Do everything a tap allows, now: the session type, the silent loop, the resume, a silent sample. Call it from your own gesture handler if you have one; the listeners call it for you.
Returns
void
AudioUnlockOptions
Options for createAudioUnlock.
Properties
keepAlive?
ts
optional keepAlive?: boolean;On a phone with no audioSession setting (an older iPhone), keep the output open with a silent looping <audio> element. Default true.
navigator?
ts
optional navigator?: Navigator;The navigator to read the session and the device from; the global one by default.
onChange?
ts
optional onChange?: (running) => void;Told each time the context's state changes.
Parameters
| Parameter | Type |
|---|---|
running | boolean |
Returns
void
onGesture?
ts
optional onGesture?: () => void;Called synchronously on every gesture, including while the context is running.
Returns
void
playback?
ts
optional playback?: boolean;Play the page as media, so a phone's ring/silent switch does not mute it (navigator.audioSession.type = 'playback'). Default true; false for a page that records the microphone and wants the call-style session.
target?
ts
optional target?: EventTarget;Where the first-touch listeners go. Defaults to the global window, so a tap anywhere on the page (a button, the canvas, the talk panel) counts.
BeatGrid
A track's beat grid.
Properties
beatsPerBar?
ts
optional beatsPerBar?: number;Beats in a bar. Defaults to 4.
bpm
ts
bpm: number;Beats per minute. 0 or less: no grid.
firstBeat?
ts
optional firstBeat?: number;Seconds from the start of the file to beat 0. Defaults to 0.
BeatTick
A beat the player heard, as drained by MusicPlayer.drain. Pooled: do not keep it.
Properties
bar
ts
bar: number;Bar number, from 0.
beat
ts
beat: number;Beat number since the music started, from 0.
beatInBar
ts
beatInBar: number;Beat within the bar, from 0.
lateMs
ts
lateMs: number;How long ago the beat was heard, in milliseconds (0 or more).
BusFx
One bus's effect setting: a preset, and optional fields that override that preset's own value for this call.
Properties
compressor?
ts
optional compressor?: boolean;Whether the bus runs through a compressor.
highpassHz?
ts
optional highpassHz?: number;High-pass cutoff in Hz. 10 or less is off.
lowpassHz?
ts
optional lowpassHz?: number;Low-pass cutoff in Hz. 22000 or more is off.
preset?
ts
optional preset?: FxPreset;The base setting. Defaults to 'dry'.
reverb?
ts
optional reverb?: number;Reverb wet level, 0..1. 0 is no reverb.
DistanceLowpass
The distance low-pass curve applied to every positioned voice.
Properties
far
ts
far: number;Metres at which the cutoff reaches minHz. Default 80.
minHz
ts
minHz: number;Cutoff at and beyond far, in Hz. Default 2500.
near
ts
near: number;Metres up to which the voice is unfiltered. Default 8.
MusicPlayer
The music service, engine.music.
Properties
blocked
ts
readonly blocked: boolean;True when the browser refused play() outside a gesture; the next gesture retries.
grid
ts
readonly grid: Readonly<Required<BeatGrid>> | undefined;The current grid, or undefined when the music has none.
playing
ts
readonly playing: boolean;True while a track or stem set is playing (or fading in).
Methods
dispose()
ts
dispose(): void;Stop and release the decks.
Returns
void
drain()
ts
drain(onBeat, onTrack): void;Hand the beats and track starts since the last drain to the host, then forget them.
Parameters
| Parameter | Type | Description |
|---|---|---|
onBeat | (tick) => void | Called once per beat heard (at most 16 per drain), oldest first. |
onTrack | (index) => void | Called with the index of each track that started. |
Returns
void
nextGridTime()
ts
nextGridTime(quantize, subdivision?): number | undefined;The context time of the next grid point after now (+10 ms of lead).
Parameters
| Parameter | Type | Description |
|---|---|---|
quantize | Quantize | 'beat' or 'bar'; 'now' returns context.currentTime. |
subdivision? | number | Grid points per beat for 'beat'. Defaults to 1. |
Returns
number | undefined
Context seconds, or undefined when the music has no grid.
play()
ts
play(args): void;Start music, crossfading from what is playing.
Parameters
| Parameter | Type | Description |
|---|---|---|
args | PlayMusicArgs | What to play and how. |
Returns
void
prime()
ts
prime(): void;Call synchronously inside a user gesture: unlocks the deck elements on phones and retries a blocked play().
Returns
void
setLayer()
ts
setLayer(
index,
gain,
fadeMs?
): void;Change one stem's gain.
Parameters
| Parameter | Type | Description |
|---|---|---|
index | number | Stem index in the stems passed to play. |
gain | number | Linear gain, 0..1. |
fadeMs? | number | Ramp length; 0 sets it now. |
Returns
void
stop()
ts
stop(fadeMs?): void;Fade the music out and stop it.
Parameters
| Parameter | Type | Description |
|---|---|---|
fadeMs? | number | Fade length; 0 cuts. |
Returns
void
update()
ts
update(): void;Advance crossfades, playlists, drift correction and the beat clock. Allocation-free.
Returns
void
MusicPlayerOptions
Options for the music player.
Properties
createMediaElement?
ts
optional createMediaElement?: () => HTMLAudioElement;Build a media element for a deck. Defaults to document.createElement('audio').
Returns
HTMLAudioElement
PlayArgs
One call to AudioEngine.play.
Properties
buffer
ts
buffer: AudioBuffer;The decoded buffer to play, normally from AudioEngine.decode.
bus?
ts
optional bus?: "dialogue" | BusName;Bus to route through. Defaults to 'sfx'; 'dialogue' aliases 'voice'.
group?
ts
optional group?: string;Concurrency group. Defaults to the decoded asset id, otherwise this call's id.
id
ts
id: number;Guest-minted sound handle. Replaces any sound already using this handle.
loop?
ts
optional loop?: boolean;Loop until stopped. Looping voices never end on their own. Defaults to false.
pitch?
ts
optional pitch?: number;Playback-rate multiplier; also scales the natural end time. Defaults to 1.
pos?
ts
optional pos?: Vec3;World position. When given the voice gets an HRTF PannerNode.
volume?
ts
optional volume?: number;Linear gain, 0..1. Defaults to 1.
when?
ts
optional when?: number;Context time (seconds, context.currentTime clock) to start at, for a sample-accurate scheduled start. Past times start now. Defaults to now.
PlayMusicArgs
One MusicPlayer.play.
Properties
fadeMs?
ts
optional fadeMs?: number;Crossfade from whatever is playing, in milliseconds. Defaults to 0 (a cut).
grid?
ts
optional grid?: BeatGrid;The beat grid of every track (or of the stems). Omit for no beat clock.
loop?
ts
optional loop?: boolean;Loop the track, the playlist or the stems. Defaults to false.
start?
ts
optional start?: Quantize;Start now, or on the current music's next beat or bar. Defaults to 'now'.
stemGains?
ts
optional stemGains?: readonly number[];Initial gain per stem, 0..1; missing entries are 1.
stems?
ts
optional stems?: readonly AudioBuffer[];Decoded, sample-locked layers, all started with one start(when).
tracks?
ts
optional tracks?: readonly StreamedTrack[];Streamed tracks in order; more than one is a playlist. Ignored when stems is non-empty.
volume?
ts
optional volume?: number;Linear gain, 0..1. Defaults to 1.
StreamedTrack
A track the player streams through a media element.
Properties
url
ts
url: string;Resolved URL. Game code never sees it: the module resolves asset ids.
Type Aliases
BusName
ts
type BusName = "master" | "sfx" | "music" | "voice";The buses a voice can be routed to. master is the sum of the other three.
FxPreset
ts
type FxPreset =
| "dry"
| "room"
| "hall"
| "cave"
| "underwater"
| "radio"
| "muffled"
| "arena";A named bus-effect setting. dry is every stage transparent.
Quantize
ts
type Quantize = "now" | "beat" | "bar";When something starts relative to the current music's grid.
Quat
ts
type Quat = readonly [number, number, number, number];A rotation, as a plain [x, y, z, w] quaternion (three.js / Jolt order).
Vec3
ts
type Vec3 = readonly [number, number, number];A world-space position, as a plain [x, y, z] array.
Variables
DEFAULT_DISTANCE_LOWPASS
ts
const DEFAULT_DISTANCE_LOWPASS: Readonly<DistanceLowpass>;The default distance curve.
FX_PRESETS
ts
const FX_PRESETS: readonly FxPreset[];Every preset name, in a fixed order (the WIT fx-preset order).
PACKAGE
ts
const PACKAGE: "@gameable/audio";Package identity marker.
Example
ts
import { PACKAGE } from 'gameable/audio';
console.log(PACKAGE); // 'gameable/audio'Functions
audio()
ts
function audio(options?): AudioModule;Create the audio module.
Register it in the engine's module list. init builds the AudioEngine and returns it, so it becomes the 'audio' service: the host maps play-sound / stop-sound / set-listener straight onto its methods and drains ended into sound-ended events each frame. Until then AudioModule.service is null and no AudioContext exists.
Browsers keep an AudioContext suspended until the page sees a user gesture, so init arms the phone unlock (unlock.ts): the first touch, key or click opens the output, with the session type and the silent loop a phone needs, and a context suspended again (a call, the lock screen) is opened again by the next touch.
Parameters
| Parameter | Type | Description |
|---|---|---|
options | AudioModuleOptions | Engine options, plus the unlock target and module order. |
Returns
The module, ready to register.
Example
ts
import { audio } from 'gameable/audio';
const mod = audio({ masterVolume: 0.8 });
const engine = mod.init(ctx);
const buffer = await mod.decodeAsset('pistol');
engine.play({ id: 1, buffer, pos: [0, 1, -3] });
mod.update(1 / 60, 0);
// The next shot needs no await: the decode has already settled.
const again = mod.decodedAsset('pistol');
if (again) engine.play({ id: 2, buffer: again });createAudioEngine()
ts
function createAudioEngine(options?): AudioEngine;Create the audio engine.
The graph is source -> gain -> [lowpass -> panner] -> bus -> [effects] -> master. Nothing is allocated per frame: voices are pooled, the listener basis is written into scratch arrays, and ended is reused.
Parameters
| Parameter | Type | Description |
|---|---|---|
options | AudioEngineOptions | Context to adopt and initial master gain. |
Returns
The engine, which is also the 'audio' module's service.
Example
ts
import { createAudioEngine } from 'gameable/audio';
const engine = createAudioEngine({ masterVolume: 0.8 });
const buffer = await engine.decode('pistol', bytes);
engine.play({ id: 1, buffer, pos: [3, 0, -4], bus: 'sfx' });
engine.update();
for (const id of engine.ended) console.log('ended', id);
engine.ended.length = 0;createAudioUnlock()
ts
function createAudioUnlock(context, options?): AudioUnlock;Arm the unlock on a context: the first gesture opens the output, and every gesture after a suspension opens it again.
Parameters
| Parameter | Type | Description |
|---|---|---|
context | AudioContext | The context to unlock. |
options | AudioUnlockOptions | See AudioUnlockOptions. |
Returns
The unlock; dispose() when the engine shuts down.
Example
ts
import { createAudioUnlock } from 'gameable/audio';
const unlock = createAudioUnlock(new AudioContext(), { onChange: (on) => console.log(on) });
unlock.dispose();createMusicPlayer()
ts
function createMusicPlayer(
context,
output,
options?
): MusicPlayer;Create the music player on a context, feeding output (the music bus).
Parameters
| Parameter | Type | Description |
|---|---|---|
context | AudioContext | The engine's context. |
output | AudioNode | Node the decks connect to. |
options? | MusicPlayerOptions | Media element factory. |
Returns
The player.
isAppleTouchDevice()
ts
function isAppleTouchDevice(nav): boolean;Whether this is a phone or tablet whose output needs keeping open: an iPhone or an iPad (which reports itself as a Mac with a touch screen).
Parameters
| Parameter | Type | Description |
|---|---|---|
nav | Navigator | undefined | The navigator. |
Returns
boolean
True on an iPhone, an iPod or an iPad.