// @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 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;
// 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 DemoThree buttons, each starting a different music track with a different crossfade profile.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.calmClip: AudioClip | nullcalmClip = null;
/** @type {AudioClip | null} */
Demo.upbeatClip: AudioClip | nullupbeatClip = null;
/** @type {AudioClip | null} */
Demo.introLoopClip: AudioClip | nullintroLoopClip = null;
/** @type {string | null} Which button's track is currently playing ('A', 'B', 'loop', or null before the first play). */
Demo.activeTrackId: string | nullactiveTrackId = null;
/** @type {string} Human-readable description of the crossfade profile last used, shown on screen. */
Demo.activeProfileLabel: stringactiveProfileLabel = '-';
/**
* 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,
};
}
/**
* 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.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 | nullcalmClip, this.Demo.upbeatClip: AudioClip | nullupbeatClip, this.Demo.introLoopClip: AudioClip | nullintroLoopClip] = await Promise.all([
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'),
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-upbeat.wav'),
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-intro-loop.wav'),
]);
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);
// 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): voidStarts the given track with its matching crossfade profile, unless it is already
playing (pressing the same button twice does nothing new).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(): voidOnce-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(): voidClears 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) => 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('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): voidStarts the given track with its matching crossfade profile, unless it is already
playing (pressing the same button twice does nothing new).playTrack(const track: anytrack.trackId);
}
}
import uiui.separator();
this.Demo.renderStatus(): voidStatus 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(): voidStatus 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: 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) {
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 | nullactiveTrackId);
import uiui.kv('Playing', const active: anyactive ? const active: anyactive.label : '-');
import uiui.kv('Fade', this.Demo.activeProfileLabel: stringactiveProfileLabel);
if (this.Demo.activeTrackId: string | nullactiveTrackId === '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): voidStarts the given track with its matching crossfade profile, unless it is already
playing (pressing the same button twice does nothing new).playTrack(trackId: string- 'A', 'B', or 'loop'.trackId) {
if (trackId: string- 'A', 'B', or 'loop'.trackId === this.Demo.activeTrackId: string | nullactiveTrackId) {
return;
}
if (trackId: string- '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) => 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.calmClip: AudioClip | nullcalmClip, { MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1, MusicPlayOptions.loop?: boolean | undefinedWhether 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: stringactiveProfileLabel = 'out, gap, in (800ms)';
} else if (trackId: string- '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) => 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.upbeatClip: AudioClip | nullupbeatClip, { MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1, MusicPlayOptions.loop?: boolean | undefinedWhether 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: stringactiveProfileLabel = '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) => 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.introLoopClip: AudioClip | nullintroLoopClip, {
MusicPlayOptions.volume?: number | undefinedTarget gain for the incoming track in `[0, 1]` (unclamped). Defaults to `1`.volume: 1,
MusicPlayOptions.loopStart?: number | undefinedLoop region start in seconds. Requires `loopEnd`; see
{@link
MusicPlayer.play
}
.loopStart: const INTRO_LOOP_START_SECONDS: 1.5INTRO_LOOP_START_SECONDS,
MusicPlayOptions.loopEnd?: number | undefinedLoop 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: stringactiveProfileLabel = 'quick overlap (600ms)';
}
this.Demo.activeTrackId: string | nullactiveTrackId = trackId: string- '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.bootstrap(class DemoThree buttons, each starting a different music track with a different crossfade profile.Demo);