/**
* 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 AudioClipDecoded 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.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.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 Vector2iInteger 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.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 DemoThree volume sliders, three mute toggles, and a ducking alert button.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
/** Palette slots of the shared UI theme colors, filled by applyTheme() in init(). */
Demo.theme: nullPalette slots of the shared UI theme colors, filled by applyTheme() in init().theme = null;
/** @type {AudioClip | null} */
Demo.musicClip: AudioClip | nullmusicClip = null;
/** @type {AudioClip | null} */
Demo.alertClip: AudioClip | nullalertClip = null;
/** Whether the music bus is currently ducked because of a recent alert. */
Demo.isDucking: booleanWhether the music bus is currently ducked because of a recent alert.isDucking = false;
/** Ticks remaining before a ducked music bus starts recovering. */
Demo.duckHoldTicksLeft: numberTicks 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: numberMusic 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.configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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.init() {
this.Demo.musicClip: AudioClip | nullmusicClip = await class AudioClipDecoded 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.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.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 | nullalertClip = await class AudioClipDecoded 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.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.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.synthPreset.hit: (seed?: number) => SynthParamsHit / 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.hit());
this.Demo.palette: Palette | nullpalette = 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) => PaletteCreates a standalone palette instance.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: nullPalette slots of the shared UI theme colors, filled by applyTheme() in init().theme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
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) => voidStores 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.paletteSet(this.Demo.palette: Palettepalette);
// 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) => voidPlays 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
}
.musicPlay(this.Demo.musicClip: AudioClip | nullmusicClip, { MusicPlayOptions.loop?: boolean | undefinedWhether 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(): voidAdvances 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: booleanWhether the music bus is currently ducked because of a recent alert.isDucking) {
this.Demo.duckHoldTicksLeft: numberTicks remaining before a ducked music bus starts recovering.duckHoldTicksLeft -= 1;
if (this.Demo.duckHoldTicksLeft: numberTicks 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.audioVolumeSet('music', this.Demo.preDuckMusicVolume: numberMusic 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: booleanWhether 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(): voidClears 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) => voidSets 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.clear(this.Demo.theme: nullPalette 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).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(): voidPlays 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: booleanWhether 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.isAudioUnlocked) {
import uiui.label(this.Demo.isDucking: booleanWhether 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).renderBusRow(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
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) => numberGets the logical (pre-mute) volume for an audio bus.
Unaffected by
{@link
BT.audioMuteSet
}
- muting never overwrites the configured level.audioVolumeGet(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
row.bus: stringbus);
const const nextVolume: anynextVolume = import uiui.slider(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
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.audioVolumeSet(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
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;
}
row.bus: stringbus === 'music' && this.Demo.isDucking: booleanWhether the music bus is currently ducked because of a recent alert.isDucking) {
this.Demo.preDuckMusicVolume: numberMusic 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) => booleanReports whether an audio bus is currently muted.isAudioMuted(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
row.bus: stringbus);
const const nextMuted: anynextMuted = import uiui.checkbox(`Mute (${row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
row.muteHint: stringmuteHint})`, const muted: booleanmuted, { key: stringkey: row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
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) => voidMutes or unmutes an audio bus.audioMuteSet(row: {
bus: string;
label: string;
muteKey: string;
muteHint: string;
}
row.bus: stringbus, const nextMuted: anynextMuted);
}
import uiui.spacer(4);
}
/**
* Plays the alert sound and ducks the music bus.
*/
Demo.triggerAlert(): voidPlays 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: booleanWhether the music bus is currently ducked because of a recent alert.isDucking) {
return;
}
this.Demo.preDuckMusicVolume: numberMusic 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) => numberGets the logical (pre-mute) volume for an audio bus.
Unaffected by
{@link
BT.audioMuteSet
}
- muting never overwrites the configured level.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.audioVolumeSet('music', this.Demo.preDuckMusicVolume: numberMusic 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) => SoundRefPlays 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.soundPlay(this.Demo.alertClip: AudioClip | nullalertClip);
this.Demo.isDucking: booleanWhether the music bus is currently ducked because of a recent alert.isDucking = true;
this.Demo.duckHoldTicksLeft: numberTicks 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.bootstrap(class DemoThree volume sliders, three mute toggles, and a ducking alert button.Demo);