Skip to content

Audio

The engine owns sound. A game declares a cue under a name and plays it by that name from a tick; the audio graph, the mixing, the mute state, the listener, and the first-gesture unlock a browser requires sit below the name and belong to the engine.

A cue is either synthesized from a description or backed by a produced audio file. Both are declared by name and both are played by name, from a point in the world or from nowhere in particular, so the simulation reads the same either way.

A synthesized cue is one oscillator through one gain node. The waveform is chosen per cue, the frequency sweeps linearly from a starting value to an ending value across the cue’s duration, the gain decays from a peak to silence over that same span, and both nodes stop when the duration ends.

That is the entire synthesis model. Five numbers describe a bleep, a rising chime, or a falling thud, which is the vocabulary a game’s effects need. A produced game gets its sound without shipping a file and without spending any of its build effort on oscillators, gain envelopes, or the autoplay policy.

A cue may instead name an audio file, resolved under the asset root and decoded once when it is declared. This is how a game plays sound the asset-generation tools produced: a case that generates its own effects or music binds each produced file to a cue name, and the simulation plays that name exactly as it plays a synthesized one.

Both kinds share one namespace, so replacing a placeholder bleep with a produced file changes the declaration and leaves every play site alone. Declaring a name that already holds a cue replaces it, which is what lets a sound be retuned mid-run.

A cue played once ends on its own. A cue looped sounds until a tick stops it, which is what an engine hum, a held thruster, or a music bed needs. A file-backed cue loops its decoded buffer seamlessly, and a synthesized cue holds its waveform at its starting frequency and its gain, so the same five numbers that describe a bleep also describe a drone.

A cue is either looping or not. Starting a loop that is already running does nothing, and stopping one that is not running does nothing, so an actor or a game mode drives a loop from its state on every tick without counting starts against stops. Each transition is announced once, as cue:looped when the loop starts and cue:stopped when it ends.

A loop belongs to the cue name, and so to the engine rather than to the world currently open: it runs across a level transition until a tick stops it, and a positioned loop keeps its point across the transition and is heard from the new world’s camera. Redeclaring a looping name stops the loop, mute silences and restores every running loop in place, and a loop requested before the unlock is looping from that moment and begins to sound when the context opens. Destroying the engine stops every loop.

A cue played with a position sounds from that point in the world. The call names the point in world units, on the same axes an actor’s transform is written in, and the engine routes the cue through a panner: the inverse distance model with a reference distance of one world unit, a rolloff of one, a maximum distance of ten thousand, and HRTF panning. A cue one unit from the listener sounds at its full gain, a cue farther away sounds quieter, and every positioned cue sounds from its direction. A cue played without a position is unpositioned, so a menu confirmation and a music bed are declared and played with nothing added.

The listener is the camera. What a player hears is placed against what the player sees, so the engine writes the camera’s world position and orientation onto the listener every frame, reading the camera as it stood at the most recent render. A tick runs before its frame renders, so a cue a tick places is heard from the camera of the picture on screen, and a following camera carries the listener with it without the game touching either.

A one-shot cue is placed once, at the point it was played from. A loop is placed when it starts and moved afterwards through place, which is what a sound bound to a moving actor needs: the actor’s tick places the loop at its own position each frame, and the panner follows. Placing emits nothing and does nothing for a cue that is not looping, so an actor drives it from its state on every tick without checking first.

A cue is a fact about the game, not about the world currently open. Definitions therefore sit on the engine and survive a level transition, so a name declared during initialization plays in every level that follows and a level opens with its whole vocabulary already live.

That placement keeps a shared sound declared once. A hit, a pickup, and a menu confirmation belong to the game, so they are declared during initialization and every level plays them by name. A level that carries sound nothing else uses declares that cue in its own load, which the engine awaits before the world is built, so the level’s first frame can play it.

The bus carries two bits, because a silent game is silent for one of two very different reasons.

Muted is a decision the game or the player made. Unlocked records that a user gesture reached the engine, which is what a browser demands before it will start an audio context at all. The engine listens for the first pointerdown or keydown event on its surface and opens the context there, once, so a build supplies no gesture handler and no enable-sound screen of its own.

Keeping the two bits apart is what lets a silent build be diagnosed. A muted bus is the build behaving as asked. A locked bus is an environment nothing has clicked yet, which says nothing about the build. The unlock is also announced as the audio:unlocked event, so a caller watching a build come up sees the moment the context opened.

Playing, looping, stopping, or placing a cue that was never declared throws, naming the cue. Silence is the expected outcome of a muted or a still-locked bus, so the throw is what distinguishes a mistyped name from a cue that played inaudibly, and the run itself reports the mistake on the frame that made it.

Each play emits a cue:played event carrying the name, the time it played at, the gain it played at, and the point it was placed at, or null for an unpositioned cue. The payload is semantic: it states that a named cue happened, so it reads identically under a muted engine, a still-locked bus, and a browser with no audio at all. A muted play is announced at a gain of zero, which keeps the mute visible and keeps a game that reacted to an event while muted distinguishable from one that never reacted. The gain is the gain the cue was played at, before any distance attenuation, so a cue placed far from the camera is announced at the same gain as one placed beside it.

This is what establishes that a build sounded a cue in response to something that happened, and where it sounded it from. A subscriber holding the collision, the score change, or the level transition it was watching for sees the cue arrive against the same frame, so the claim under check is that the game responded rather than that a speaker moved.

Delivery is synchronous, inside the play. A subscriber therefore runs while the frame that played the cue is still running, and can attribute the cue to that frame’s counter, respond to it, or count it against a frame it holds. A subscription also keeps the engine’s memory flat over a run of any length, and puts the shape of the record with the code that wants one.

An actor, a component, a controller, and a game mode all reach the bus through the world they belong to, and each plays from its own tick. What a frame sounds is therefore decided by the same code that advanced the simulation, at the point where the event that warrants the sound was detected.

The rendering pipeline plays nothing. A frame’s picture is produced from the world the ticks settled, and a frame’s sound is produced by the ticks themselves, so a redraw of an unchanged world is silent.

A cue’s timestamp is the loop’s accumulated simulated time, so it lines up with the frame counter and with the simulated time everything else is expressed in.

That is what keeps the timestamp meaningful under a scripted clock. Frames stepped through engine.advance run back to back and no real time passes, so wall-clock stamps would put every cue of a several-hundred-frame advance at the same instant. Read against the frame clock, a cue’s time says which frame of the simulation it belongs to, and an ordering assertion over cues becomes an assertion about the game’s behavior over its own timeline.