/**
 * Audio Buses Demo - mixer volume sliders, mute toggles, and a music-ducking alert.
 * @description Mixer buses: drag main, music, and sfx sliders, mute without losing the level, and duck for an alert.
 *
 * Part of the BLIT386 demo series.
 * Prerequisites:
 *   Audio Basics  https://demos.blit386.dev/audio-basics
 *   Music         https://demos.blit386.dev/music
 *
 * Live version: https://demos.blit386.dev/audio-buses
 *
 * Every sound in the engine flows through one of three "buses" before it reaches your
 * speakers: `main`, `music`, and `sfx`. Think of a bus like a volume knob that affects a
 * whole category of sound at once - turning down `music` fades every music track without
 * touching sound effects, and turning down `main` fades everything together.
 *
 * Drag any of the three sliders below (mouse or finger - the whole UI is touch-friendly) to
 * change that bus's volume with BT.audioVolumeSet(). Tap a Mute checkbox to silence a bus
 * with BT.audioMuteSet() - notice the slider value does not change when you mute; muting
 * hides the volume, it does not erase it. BT.audioVolumeGet() and BT.isAudioMuted() read
 * those two things back separately.
 *
 * The Alert button plays a short sound effect while "ducking" (temporarily lowering) the
 * music bus's volume so the alert is easy to hear over the background music, then fades the
 * music back up afterward - the same trick movies and games use so an important sound is
 * never buried under the soundtrack.
 *
 * The panels, sliders, checkboxes, and the button all come from the shared UI kit in
 * src/shared/ui.js - the same look every demo in this series uses. The kit works
 * "immediate-mode" style: render() simply declares the widgets it wants each frame, and the
 * kit draws them and answers clicks and taps on the spot.
 *
 * Click or press a key to unlock sound first (see Audio Basics for why browsers require
 * that first click).
 */

