Audio · class
AudioGraph
Explained in Your first game.
class AudioGraphimport { AudioGraph } from '@driftengine/audio';Properties
| Name | Type | Description |
|---|---|---|
contextreadonly | BaseAudioContext | |
registryreadonly | SoundRegistry |
Accessors
| Name | Type | Description |
|---|---|---|
consoleget | MixConsole | The mix this graph plays into, for a caller that wants a bus of its own. |
audibleget | boolean | Whether the context is actually producing sound.MoreA context built without a user gesture is |
playingget | boolean | Whether the stems are running. |
heldget | boolean | Whether the transport is stopped mid-track, as opposed to not yet started. |
startsInSecget | number | Seconds until the scheduled start of whatever is playing, or 0 if it is
already sounding.MoreReads the instant |
layoutget | DefaultLayout | The layout this graph plays into: its buses, its inserts and its returns.MoreThis is where the mix went in 3.0.0. Every setter this class used to carry — the two
volumes, the lift, the slam, the master filter, the three sends and the delay — is a method on
a bus or an insert now, and |
levelsget | MixLevels | The two levels a player chose, read from the buses that hold them.MoreDerived rather than mirrored, which it was until 3.0.0. A mirror is a second place the answer
is decided, and the reason the old one existed — that a level is ramped, so mid-ramp the
parameter is between two values — is answered by the bus itself keeping its own fader
setting. A duck does not move it, which is the distinction |
Methods
createstatic
static create(options: AudioGraphOptions): Promise<AudioGraph | null>Build a graph, or return null only if the browser has no audio to give.
| Parameter | Type | Description |
|---|---|---|
options | AudioGraphOptions |
More
Null means "this browser will not do audio at all", never "not yet". The
distinction is the whole of a bug that silenced audio on mobile devices, both
Android and iOS: this used to await context.resume()
inside the try, so a browser that rejects that call — which is what a
rejection means when autoplay is blocked — threw a perfectly good graph into
the catch and reported no audio. The caller latches its load so it happens
once, so that null was permanent: silence for the session, with a wake()
that had nothing left to wake.
A suspended context is not a failure. Its clock does not advance, so nothing
scheduled on it is missed, and wake() exists to start it on the first
gesture. The resume is still attempted here, because when this is called
from a gesture — or on a site the browser already trusts — it starts
immediately and there is no reason to wait for a tap that already happened.
It is just no longer awaited, and no longer fatal.
Desktop cannot show you this. Chrome grants autoplay to a site its user keeps visiting, so on the machine this game is built on the context comes up already running. It only breaks on a device that has not earned that trust, which is every phone arriving from a share link.
wake
wake(): voidAsk the browser to start the context, if it will.
More
Safe to call from anywhere and safe to call repeatedly: outside a gesture the promise simply rejects, which is not an error state — it is the policy working. Anything already scheduled begins when it succeeds, because a suspended context's clock does not advance, so nothing is missed in the meantime.
loadStem
loadStem(index: number, buffer: AudioBuffer): void| Parameter | Type | Description |
|---|---|---|
index | number | |
buffer | AudioBuffer |
start
start(): voidStart every stem at one scheduled instant.
More
Layers must be sample-locked: started independently they drift apart by
however long each start() call happened to take, and a bassline a few
milliseconds off its drums is heard as flamming rather than as one track.
hold
hold(): voidStop the stems where they are.
More
The tape stops — this is a pause, not a duck, and the distinction is what
several rounds of feedback kept correcting toward: the pause itself was right, it
only ever needed a longer tail of effects to keep the music in the background.
So the source stops
and the sends carry what was already in flight. Reverb and delay live downstream
of the stems, so cutting the source is exactly what leaves a decaying tail
behind, and setLongReverbSend is how far that tail reaches.
Idempotent: the caller is a per-frame mix that knows a state, not an event.
release
release(): voidStart the stems again from where hold left them.
More
From where it left them, rather than from where the tape would have been: a pause that catches up is a jump cut, and on a long glide it is audible as the track skipping. The cost is that airborne time puts the score behind the route's bar grid — a real trade, taken deliberately, because the hold is felt on every jump and the grid is felt once at the start line.
restart
restart(fromSec?: number): numberStop the stems and start them again: from the top, or from fromSec into the track.
| Parameter | Type | Description |
|---|---|---|
fromSec? | number |
More
A BufferSource cannot be rewound — the spec makes it one-shot — so starting
over means discarding the sources and creating new ones. That is cheap: a
source node is a handle onto a buffer that is already decoded and already
resident, and nothing about the graph downstream of it is rebuilt.
Exists because a caller needs the track's beat zero to coincide with something in its own world. Left running instead, a loop's downbeats land somewhere different on every attempt.
fromSec is a seek, for music held to a clock of its own: a demo's frame counter or a
replay's tick has to resume the score where that clock is after every skip, and every stem
starts there at one instant, wrapped by its own length because the stems loop. A caller that
ran its own source into the music bus instead had a track outside every stem's lock. Anything
not a positive number is the top.
Returns how long until that place is actually heard, in seconds, because
launch schedules a little ahead of now and a caller lining a picture up
against the music needs that number rather than an assumption. Zero when
nothing started.
at
at(seconds: number | null): voidSchedule everything that follows at seconds on this context's timeline, or at
"now" when null.
| Parameter | Type | Description |
|---|---|---|
seconds | number | null |
More
An offline render sets it once per frame and gets a mix whose every move lands where the picture is, exactly, with no clock involved. Live callers never touch it.
setStemGain
setStemGain(index: number, gain: number): void| Parameter | Type | Description |
|---|---|---|
index | number | |
gain | number |
setPlaybackRate
setPlaybackRate(rate: number): void| Parameter | Type | Description |
|---|---|---|
rate | number |
createLoop
createLoop(buffer: AudioBuffer | undefined): AmbientLoop | nullStart a looping environmental bed, silent until the caller gives it a level. Routed through the effects stage, so the effects slider governs the world's own sound and the music slider governs only the score.
| Parameter | Type | Description |
|---|---|---|
buffer | AudioBuffer | undefined |
More
Returns null for a missing buffer, so a caller can create loops unconditionally and let an unresolved slot simply be silent.
createKickDetector
createKickDetector(): KickDetector | nullA live kick detector listening to the music.
More
Tapped off the music stage rather than the master bus, so it hears the track and not the game's own sound effects — a splash landing on the beat would otherwise read as a kick and flash the world.
The taps are pure observers: nothing is connected onward from them, so inserting a detector cannot change what anyone hears.
captureStream
captureStream(): MediaStream | nullA stream of everything the player is hearing, for a clip recording.
More
Tapped off the mix rather than replacing the destination, so recording cannot silence the game — a clip that captures perfectly while the player hears nothing is a bug they would report as "the export broke the sound".
Off out, which is the whole mix, and not off master, which is the dry
path. The send returns rejoin downstream of the master filter, so a tap on
master hears the track and the speed filter and nothing wet at all. See
out.
The alignment of this against the video is not ours to fix, and that was
measured rather than assumed. A probe that flashed one frame white while
scheduling a click at the same instant, decoded back out of the file, found the
audio leading the picture by a mean of 51 ms in one run and 85 ms in the next, with
outputLatency reporting 0.048 then 0.024. Feeding the tap through a delay does
move it — a forced 200 ms landed at +132 ms, near one for one — but there is no
constant to use: correcting by an unstable reading made a run worse, from -51 to
-85. MediaRecorder aligns its tracks by when data reached it, and nothing here can
see that. The offline path exists because it never asks this question.
The same tap every time. Building one per recording left the last one connected and running.
Null while the context is not running, audible false. A suspended context's
stream is a live track that never carries a sample, and a recorder given one stalls
on it: a recording started before the page's first gesture kept 19 frames of 120 and
no sound. Asked again once the context runs, it answers the tap.
play
play(buffer: AudioBuffer | undefined, gain?: number, pan?: number): voidFire a one-shot. Routed so it sits under the same master filter and sends.
| Parameter | Type | Description |
|---|---|---|
buffer | AudioBuffer | undefined | |
gain? | number | |
pan? | number |
More
pan places it across the stereo field (-1 to 1); pass the result of
stereoPan. Omitted, the sound is centred, which is right for anything
that happens to the player rather than somewhere near them.
dispose
dispose(): void