Audio
Audio buses, SFX voice pool, crossfading music, and procedural synthesis.
Bus volume, mute, and the browser autoplay-unlock state. For the higher-level subsystem walkthrough (bus graph layout, locked vs. unlocked, and web autoplay constraints), see the Audio Guide.
The audio graph has three buses: 'sfx' and 'music' feed into 'main', which feeds the browser's audio destination.
All three are independently controllable.
sfx ─┐
music ─┼─► main ─► destinationAudioBus is the type of a bus name:
AudioBus Since 1.3.0
declare const : ; // 'main' | 'music' | 'sfx'Volume
BT.audioVolumeSet Since 1.3.0
BT.audioVolumeGet Since 1.3.0
BT.audioVolumeSet sets a bus's volume, optionally fading to it. BT.audioVolumeGet reads it back.
.('music', 0.5); // immediate change
.('sfx', 0, { : 300 }); // linear fade to silence over 300 ms
.('main', 0.8, { : 500, : 'ease-out' }); // eased fade
.('music'); // 0.5options fields:
Prop
Type
With no fadeMs, the bus gain changes immediately. With fadeMs, 'linear' easing schedules a linear ramp; any other
easing curve is sampled into a value curve so the fade follows that curve rather than a straight line.
Mute
BT.audioMuteSet Since 1.3.0
BT.isAudioMuted Since 1.3.0
BT.audioMuteSet mutes or unmutes a bus without touching its configured volume. BT.isAudioMuted reports the current
mute state.
.('music', 0.75);
.('music', true); // silences the bus immediately (no fade)
.('music'); // true
.('music'); // still 0.75 - muting never overwrites the configured level
.('music', false); // restores audio at 0.75BT.audioVolumeGet always returns the logical (pre-mute) volume, so reading it while muted still reports the level you
configured, not 0.
Unlock state
BT.isAudioUnlocked Since 1.3.0
Browsers block audio playback until a user gesture. BT.isAudioUnlocked is false until the first pointerdown,
pointerup, touchend, or keydown on the canvas successfully resumes the audio context, and stays true for the
rest of the session. On a touch screen that is the end of the first tap, since touchstart is not a user activation.
if (!.) {
// Show a "click or press a key to enable audio" prompt.
}See Web audio constraints for what happens to volume/mute calls made before the gesture, and why no configure flag can skip this requirement.
A BT.renderAt seek suspends the context while the loop is stopped (a gesture that unlocks audio mid-seek stays
suspended too) and drops BT.soundPlay calls made by stepped updates; BT.resume() brings it back. See
Seeking a frame with renderAt.
Loading
AudioClip Since 1.3.0
AudioClip decodes an audio file into a reusable AudioBuffer, exposing the winning source URL, duration, and sample
rate. Loading and decoding work even while the audio context is locked (suspended, pre-gesture) - only real-time
playback needs an unlocked context. See Preloading audio clips.
import { } from 'blit386';
// Load a single clip (cached by its resolved URL)
const = await .('audio/theme.mp3');
// Fallback list: tries each URL in order, resolving with the first that decodes
const = await .(['audio/hit.ogg', 'audio/hit.mp3']);
// Load multiple clips in parallel - each entry is a single URL or a fallback list
const = await .(['audio/theme.mp3', ['audio/hit.ogg', 'audio/hit.mp3']]);
// Check cache before loading
if (.('audio/theme.mp3')) {
// already cached
}
// Number of clip loads currently in flight
.;For a single loading-screen signal that also covers sprite sheet images, see
BT.loadingAssetsCount.
Pass onProgress to report phased load progress:
await .('audio/theme.mp3', {
: () => {
.(., .);
},
});Prop
Type
Release a clip's decoded buffer with unload() once you no longer need it:
.(); // releases the decoded buffer; safe to call more than onceAudioClip.load()andloadAll()throw a beginner-friendly error covering network/CORS failures, HTTP status errors, unsupported container/codec decode failures, and loading before the engine has started.- Prefer a fallback list (for example
['theme.ogg', 'theme.mp3']) for any clip whose primary format might not decode in every browser - see Audio formats. - Playing a loaded clip back as SFX is covered in Playback (SFX) below; as looping, crossfading music in Playback (Music).
Under a Vite dev server with the blit386/vite plugin installed, editing an audio file under a watched asset directory
(public/ by default) swaps the decoded buffer inside the same AudioClip instance - demo-held references stay valid.
Any SFX voice still playing the old buffer is stopped; if the replaced clip is the currently playing music track,
playback restarts immediately (fadeMs: 0, no crossfade). duration/sampleRate reflect the buffer decoded at initial
load and are not updated by a hot reload. See Hot Reload for the full
asset type matrix.
Synth
SynthParams Since 1.3.0
AudioClip.synth renders a clip procedurally from a SynthParams descriptor - no source file, no network request, and
no OfflineAudioContext. Rendering runs entirely on the CPU and is deterministic: identical params (including seed)
always produce identical sample data, so a SynthParams object can be stored and replayed as a preset. Unlike
Loading, there is nothing to fetch or decode from the network - a synthesized clip is ready the instant
synth() resolves, even before BT.isAudioUnlocked is true (see Unlock state above).
import { } from 'blit386';
const = await .({
: 'sawtooth',
: 880,
: 0.3,
: { : 110 },
: 1,
});SynthParams fields:
Prop
Type
SynthEnvelope Since 1.3.0
SynthEnvelope fields (all optional, times in seconds):
Prop
Type
The release phase always finishes exactly at duration, even on a very short clip where the attack, decay, and release
phases overlap - a percussive hit is never cut off mid-fade.
SynthPitchSweep Since 1.3.0
SynthPitchSweep fields:
Prop
Type
SynthVibrato Since 1.3.0
SynthVibrato fields (both optional):
Prop
Type
import { , } from 'blit386';
// A short, noisy boom: mostly noise, with a touch of low square tone, quick attack and decay.
const = await .({
: 'square',
: 90,
: 0.4,
: 0.8,
: { : 0, : 0.05, : 0.3, : 0.3 },
: 42,
});
.(, { : 0.7 });Not cached, not deduplicated
Unlike AudioClip.load(), AudioClip.synth() never populates the URL-keyed resolved cache and never deduplicates
concurrent calls - every call renders a fresh, independent AudioBuffer, even for byte-identical params. unload()
still works the same way (releases the buffer, stops any voice playing it), it just has no cache entry to clear.
Presets
BT.synthPreset Since 1.3.0
BT.synthPreset bundles six ready-made SynthParams factories for common sound effects: jump, pickup, explosion,
laser, hit, blip. Each takes an optional seed and applies small, bounded, deterministic jitter to a couple of
hand-picked fields (frequency, duration, or noise mix) - the same seed always renders the exact same variant, so a
preset stays reproducible even though it varies from call to call.
import { , } from 'blit386';
// Same character every time - useful for a UI sound that should stay consistent.
const = await .(..());
// A different seed per pickup keeps repeated collects from sounding robotic, while staying
// reproducible - a wall-clock seed like Date.now() would defeat that, since it can never be
// replayed the same way twice.
let = 0;
const = await .(..(++));
.();
.(, { : 0.8 });Omitting seed (or passing 0) always renders the same baseline variant - useful when you want a preset's default
character rather than per-play variation. See Playback (SFX) below for playing the resulting clip
through the SFX voice pool, and Design a sound in the Audio Guide for a walkthrough of
tuning SynthParams by hand and storing presets as data.
Playback (SFX)
BT.soundPlay Since 1.3.0
SoundRef Since 1.3.0
BT.soundStop Since 1.3.0
BT.isSoundPlaying Since 1.3.0
BT.soundPlay plays a loaded AudioClip through a fixed-size pool of SFX voices. Each call returns an opaque
SoundRef handle - pass it to BT.soundStop and the per-sound volume, pitch, and pan controls.
import { , } from 'blit386';
const = await .('audio/hit.mp3');
const = .(, { : 0.8 });
.(); // true
.(, { : 200 }); // fades out over 200 msSoundPlayOptions Since 1.3.0
options fields:
Prop
Type
Voice cap and stealing
HardwareSettings.audioVoices (default 16) sizes a fixed pool of voices - it never grows. When every voice is in use,
a new BT.soundPlay call steals the lowest-priority active voice at or below the incoming priority (ties broken by
whichever voice started first). If every active voice outranks the incoming priority, the new sound is dropped silently:
no throw, and the returned SoundRef is inert (BT.isSoundPlaying reports false for it immediately).
Set isOverlayAudioMetersEnabled: true to see active/total voices, steal count, and drop count live in the overlay
instead of reasoning about the pool from code - see Audio meters.
A common pattern - vary pitch slightly per play so a repeated sound (footsteps, hits) doesn't sound robotic, using the
seedable BT.random:
import { , } from 'blit386';
const = await .('audio/footstep.mp3');
function () {
const = ..(0.9, 1.1); // 0.9-1.1x
.(, { , : 0.6 });
}BT.soundVolumeSet Since 1.3.0
BT.soundVolumeGet Since 1.3.0
BT.soundPitchSet Since 1.3.0
BT.soundPitchGet Since 1.3.0
BT.soundPanSet Since 1.3.0
BT.soundPanGet Since 1.3.0
Per-sound controls, all silent no-ops on a stale or invalid SoundRef (already stopped, stolen, or completed - never
throws):
.(, 0.5, { : 100 });
.(); // 0.5
.(, 1.2);
.(); // 1.2
.(, -0.3);
.(); // -0.3BT.soundPlay also returns an inert SoundRef (no throw) when clip hasn't finished loading yet, or was already
released with clip.unload().
Playback (Music)
BT.musicPlay Since 1.3.0
BT.musicStop Since 1.3.0
BT.isMusicPlaying Since 1.3.0
BT.musicPlay plays a loaded AudioClip through a single looping music player, distinct from the SFX voice pool -
there is no SoundRef to manage, and calling it again crossfades from whatever is currently playing into the new track
rather than layering the two.
import { , } from 'blit386';
const = await .('audio/theme.mp3');
.(, { : 0.8 });
.; // true
.({ : 500 }); // fades out over 500 msMusicPlayOptions Since 1.3.0
options fields:
Prop
Type
Crossfading
fadeMs sets how long each side of a crossfade takes; overlap sets how the two sides line up in time:
import { , } from 'blit386';
const = await .('audio/calm.mp3');
const = await .('audio/battle.mp3');
.();
// Later, when the fight starts:
.(, { : 800, : 1 }); // calm fades out while battle fades in, at the same time
.(, { : 800, : 0 }); // battle starts fading in exactly as calm finishes fading out
.(, { : 800, : -1 }); // an 800 ms silence gap between the twoCalling BT.musicPlay again before a crossfade finishes immediately cuts the track that was already fading out - only
one crossfade is ever in flight, so rapid calls (menu navigation, quick scene changes) never pile up.
Loop points
Loop the whole track (the default), a one-shot, or a specific region such as a bridge that repeats while an intro plays only once:
import { , } from 'blit386';
const = await .('audio/theme.mp3');
.(); // loops the whole track (default)
.(, { : false }); // plays once and stops
.(, { : 8, : 32 }); // an 8s intro, then loops the 8-32s region foreverloopStart and loopEnd must be given together, with 0 <= loopStart < loopEnd <= duration - BT.musicPlay throws a
beginner-friendly error otherwise, since a mismatched pair is always a programmer mistake rather than something that can
happen from normal play.
BT.musicVolumeSet Since 1.3.0
BT.musicVolumeGet Since 1.3.0
BT.musicVolumeSet sets the current track's gain, optionally fading to it; BT.musicVolumeGet reads back the last
requested target (not a mid-fade instantaneous value):
.(0.4, { : 300 });
.(); // 0.4Remembered while locked, not dropped
Unlike BT.soundPlay, a BT.musicPlay call made before BT.isAudioUnlocked is true is not dropped - the engine
remembers the most recent pending request and starts it automatically the instant the context unlocks. Calling
BT.musicPlay again while still locked replaces the remembered request; only the latest survives to unlock.
BT.musicPlay silently does nothing (no throw) when clip hasn't finished loading yet, or was already released with
clip.unload() - the same as BT.soundPlay.
See Playing music in the Audio Guide for a state-based music switching pattern and an intro-then-loop recipe.
Hardware settings
audioVoices (default 16) caps the number of simultaneous SFX voices - see Playback (SFX) for the
allocation and stealing policy. Documented in Hardware settings.
API history
| Symbol | Since | Last changed | Status |
|---|---|---|---|
AudioBus | 1.3.0 | – | stable |
AudioClip | 1.3.0 | – | stable |
BT.audioMuteSet | 1.3.0 | – | stable |
BT.audioVolumeGet | 1.3.0 | – | stable |
BT.audioVolumeSet | 1.3.0 | – | stable |
BT.isAudioMuted | 1.3.0 | – | stable |
BT.isAudioUnlocked | 1.3.0 | – | stable |
BT.isMusicPlaying | 1.3.0 | – | stable |
BT.isSoundPlaying | 1.3.0 | – | stable |
BT.musicPlay | 1.3.0 | – | stable |
BT.musicStop | 1.3.0 | – | stable |
BT.musicVolumeGet | 1.3.0 | – | stable |
BT.musicVolumeSet | 1.3.0 | – | stable |
BT.soundPanGet | 1.3.0 | – | stable |
BT.soundPanSet | 1.3.0 | – | stable |
BT.soundPitchGet | 1.3.0 | – | stable |
BT.soundPitchSet | 1.3.0 | – | stable |
BT.soundPlay | 1.3.0 | – | stable |
BT.soundStop | 1.3.0 | – | stable |
BT.soundVolumeGet | 1.3.0 | – | stable |
BT.soundVolumeSet | 1.3.0 | – | stable |
BT.synthPreset | 1.3.0 | – | stable |
MusicPlayOptions | 1.3.0 | – | stable |
SoundPlayOptions | 1.3.0 | – | stable |
SoundRef | 1.3.0 | – | stable |
SynthEnvelope | 1.3.0 | – | stable |
SynthParams | 1.3.0 | – | stable |
SynthPitchSweep | 1.3.0 | – | stable |
SynthVibrato | 1.3.0 | – | stable |
1.3.0 - Mon Jul 13 2026
- Added
AudioBus - Added
AudioClip - Added
BT.audioMuteSet - Added
BT.audioVolumeGet - Added
BT.audioVolumeSet - Added
BT.isAudioMuted - Added
BT.isAudioUnlocked - Added
BT.isMusicPlaying - Added
BT.isSoundPlaying - Added
BT.musicPlay - Added
BT.musicStop - Added
BT.musicVolumeGet - Added
BT.musicVolumeSet - Added
BT.soundPanGet - Added
BT.soundPanSet - Added
BT.soundPitchGet - Added
BT.soundPitchSet - Added
BT.soundPlay - Added
BT.soundStop - Added
BT.soundVolumeGet - Added
BT.soundVolumeSet - Added
BT.synthPreset - Added
MusicPlayOptions - Added
SoundPlayOptions - Added
SoundRef - Added
SynthEnvelope - Added
SynthParams - Added
SynthPitchSweep - Added
SynthVibrato
See also
Audio Guide
API: Easing
API: Core
API: Overlay
API: Browser Support
Hot Reload Guide
Last updated on October 18, 2026