import { class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js'; /** @typedef {import('blit386').IBTDemo} IBTDemo */ /** @typedef {import('blit386').HardwareSettings} HardwareSettings */ /** @typedef {import('blit386').Palette} Palette */ const const DISPLAY_W: 320DISPLAY_W = 320; const const DISPLAY_H: 240DISPLAY_H = 240; // One entry per bus: which engine bus it controls, the slider label, the keyboard shortcut // that toggles its mute, and the letter shown in the checkbox label as a hint. const const BUSES: {}BUSES = [ { bus: stringbus: 'main', label: stringlabel: 'Main', muteKey: stringmuteKey: 'KeyQ', muteHint: stringmuteHint: 'Q' }, { bus: stringbus: 'music', label: stringlabel: 'Music', muteKey: stringmuteKey: 'KeyW', muteHint: stringmuteHint: 'W' }, { bus: stringbus: 'sfx', label: stringlabel: 'Sfx', muteKey: stringmuteKey: 'KeyE', muteHint: stringmuteHint: 'E' }, ]; // Ducking behavior: how far the music bus dips, how quickly it dips and recovers, and how // long it stays dipped before recovering. const const DUCK_VOLUME_FACTOR: 0.25DUCK_VOLUME_FACTOR = 0.25; const const DUCK_FADE_MS: 150DUCK_FADE_MS = 150; const const DUCK_HOLD_TICKS: 90DUCK_HOLD_TICKS = 90; const const DUCK_RECOVER_FADE_MS: 600DUCK_RECOVER_FADE_MS = 600; /** * Three volume sliders, three mute toggles, and a ducking alert button. * * @implements {IBTDemo} */ class class Demo
Three volume sliders, three mute toggles, and a ducking alert button.
@implementsIBTDemo
Demo
{
/** @type {Palette | null} */ Demo.palette: Palette | null
@type{Palette | null}
palette
= null;
/** Palette slots of the shared UI theme colors, filled by applyTheme() in init(). */ Demo.theme: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
= null;
/** @type {AudioClip | null} */ Demo.musicClip: AudioClip | null
@type{AudioClip | null}
musicClip
= null;
/** @type {AudioClip | null} */ Demo.alertClip: AudioClip | null
@type{AudioClip | null}
alertClip
= null;
/** Whether the music bus is currently ducked because of a recent alert. */ Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
= false;
/** Ticks remaining before a ducked music bus starts recovering. */ Demo.duckHoldTicksLeft: number
Ticks remaining before a ducked music bus starts recovering.
duckHoldTicksLeft
= 0;
/** Music bus volume captured right before the most recent duck, so it can be restored. */ Demo.preDuckMusicVolume: number
Music bus volume captured right before the most recent duck, so it can be restored.
preDuckMusicVolume
= 1;
/** * Sets the logical display size and turns on the engine's built-in audio meters in * the overlay. * * @returns {Partial<HardwareSettings>} */ Demo.configure(): Partial<HardwareSettings>
Sets the logical display size and turns on the engine's built-in audio meters in the overlay.
@returns
configure
() {
return { displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const DISPLAY_W: 320DISPLAY_W, const DISPLAY_H: 240DISPLAY_H),
// Live per-bus level meters and a voice-count readout in the overlay (off by default). isOverlayAudioMetersEnabled: booleanisOverlayAudioMetersEnabled: true, // Space is the Alert shortcut - keep the page from scrolling when it is pressed. isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true, }; } /** * Loads the background music and the alert sound, sets up the shared UI theme, and * starts the music loop. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Loads the background music and the alert sound, sets up the shared UI theme, and starts the music loop.
@returns
init
() {
this.Demo.musicClip: AudioClip | null
@type{AudioClip | null}
musicClip
= await class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
.AudioClip.load(url: string | string[], options?: AudioClipLoadOptions): Promise<AudioClip>
Loads an audio clip from a single URL, or from an ordered list of candidate URLs. A single URL runs the download+decode pipeline directly, sharing the per-URL cache and in-flight dedup described on {@link AudioClip } . A URL array tries each candidate in order and resolves with the first one that downloads and decodes successfully - useful for offering a browser-friendly fallback (for example `['music.ogg', 'music.mp3']`) when a container or codec isn't universally supported. If every candidate fails, the error from the last candidate is thrown.
@paramurl - Single audio URL, or an ordered list of fallback URLs.@paramoptions - Optional load options (progress reporting).@returnsLoaded clip, cached under its winning source URL.@throwsError if the URL (or every URL in the list) fails to load or decode.
load
('/audio/music-calm.wav');
// BT.synthPreset.hit() is one of the six built-in sound recipes Synth Toy // explores - a short, punchy stinger, a good fit for an alert. this.Demo.alertClip: AudioClip | null
@type{AudioClip | null}
alertClip
= await class AudioClip
Decoded audio asset with its winning source URL and buffer-derived metadata. Construct instances with {@link AudioClip.load } , {@link AudioClip.loadAll } , or {@link AudioClip.synth } ; there is no public constructor.
@since1.3.0
AudioClip
.AudioClip.synth(params: SynthParams): Promise<AudioClip>
Synthesizes a clip from deterministic procedural parameters - no source file, no `OfflineAudioContext`, and no audio graph involved. Rendering happens entirely on the CPU via the pure {@link renderSynthSamples } function against an `AudioBuffer` allocated from the registered decode context. The returned clip flows through the same {@link buffer } getter and playback path as a loaded clip, but uses a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the URL-keyed resolved cache and never deduplicated, so identical `params` still render a fresh, independent `AudioBuffer` on every call. See {@link SynthParams } for the full parameter set.
@paramparams - Deterministic synthesis parameters.@returnsA new clip wrapping the synthesized buffer.@throwsError if `params` fails validation, or the engine has not registered a decode context yet (see {@link audioClipNotReadyError }).
synth
(
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.
synthPreset: {
    jump: (seed?: number) => SynthParams;
    pickup: (seed?: number) => SynthParams;
    explosion: (seed?: number) => SynthParams;
    laser: (seed?: number) => SynthParams;
    hit: (seed?: number) => SynthParams;
    blip: (seed?: number) => SynthParams;
}
Pre-configured `SynthParams` presets for common sound effects ("jump", "pickup", "explosion", "laser", "hit", "blip"). Each function returns a fresh {@link SynthParams } object; pass it to {@link AudioClip.synth } to render a clip, then play the result via {@link BT.soundPlay } . An optional `seed` argument applies small, bounded, deterministic jitter to a few hand-picked fields per preset, so repeated plays vary without losing reproducibility - the same seed always renders the exact same variant.
@since1.3.0@exampleconst jumpClip = await AudioClip.synth(BT.synthPreset.jump()); BT.soundPlay(jumpClip);
synthPreset
.hit: (seed?: number) => SynthParams
Hit / damage taken: a short, low percussive tone mixed with noise for a punchy impact. `seed` jitters the base frequency (+/-8%) and `noiseMix` (+/-10%, clamped to `[0, 1]`). Omit `seed` for a fixed baseline variant.
@paramseed - Seed for deterministic jitter. Defaults to {@link DEFAULT_PRESET_SEED }.@returnsA fresh `SynthParams` for a hit sound effect.
hit
());
this.Demo.palette: Palette | null
@type{Palette | null}
palette
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
// applyTheme() installs the twelve shared UI colors (into high palette slots, far // from any scene colors) and hands back where they landed, so render() can clear // the screen with the theme's background color. this.Demo.theme: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
= import applyThemeapplyTheme(this.Demo.palette: Palette
@type{Palette | null}
palette
);
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(this.Demo.palette: Palette
@type{Palette | null}
palette
);
// Music started before the page is unlocked is "remembered" and begins the // instant you click or press a key - unlike BT.soundPlay(), which drops sounds // played too early.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.musicPlay: (clip: AudioClip, options?: MusicPlayOptions) => void
Plays a loaded audio clip through the music player, crossfading out whatever is currently playing. Silently does nothing when the clip hasn't finished loading yet (or was already unloaded with `clip.unload()`), or before the engine has initialized. While the audio context is still locked (before the first unlock gesture), the request is remembered instead of dropped - it starts automatically the instant the context unlocks, unlike {@link BT.soundPlay } .
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Crossfade, volume, and loop options; see {@link MusicPlayOptions}.
musicPlay
(this.Demo.musicClip: AudioClip | null
@type{AudioClip | null}
musicClip
, { MusicPlayOptions.loop?: boolean | undefined
Whether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.
loop
: true });
return true; } /** * Advances the duck-and-recover countdown. * * ui.tick() must run first: it is the kit's once-per-tick housekeeping that (among * other things) safely catches the keyboard shortcuts bound to the widgets below. * Keyboard presses can only be read reliably here in update(), never in render() - * keyboard-input explains why in detail. */ Demo.update(): void
Advances the duck-and-recover countdown. ui.tick() must run first: it is the kit's once-per-tick housekeeping that (among other things) safely catches the keyboard shortcuts bound to the widgets below. Keyboard presses can only be read reliably here in update(), never in render() - keyboard-input explains why in detail.
update
() {
import uiui.tick(); if (this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
) {
this.Demo.duckHoldTicksLeft: number
Ticks remaining before a ducked music bus starts recovering.
duckHoldTicksLeft
-= 1;
if (this.Demo.duckHoldTicksLeft: number
Ticks remaining before a ducked music bus starts recovering.
duckHoldTicksLeft
<= 0) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.
audioVolumeSet: (bus: AudioBus, value: number, options?: {
    fadeMs?: number;
    easing?: EasingFunction;
}) => void
Sets the logical volume for an audio bus, optionally fading to it.
@since1.3.0@parambus - Audio bus to update (`'main'`, `'music'`, or `'sfx'`).@paramvalue - Target volume, clamped to `[0, 1]`.@paramoptions - Optional fade behavior.@paramoptions.fadeMs - Fade duration in milliseconds. Omit for an immediate change.@paramoptions.easing - Easing curve for the fade. Defaults to `'linear'`; ignored when `fadeMs` is omitted.
audioVolumeSet
('music', this.Demo.preDuckMusicVolume: number
Music bus volume captured right before the most recent duck, so it can be restored.
preDuckMusicVolume
, { fadeMs?: number | undefinedfadeMs: const DUCK_RECOVER_FADE_MS: 600DUCK_RECOVER_FADE_MS });
this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
= false;
} } } /** * Clears the screen and declares the whole UI: a title bar, then one mixer panel with * a slider + mute checkbox per bus, the alert button, and a status line. * * With the immediate-mode kit there is no separate "handle input" step for the pointer: * ui.slider() and ui.checkbox() return the (possibly changed) value right away, and * ui.button() returns true on the frame it was clicked or tapped. */ Demo.render(): void
Clears the screen and declares the whole UI: a title bar, then one mixer panel with a slider + mute checkbox per bus, the alert button, and a status line. With the immediate-mode kit there is no separate "handle input" step for the pointer: ui.slider() and ui.checkbox() return the (possibly changed) value right away, and ui.button() returns true on the frame it was clicked or tapped.
render
() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(this.Demo.theme: null
Palette slots of the shared UI theme colors, filled by applyTheme() in init().
theme
.bg);
// The full-width title strip along the top edge. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_BAR); import uiui.panel('Audio Buses - drag a slider, toggle a Mute, try the Alert'); import uiui.end(); // The mixer panel, pinned just below the title strip. Width and height size // themselves to the widest row and the number of rows - no layout math here. import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { y: numbery: 30 }); import uiui.panel('Mixer'); for (const const row: anyrow of const BUSES: {}BUSES) { this.
Demo.renderBusRow(row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}): void
One bus's controls: a volume slider and a mute checkbox. The engine itself is the single source of truth here - every frame we read the current volume and mute state back from the audio system, show them, and only write a new value when the widget reports a change. That way the UI can never drift out of sync with what the engine is actually doing (for example during the alert duck).
@paramrow
renderBusRow
(const row: anyrow);
} import uiui.separator(); // The button reports a click, a tap, or its Space shortcut - all three the same way. if (import uiui.button('Alert (Space)', { key: stringkey: 'Space' })) { this.Demo.triggerAlert(): void
Plays the alert sound and ducks the music bus.
triggerAlert
();
} // The shared "click to enable sound" row - it draws itself only while sound is // still locked, and disappears on its own after the first click or key press. import uiui.audioUnlockHint(); // Once sound works, report whether the music bus is currently ducked instead. if (
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isAudioUnlocked: boolean
Whether the audio context has been unlocked by a user gesture. Browsers require a user gesture (pointer, key, or touch press) before allowing audio playback. Starts `false`; flips to `true` for the rest of the session after the first gesture successfully resumes the audio context.
@since1.3.0@returns`true` once unlocked; `false` when locked or before initialization.
isAudioUnlocked
) {
import uiui.label(this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
? 'Music ducked while the alert plays...' : 'Music at full volume.', {
color: stringcolor: 'dim', }); } import uiui.end(); } /** * One bus's controls: a volume slider and a mute checkbox. * * The engine itself is the single source of truth here - every frame we read the * current volume and mute state back from the audio system, show them, and only write * a new value when the widget reports a change. That way the UI can never drift out of * sync with what the engine is actually doing (for example during the alert duck). * * @param {{ bus: string, label: string, muteKey: string, muteHint: string }} row */
Demo.renderBusRow(row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}): void
One bus's controls: a volume slider and a mute checkbox. The engine itself is the single source of truth here - every frame we read the current volume and mute state back from the audio system, show them, and only write a new value when the widget reports a change. That way the UI can never drift out of sync with what the engine is actually doing (for example during the alert duck).
@paramrow
renderBusRow
(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
) {
const const volume: numbervolume =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.audioVolumeGet: (bus: AudioBus) => number
Gets the logical (pre-mute) volume for an audio bus. Unaffected by {@link BT.audioMuteSet } - muting never overwrites the configured level.
@since1.3.0@parambus - Audio bus to query.@returnsVolume in `[0, 1]`, or `0` before initialization.
audioVolumeGet
(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.bus: stringbus);
const const nextVolume: anynextVolume = import uiui.slider(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.label: stringlabel, const volume: numbervolume);
if (const nextVolume: anynextVolume !== const volume: numbervolume) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.
audioVolumeSet: (bus: AudioBus, value: number, options?: {
    fadeMs?: number;
    easing?: EasingFunction;
}) => void
Sets the logical volume for an audio bus, optionally fading to it.
@since1.3.0@parambus - Audio bus to update (`'main'`, `'music'`, or `'sfx'`).@paramvalue - Target volume, clamped to `[0, 1]`.@paramoptions - Optional fade behavior.@paramoptions.fadeMs - Fade duration in milliseconds. Omit for an immediate change.@paramoptions.easing - Easing curve for the fade. Defaults to `'linear'`; ignored when `fadeMs` is omitted.
audioVolumeSet
(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.bus: stringbus, const nextVolume: anynextVolume, { fadeMs?: number | undefinedfadeMs: 0 });
// While music is ducked, remember the user's latest Music slider value so // update() restores that level (not the pre-duck capture) when the hold ends. if (
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.bus: stringbus === 'music' && this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
) {
this.Demo.preDuckMusicVolume: number
Music bus volume captured right before the most recent duck, so it can be restored.
preDuckMusicVolume
= const nextVolume: anynextVolume;
} } const const muted: booleanmuted =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.isAudioMuted: (bus: AudioBus) => boolean
Reports whether an audio bus is currently muted.
@since1.3.0@parambus - Audio bus to query.@returns`true` when muted; `false` when unmuted or before initialization.
isAudioMuted
(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.bus: stringbus);
const const nextMuted: anynextMuted = import uiui.checkbox(`Mute (${
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.muteHint: stringmuteHint})`, const muted: booleanmuted, { key: stringkey:
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.muteKey: stringmuteKey });
if (const nextMuted: anynextMuted !== const muted: booleanmuted) {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.audioMuteSet: (bus: AudioBus, muted: boolean) => void
Mutes or unmutes an audio bus.
@since1.3.0@parambus - Audio bus to mute or unmute.@parammuted - `true` to mute, `false` to unmute.
audioMuteSet
(
row: {
    bus: string;
    label: string;
    muteKey: string;
    muteHint: string;
}
@paramrow
row
.bus: stringbus, const nextMuted: anynextMuted);
} import uiui.spacer(4); } /** * Plays the alert sound and ducks the music bus. */ Demo.triggerAlert(): void
Plays the alert sound and ducks the music bus.
triggerAlert
() {
// Ignore presses while a duck is already in progress. Without this guard, a rapid // re-press would capture the already-ducked volume as the new restore target, so each // re-press would multiply the eventual "restored" volume by DUCK_VOLUME_FACTOR again. if (this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
) {
return; } this.Demo.preDuckMusicVolume: number
Music bus volume captured right before the most recent duck, so it can be restored.
preDuckMusicVolume
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.audioVolumeGet: (bus: AudioBus) => number
Gets the logical (pre-mute) volume for an audio bus. Unaffected by {@link BT.audioMuteSet } - muting never overwrites the configured level.
@since1.3.0@parambus - Audio bus to query.@returnsVolume in `[0, 1]`, or `0` before initialization.
audioVolumeGet
('music');
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.
audioVolumeSet: (bus: AudioBus, value: number, options?: {
    fadeMs?: number;
    easing?: EasingFunction;
}) => void
Sets the logical volume for an audio bus, optionally fading to it.
@since1.3.0@parambus - Audio bus to update (`'main'`, `'music'`, or `'sfx'`).@paramvalue - Target volume, clamped to `[0, 1]`.@paramoptions - Optional fade behavior.@paramoptions.fadeMs - Fade duration in milliseconds. Omit for an immediate change.@paramoptions.easing - Easing curve for the fade. Defaults to `'linear'`; ignored when `fadeMs` is omitted.
audioVolumeSet
('music', this.Demo.preDuckMusicVolume: number
Music bus volume captured right before the most recent duck, so it can be restored.
preDuckMusicVolume
* const DUCK_VOLUME_FACTOR: 0.25DUCK_VOLUME_FACTOR, { fadeMs?: number | undefinedfadeMs: const DUCK_FADE_MS: 150DUCK_FADE_MS });
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 106 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRef
Plays a loaded audio clip through the SFX voice pool. Returns an inert {@link SoundRef } without allocating a voice when the clip hasn't finished loading yet (or was already unloaded), when the pool has no free or stealable voice at this priority, or before the engine has unlocked audio playback.
@since1.3.0@paramclip - Loaded audio clip to play.@paramoptions - Playback options.@returnsA handle identifying the new voice; pass it to {@link BT.soundStop} and the other per-sound controls. Safe to use even when playback was silently dropped - every accessor on an inert handle is a no-op.
soundPlay
(this.Demo.alertClip: AudioClip | null
@type{AudioClip | null}
alertClip
);
this.Demo.isDucking: boolean
Whether the music bus is currently ducked because of a recent alert.
isDucking
= true;
this.Demo.duckHoldTicksLeft: number
Ticks remaining before a ducked music bus starts recovering.
duckHoldTicksLeft
= const DUCK_HOLD_TICKS: 90DUCK_HOLD_TICKS;
} } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class Demo
Three volume sliders, three mute toggles, and a ducking alert button.
@implementsIBTDemo
Demo
);