// @pageTitle BLIT386 Demo – Music Playback
// @description Crossfade two looping tracks with different fade profiles, plus a third with a seamless loop point.

/**
 * Music Demo - crossfading between two tracks and playing one with a seamless loop point.
 *
 * Part of the BLIT386 demo series.
 * Prerequisites:
 *   Audio Basics  https://demos.blit386.dev/audio-basics
 *
 * Live version: https://demos.blit386.dev/music
 *
 * Sound effects (audio-basics, synth-toy) are short one-shot clips: press a key, hear a blip, done. Music
 * is different - it is meant to loop forever in the background, and switching from one
 * track to another should not just cut off with a click. BT.musicPlay() handles both of
 * those jobs for you.
 *
 * This page has three buttons, each backed by a different AudioClip:
 * - Track A and Track B swap between two looping tunes. Each swap uses a different
 *   "crossfade" - a fade-out of the old track happening alongside (or after) a fade-in of
 *   the new one, so the music blends instead of jumping. Switching to Track A lets the old
 *   track fade all the way out first, waits a moment, then fades Track A in. Switching to
 *   Track B overlaps the two fades so they happen at the same time. Listen for the
 *   difference - one has a small silent gap in the middle, the other does not.
 * - Loop Demo plays a track that starts with a short intro passage, then loops forever from
 *   a chosen point onward - the intro only ever plays once, exactly like the opening jingle
 *   of a real game level that then settles into its main tune.
 *
 * The title strip, track buttons, and status readout all come from the shared UI kit in
 * src/shared/ui.js - the same look every demo in this series uses, and it is fully
 * touch-friendly: tap a button with a finger, click it with a mouse, or press its number
 * key, and the kit reports all three the same way.
 *
 * 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; // These two numbers come straight out of public/audio/music-intro-loop.loop.json, generated // by scripts/generate-audio-loops.mjs. They mark where the short intro ends and the // repeating loop section begins/ends inside that one audio file. const const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS = 1.5; const const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS = 7.9; // Fading to Track A: the old track fades all the way out first, then - after a short silent // gap - Track A fades in. `overlap: -1` is what creates that gap. const
const PROFILE_TO_A: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_A
= { fadeMs: numberfadeMs: 800, overlap: numberoverlap: -1, easeIn: stringeaseIn: 'linear', easeOut: stringeaseOut: 'linear' };
// Fading to Track B: the new track fades in at the same time as the old one fades out // (`overlap: 1`), and both fades use an "ease-in-out" curve so the volume change starts and // ends gently instead of at a constant speed the whole way through. const
const PROFILE_TO_B: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_B
= { fadeMs: numberfadeMs: 1200, overlap: numberoverlap: 1, easeIn: stringeaseIn: 'ease-in-out', easeOut: stringeaseOut: 'ease-in-out' };
// Fading into the loop-point track: a plain, fairly quick overlapping crossfade. const
const PROFILE_TO_LOOP: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_LOOP
= { fadeMs: numberfadeMs: 600, overlap: numberoverlap: 1, easeIn: stringeaseIn: 'linear', easeOut: stringeaseOut: 'linear' };
// One entry per track button: which track it plays, its on-screen name, and the keyboard // shortcut the kit binds to the button (`keyCode` is the raw key name, `keyHint` is the // friendly character shown in the button label). const const TRACKS: {}TRACKS = [ { trackId: stringtrackId: 'A', label: stringlabel: 'Track A - calm', keyCode: stringkeyCode: 'Digit1', keyHint: stringkeyHint: '1' }, { trackId: stringtrackId: 'B', label: stringlabel: 'Track B - upbeat', keyCode: stringkeyCode: 'Digit2', keyHint: stringkeyHint: '2' }, { trackId: stringtrackId: 'loop', label: stringlabel: 'Loop Demo - intro + loop', keyCode: stringkeyCode: 'Digit3', keyHint: stringkeyHint: '3' }, ]; /** * Three buttons, each starting a different music track with a different crossfade profile. * * @implements {IBTDemo} */ class class Demo
Three buttons, each starting a different music track with a different crossfade profile.
@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.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
= null;
/** @type {AudioClip | null} */ Demo.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
= null;
/** @type {AudioClip | null} */ Demo.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
= null;
/** @type {string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play). */ Demo.activeTrackId: string | null
@type{string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play).
activeTrackId
= null;
/** @type {string} Human-readable description of the crossfade profile last used, shown on screen. */ Demo.activeProfileLabel: string
@type{string} Human-readable description of the crossfade profile last used, shown on screen.
activeProfileLabel
= '-';
/** * 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, }; } /** * Loads all three music clips, sets up the shared UI theme, and starts Track A playing. * * @returns {Promise<boolean>} */ async Demo.init(): Promise<boolean>
Loads all three music clips, sets up the shared UI theme, and starts Track A playing.
@returns
init
() {
// Load all three tracks at once - Promise.all waits for the slowest fetch, not // the sum of three sequential downloads. [this.Demo.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
, this.Demo.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
, this.Demo.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
] = await Promise.all([
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'),
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-upbeat.wav'),
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-intro-loop.wav'),
]); 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
);
// Start with Track A playing so there is always music, even before you press // anything. BT.musicPlay() called before the page is unlocked is "remembered" and // starts for real the instant you click or press a key - unlike BT.soundPlay(), // which drops sounds played too early. this.Demo.playTrack(trackId: string): void
Starts the given track with its matching crossfade profile, unless it is already playing (pressing the same button twice does nothing new).
@paramtrackId - 'A', 'B', or 'loop'.
playTrack
('A');
return true; } /** * Once-per-tick housekeeping for the UI kit. * * ui.tick() is what safely catches the number-key shortcuts bound to the buttons in * render(). Keyboard presses can only be read reliably here in update(), never in * render() - the engine clears "was this just pressed?" flags once per tick, and that * tick always finishes before this frame's render() runs (keyboard-input explains * this in more detail). The kit latches the presses now so the buttons can answer * later, during render(). */ Demo.update(): void
Once-per-tick housekeeping for the UI kit. ui.tick() is what safely catches the number-key shortcuts bound to the buttons in render(). Keyboard presses can only be read reliably here in update(), never in render() - the engine clears "was this just pressed?" flags once per tick, and that tick always finishes before this frame's render() runs (keyboard-input explains this in more detail). The kit latches the presses now so the buttons can answer later, during render().
update
() {
import uiui.tick(); } /** * Clears the screen and declares the whole UI: a title strip, one button per track, * and the status readout. * * With the immediate-mode kit there is no separate "handle input" step: ui.button() * returns true on the frame it was clicked, tapped, or its number key was pressed, so * the track switch happens right where the button is declared. */ Demo.render(): void
Clears the screen and declares the whole UI: a title strip, one button per track, and the status readout. With the immediate-mode kit there is no separate "handle input" step: ui.button() returns true on the frame it was clicked, tapped, or its number key was pressed, so the track switch happens right where the button is declared.
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('Music Playback - Crossfade and Loop Points'); import uiui.end(); // The track 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('Tracks'); // One button per track. Each label ends with its keyboard hint, and the kit binds // the matching key so pressing it acts exactly like a click or a tap. for (const const track: anytrack of const TRACKS: {}TRACKS) { if (import uiui.button(`${const track: anytrack.label} (${const track: anytrack.keyHint})`, { key: anykey: const track: anytrack.keyCode })) { this.Demo.playTrack(trackId: string): void
Starts the given track with its matching crossfade profile, unless it is already playing (pressing the same button twice does nothing new).
@paramtrackId - 'A', 'B', or 'loop'.
playTrack
(const track: anytrack.trackId);
} } import uiui.separator(); this.Demo.renderStatus(): void
Status rows inside the track panel: the unlock prompt (until sound is unlocked), then the currently playing track, its crossfade profile, and - only for the loop track - the loop boundaries in seconds.
renderStatus
();
import uiui.end(); } /** * Status rows inside the track panel: the unlock prompt (until sound is unlocked), * then the currently playing track, its crossfade profile, and - only for the loop * track - the loop boundaries in seconds. */ Demo.renderStatus(): void
Status rows inside the track panel: the unlock prompt (until sound is unlocked), then the currently playing track, its crossfade profile, and - only for the loop track - the loop boundaries in seconds.
renderStatus
() {
// 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(); // Until sound is unlocked there is nothing worth reporting yet, so the readout // rows below wait for that first click or key press too. 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
) {
return; } // Look up the friendly name of whichever track is active. Array.find() walks the // list and returns the first entry the test function says yes to (or undefined if // none matches - which here only happens before the first play). const const active: anyactive = const TRACKS: {}TRACKS.find((track: anytrack) => track: anytrack.trackId === this.Demo.activeTrackId: string | null
@type{string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play).
activeTrackId
);
import uiui.kv('Playing', const active: anyactive ? const active: anyactive.label : '-'); import uiui.kv('Fade', this.Demo.activeProfileLabel: string
@type{string} Human-readable description of the crossfade profile last used, shown on screen.
activeProfileLabel
);
if (this.Demo.activeTrackId: string | null
@type{string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play).
activeTrackId
=== 'loop') {
import uiui.kv('Loop', `${const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS}s - ${const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS}s (intro once)`); } } /** * Starts the given track with its matching crossfade profile, unless it is already * playing (pressing the same button twice does nothing new). * * @param {string} trackId - 'A', 'B', or 'loop'. */ Demo.playTrack(trackId: string): void
Starts the given track with its matching crossfade profile, unless it is already playing (pressing the same button twice does nothing new).
@paramtrackId - 'A', 'B', or 'loop'.
playTrack
(trackId: string
- 'A', 'B', or 'loop'.
@paramtrackId - 'A', 'B', or 'loop'.
trackId
) {
if (trackId: string
- 'A', 'B', or 'loop'.
@paramtrackId - 'A', 'B', or 'loop'.
trackId
=== this.Demo.activeTrackId: string | null
@type{string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play).
activeTrackId
) {
return; } if (trackId: string
- 'A', 'B', or 'loop'.
@paramtrackId - 'A', 'B', or 'loop'.
trackId
=== 'A') {
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.calmClip: AudioClip | null
@type{AudioClip | null}
calmClip
, { MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1, MusicPlayOptions.loop?: boolean | undefined
Whether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.
loop
: true, ...
const PROFILE_TO_A: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_A
});
this.Demo.activeProfileLabel: string
@type{string} Human-readable description of the crossfade profile last used, shown on screen.
activeProfileLabel
= 'out, gap, in (800ms)';
} else if (trackId: string
- 'A', 'B', or 'loop'.
@paramtrackId - 'A', 'B', or 'loop'.
trackId
=== 'B') {
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.upbeatClip: AudioClip | null
@type{AudioClip | null}
upbeatClip
, { MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1, MusicPlayOptions.loop?: boolean | undefined
Whether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.
loop
: true, ...
const PROFILE_TO_B: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_B
});
this.Demo.activeProfileLabel: string
@type{string} Human-readable description of the crossfade profile last used, shown on screen.
activeProfileLabel
= 'out + in together (1200ms)';
} else {
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.introLoopClip: AudioClip | null
@type{AudioClip | null}
introLoopClip
, {
MusicPlayOptions.volume?: number | undefined
Target gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.
volume
: 1,
MusicPlayOptions.loopStart?: number | undefined
Loop region start in seconds. Requires `loopEnd`; see {@link MusicPlayer.play } .
loopStart
: const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS,
MusicPlayOptions.loopEnd?: number | undefined
Loop region end in seconds. Requires `loopStart`; see {@link MusicPlayer.play } .
loopEnd
: const INTRO_LOOP_END_SECONDS: 7.9INTRO_LOOP_END_SECONDS,
...
const PROFILE_TO_LOOP: {
    fadeMs: number;
    overlap: number;
    easeIn: string;
    easeOut: string;
}
PROFILE_TO_LOOP
,
}); this.Demo.activeProfileLabel: string
@type{string} Human-readable description of the crossfade profile last used, shown on screen.
activeProfileLabel
= 'quick overlap (600ms)';
} this.Demo.activeTrackId: string | null
@type{string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play).
activeTrackId
= trackId: string
- 'A', 'B', or 'loop'.
@paramtrackId - 'A', 'B', or 'loop'.
trackId
;
} } 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 buttons, each starting a different music track with a different crossfade profile.
@implementsIBTDemo
Demo
);