The pack
audio
function audio(opts: AudioOptions = {}): Pack<{ sound: SoundApi }>
The synth, as a pack. The kernel owns the unlock moment (input.firstGesture), the chance source (Law 4), and the shell pause (the context suspends with the clock).
Functions
jingleStep
function jingleStep(spec: SoundSpec): number
Seconds a jingle step lasts for this spec.
noteFreq
function noteFreq(name: string): number
Frequency in Hz of a named note — 'C5', 'F#4', 'Bb3' (letter, optional #/b, octave).
Equal temperament, A4 = 440. Returns 0 for an unrecognised name.
parseJingle
function parseJingle(notation: string): { events: JingleEvent[]; total: number }
Parse jingle notation: whitespace-separated tokens, each a note (C5), a chord (C4+E4+G4),
a hit (x — the voice's own sound, for drums and zaps in time) or a rest (-); a token takes
! to accent or ~ to soften, and a length multiplier (C5:3, -:2, x!:0.5).
Unrecognised tokens advance time like a rest, so rhythm survives a typo.
soundLength
function soundLength(sound: Sound): number
Seconds a sound lasts — the longest of its layers. 0 for a URL.
soundTail
function soundTail(sound: Sound): number
Seconds until a sound is completely silent — its length plus the longest echo tail and release.
specLength
function specLength(spec: SoundSpec): number
Seconds one spec sounds for, including its delay, not counting an echo tail.
Types
AudioInput
interface AudioInput
What the pack puts in the registry under ('audio-input', 'audio'), so a pack that plays a media element (video)
sends its sound through the mixer: the master and bus volumes and mute then apply to it.
name: string— The registry name:'audio'.route(el: HTMLMediaElement, bus: 'music' | 'sfx'): { gain: number } | null— Routeelintobus, or move it there. Returns the route, whosegain(0..1) is the element's own volume, or null where there is no WebAudio, and the element then plays on its own.
AudioOptions
interface AudioOptions
Manifest options for the audio pack.
suspendWhenHidden?: boolean— Suspend audio while the page is hidden. Default true; false permits background music when simulation also continues.masterVolume?: number— Master bus gain, 0..1. Default 0.25.sounds?: Record<string, Sound>— Sounds defined at attach, by name; the same as callingdefineAll. Default none.
AudioTap
interface AudioTap
What the pack puts in the registry under ('audio-tap', 'audio'), so a recorder can mux the mix into a clip.
name: string— The registry name:'audio'.track(): MediaStreamTrack | null— A live track of the master mix, or null when nothing has played yet and there is no audio graph.stop(): void— Release the tap.
Bus
type Bus = 'master' | 'music' | 'sfx'
A mixer bus. music and sfx feed master.
JingleEvent
interface JingleEvent
One scheduled step of a parsed jingle: simultaneous frequencies (or a HIT — the voice's own freq/sweep/noise, the drum-machine case), start and length in steps, and a loudness multiplier.
freqs: number[]— Pitches to sound together; empty for a hit (x), which plays the spec's own tone.start: number— When the event starts, in steps from the start of the jingle.len: number— How long the event lasts, in steps.accent: number— 1 normally;!accents (1.35),~softens (0.45).
MusicOptions
interface MusicOptions
Options for sound.music(url, opts) — looped background music / BGM / audio track.
loop?: boolean— Loop the track. Defaulttrue.volume?: number— Track volume 0→1 relative to master. Default 1.fadeIn?: number— Fade-in duration in seconds. Default 0 (instant), orcrossfadewhen that is set.crossfade?: number— Cross-fade from the track playing now over this many seconds: it fades out while this one fades in. Default 0 (a cut).
NoiseColor
type NoiseColor = 'white' | 'pink' | 'brown'
Noise colour: white is hiss, pink is rain, brown is rumble.
PlayOptions
interface PlayOptions
How loud, and where: a sound played at a place in the world is panned and fades with its distance from the
listener (listener(x, y, z, yaw, pitch)), in world units with y up.
volume?: number— Gain 0..1 on top of the sound's own volume. Default 1.at?: { x: number; y?: number; z: number }— Where it plays, in world units; without it the sound is not positional.near?: number— Distance at full volume, world units, above 0. Default 2.far?: number— Distance past which it is not played at all, world units. Default 60.rolloff?: number— How fast it fades pastnear, 0 to 10: the inverse-distance rolloff. Default 1.
Sound
type Sound = SoundSpec | SoundSpec[] | string
A sound definition: a synthesised SoundSpec, an array of specs played together (a layered/compound sound), or a URL string to an audio file (mp3/ogg/wav).
SoundApi
interface SoundApi
What game.sound presents. Every method is a no-op where WebAudio is unavailable; before the first gesture sounds are dropped and music waits for it.
define(name: string, sound: Sound): void— Name a sound once (the art/audio agent's contract); play it by name everywhere. Unknown names are a dev error. A URL sound starts loading at once, through the loader: defined beforegame.start(), it is preloaded.defineAll(sounds: Record<string, Sound>): void—defineevery entry of a record; its URL sounds load as one job. Defined beforegame.start(), they are part of the preload, fetched and decoded under the loading screen and ready for their firstplay.play(name: string, opts?: PlayOptions): Voice— Play a defined sound. Throws in dev on an unknown name; silent in production. Returns a function that stops it early.{ volume }scales it;{ at: { x, y, z } }plays it at a place in the world, panned and fading with its distance from thelistener(nearfull volume, default 2;farnot played at all, default 60;rolloffhow fast it fades, default 1), and the returned voice'smove(x, y, z)follows whatever makes it.listener(x: number, y: number, z: number, yaw: number, pitch?: number): void— Where the player's ears are, for positional sounds: world units with y up, the engine'syaw(0 faces +z, turning right as it grows) andpitch(positive looks down). Call it as the camera moves, once a frame.playSpec(spec: SoundSpec | SoundSpec[]): Stop— A one-off spec, for prototyping. Prefer define + play by name.loop(name: string, opts?: PlayOptions): Voice— Play a defined synthesised sound back to back until stopped — a chiptune written in jingle notation becomes the background music. Returns the stop function. One loop at a time is the usual arrangement; the API does not enforce it.length(name: string): number— Seconds a defined sound lasts (its longest layer), 0 for a URL or an unknown name.music(track: string, opts?: MusicOptions): void— Stream a track on the music bus, replacing the current one. Defaults: loop, volume 1, no fade.trackis a key the loader holds ({ kind: 'audio', key, url, stream: true }) or the URL of a file it holds under any key, and then plays the file already fetched; any other URL streams from the network, in the background. Throws in dev on a name that is neither a loaded key nor a URL. Asked for before the first gesture, it starts on that gesture.{ crossfade: s }fades the current track out while this one fades in.sample(key: string, opts?: PlayOptions & { rate?: number; loop?: boolean; pan?: number }): Voice— Play a SAMPLE the loader fetched (assets.load([{ kind: 'audio', key, url }])). It was decoded inside the load job, so it plays at once; astreamfile is decoded on its first play instead, in the background, and plays when that lands.volume0..1,rate,loop,pan-1..1 orata place asplaytakes. Returns a stop function. Throws in dev on a key the loader has not got, saying so when it failed to load; a no-op before the first gesture or headless.stopMusic(opts?: { fadeOut?: number }): void— Stop the music;fadeOutin seconds fades first (default 0).readonly musicTime: number— Seconds into the current music track, as heard: what to time a logo's landing on the music's downbeat by. It wraps to 0 each time a looping track goes round, stands still while the game is paused or hidden and while the track buffers, counts from the start of a fade-in (a fade is only volume), and follows the new track through a cross-fade. The output latency the browser reports is taken off. 0 with no music, afterstopMusic, and before the track starts (music asked for before the first gesture waits for it). Headless, with no WebAudio, the track is played on the game clock instead: seconds of game time sincemusic, pausing with the game.readonly musicDuration: number— The current track's length in seconds once the browser has read it, else 0: no music, still loading, a live stream, or headless.volume(bus: Bus, v: number): void— Set a bus gain, 0..1, with no fade.readonly muted: boolean— True after themutecommand, untilunmute.readonly names: ReadonlyArray<string>— Every defined sound name, in definition order.
SoundEcho
interface SoundEcho
A feedback echo on the sound — caves, space, arcade halls.
time?: number— Seconds between repeats. Default 0.25.feedback?: number— How much of each repeat feeds the next, 0→0.9. Default 0.35.mix?: number— Level of the repeats against the dry sound, 0→1. Default 0.3.
SoundFilter
interface SoundFilter
A filter stage in a synthesised sound. Sweep a lowpass over 'noise' for crunchy explosions.
type?: BiquadFilterType— Filter shape ('lowpass','highpass','bandpass', …). Default'lowpass'.freq?: number— Cutoff frequency in Hz at the start of the sound. Default 1000.freqEnd?: number— Optional cutoff sweep target in Hz — e.g. 2400 → 60 collapses a blast into a rumble.sweep?: 'linear' | 'exponential'— Shape of the cutoff sweep. Default'exponential'.decay?: number— Seconds the sweep takes. Default: the whole sound. A short one is a filter envelope — the pluck of a bass.q?: number— Resonance (Q). Higher = a whistly peak at the cutoff. Default 1.
SoundSpec
interface SoundSpec
Parameters for a synthesised sound effect (SFX / beep / tone / noise), or a tune in jingle notation.
type?: Waveform | OscillatorType— Oscillator waveform, or'noise'. Default'square'.noise?: NoiseColor— Colour of'noise'. Default'white'.freq?: number— Start frequency in Hz. Ignored for'noise'. Default 440.freqEnd?: number— Optional frequency sweep target in Hz — creates a zap/pew slide.sweep?: 'linear' | 'exponential'— Shape of thefreq→freqEndslide.'exponential'sounds pitch-linear to the ear — better lasers, jumps, falls. Default'linear'.notes?: number[] | string— Pitch sequence (overridesfreq/freqEnd). -number[]— Hz steps spread evenly acrossduration, glued under one envelope (coins, power-up runs). -string— jingle notation: each note gets its own envelope (real articulation — 'ta-daa!' fanfares). Tokens: notesC5/F#4/Bb3, chordsC4+E4+G4, hitsx(this voice's own sound — drums), rests-; append!to accent,~to soften,:Nto stretch (C5:3= 3 steps). One step lastsstepseconds (or a 16th attempo); total length comes from the notation (durationis ignored).step?: number— Seconds per notation step in jingle mode. Default 0.12, or a 16th note whentempois set.tempo?: number— Tempo in BPM for jingle mode — a step is a 16th note (120 BPM = 0.125 s).stepwins if both are set.gate?: number— How much of each step a note sounds, 0→1: staccato at 0.5, legato at 1. Default 0.92.slide?: boolean— Glide between consecutive notes instead of stepping — 303 lines, sirens, slurs. Default false.duration?: number— Duration in seconds. Default 0.15.volume?: number— Amplitude 0→1 relative to the sfx bus. Default 1.attack?: number— Attack ramp in seconds. Default 0.005.decay?: number— Seconds to fall from the attack peak to thesustainlevel. Default 0 (no decay stage).sustain?: number— Level held after the decay, 0→1 ofvolume. Default 1. Withdecaythis is a real ADSR.release?: number— Release ramp in seconds. Default 0.06.curve?: 'linear' | 'exponential'— Shape of the release ramp.'exponential'= percussive, natural die-away (explosions, thumps, plucks). Default'linear'.jitter?: number— Random per-play pitch variation 0→1 (0.1 = up to ±10%) — keeps rapid-fire SFX from sounding robotic. Default 0.vibrato?: SoundVibrato— Pitch wobble (LFO):{ freq, depth }— sirens, drones, shimmer.tremolo?: SoundTremolo— Loudness wobble (LFO):{ freq, depth }— engines, alarms, helicopters.duty?: number— Pulse width 0→0.5 for'square'(0.5 = even square). 0.25/0.125 give classic thin chip-tune timbres.filter?: SoundFilter— Filter stage:{ type, freq, freqEnd, sweep, decay, q }. A swept lowpass over'noise'is the crunchy-explosion move.distortion?: number— Distortion drive 0→1 — adds grit/crunch (explosions, heavy impacts, dirty lasers). Default 0.crush?: number— Bit-crush 0→1 — fewer bits, more fizz; the lo-fi 8-bit stamp. Default 0.echo?: SoundEcho— Echo:{ time, feedback, mix }— space, caves, the arcade hall.pan?: number— Stereo position -1 (left) → 1 (right). Default 0 (centre).delay?: number— Start offset in seconds — sequence the layers of a compound sound (play([...])). Default 0.
SoundTremolo
interface SoundTremolo
Loudness wobble (LFO) — engines, helicopters, a pulsing alarm.
freq?: number— Wobble rate in Hz. Default 10.depth?: number— How far the level dips, 0→1. Default 0.5.
SoundVibrato
interface SoundVibrato
Pitch wobble (LFO) for a synthesised sound — sirens, UFO drones, magic shimmer. Ignored for 'noise'.
freq?: number— Wobble rate in Hz. Default 8.depth?: number— Wobble depth as a fraction of the base pitch (0.1 = ±10%). Default 0.06.
Stop
type Stop = () => void
Stops a playing sound (or a loop) early. Calling it after the sound has ended is harmless.
Voice
type Voice = Stop & { /** Move a positional sound to world `x, y, z`. */ move(x: number, y: number, z: number): void; }
A sound that is playing: call it to stop it early, and move it to follow what makes it (a no-op unless it was played at a place).
Waveform
type Waveform = 'sine' | 'square' | 'sawtooth' | 'triangle' | 'noise'
Oscillator waveforms, plus 'noise'.


