/*
Audio Buses: 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.
@ogScale integer
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
Guides:
- Audio: https://blit386.dev/docs/guides/audio#playing-sfx
Reference:
- Audio: https://blit386.dev/docs/api/audio#volume
- Overlay: https://blit386.dev/docs/api/overlay#audio-meters-optional
---
WHAT YOU WILL SEE
-----------------
A 320x260 screen in the UI theme's deep navy. The engine overlay is open from the start:
white bars along the top with live audio level meters, and a hint bar along the bottom.
Between them, one centered line of text reads "Click or press a key to enable sound" in
warm orange until you click or press a key; after that it turns into a dim reminder:
"Drag a slider, toggle a Mute, try the Alert."
Under that line sit three bus panels side by side - "Main", "Music", and "Sfx". Each says
what the bus carries here ("All sound", "The loop", "The alert"), then has a "Volume"
slider and a "Mute (Q)", "Mute (W)", or "Mute (E)" checkbox.
Below them, the "Alert" panel has one button, and the wider "Ducking" panel shows whether
the music is ducked ("not ducked" or "ducked to 25%"), the volume it will come back to,
and an amber "Hold" bar that drains while the duck lasts.
A calm music loop starts on its own the moment sound is unlocked.
WHAT YOU WILL LEARN
-------------------
- Every sound flows through one of three buses before it reaches your speakers: 'main',
'music', and 'sfx'. A bus is like a volume knob for a whole category of sound.
- BT.audioVolumeSet(bus, volume, { fadeMs }) changes a bus's volume, instantly or with a
fade, and BT.audioVolumeGet(bus) reads it back.
- BT.audioMuteSet(bus, muted) silences a bus without touching its volume, and
BT.isAudioMuted(bus) reads the mute state back.
- Ducking: dip the music bus while a sound effect plays, hold, then fade it back up.
- Reading engine state back every frame (instead of keeping a copy) so the sliders and
checkboxes can never drift out of sync with what the engine is doing.
- AudioClip.synth(BT.synthPreset.hit()) makes a sound effect from a recipe, with no file.
- isOverlayAudioMetersEnabled: true adds live bus level bars to the engine overlay, and
overlayStyle / overlayAudioMeterStyle pick its colors from the palette.
- isCapturingKeyboardScroll: true keeps Space from scrolling the page.
HOW DUCKING WORKS
-----------------
Movies and games lower the soundtrack for a moment so an important sound is never buried
under the music. The Alert button does the same thing in four steps:
1. Remember the music bus's current volume.
2. Fade the music bus down to a quarter of that (DUCK_VOLUME_FACTOR 0.25) over 150 ms
and play the alert sound through the sfx bus.
3. Hold for 90 ticks - a tick is one fixed update, so at 60 ticks per second that is
1.5 seconds. update() counts the ticks down.
4. Fade the music bus back to the remembered volume over 600 ms.
While the music is ducked, pressing Alert again does nothing, and dragging the Music
slider replaces the remembered volume, so the music comes back to where you left the
slider.
TRY THIS
--------
- Click anywhere, or press any key, to unlock sound. The calm music loop starts by itself.
- Drag a slider with the mouse or a finger. Main changes everything at once; Music changes
only the loop; Sfx changes only the alert sound.
- Tap a Mute checkbox, or press Q (Main), W (Music), or E (Sfx). The slider next to it
does not move - muting hides the volume, it does not erase it. Unmute and the level is
back.
- Click Alert, or press Space, and listen to the music dip under the alert and then swell
back. The Music slider drops to a quarter, the Ducking panel says "ducked to 25%", and
the Hold bar drains until the music starts coming back.
- Drag the Music slider while the music is ducked: the "Restore" value follows it, and the
music comes back to that level instead.
- Watch the overlay's main, music, and sfx level bars follow the sliders and the duck.
Press the backquote key (`) or click the small icon in the bottom-left corner to hide or
show the overlay.
*/
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;
... 124 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 FONT_WFONT_W, import THEME_OVERLAY_AUDIO_METER_STYLETHEME_OVERLAY_AUDIO_METER_STYLE, import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, import uiui } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
const const DISPLAY_W: 320DISPLAY_W = 320;
// Taller than the default 240 px, so the mixer and the ducking panels both fit between
// the overlay's bars.
const const DISPLAY_H: 260DISPLAY_H = 260;
// One entry per bus: which engine bus it controls, its panel title, what it carries in
// this demo, 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', carries: stringcarries: 'All sound', muteKey: stringmuteKey: 'KeyQ', muteHint: stringmuteHint: 'Q' },
{ bus: stringbus: 'music', label: stringlabel: 'Music', carries: stringcarries: 'The loop', muteKey: stringmuteKey: 'KeyW', muteHint: stringmuteHint: 'W' },
{ bus: stringbus: 'sfx', label: stringlabel: 'Sfx', carries: stringcarries: 'The alert', muteKey: stringmuteKey: 'KeyE', muteHint: stringmuteHint: 'E' },
];
// Screen layout, in display pixels. The engine overlay is open from the start: its bars
// cover the top 56 rows and its hint bar the bottom 14, so everything sits in the band
// between them. The status line comes first, then a row of three bus panels, then the
// alert and ducking panels lined up under them.
const const STATUS_Y: 64STATUS_Y = 64;
const const MARGIN_X: 4MARGIN_X = 4;
const const PANEL_GAP: 6PANEL_GAP = 6;
const const BUS_PANEL_W: 100BUS_PANEL_W = 100;
const const BUS_ROW_Y: 76BUS_ROW_Y = 76;
const const BUS_PANEL_H: 81BUS_PANEL_H = 81;
const const DUCK_ROW_Y: numberDUCK_ROW_Y = const BUS_ROW_Y: 76BUS_ROW_Y + const BUS_PANEL_H: 81BUS_PANEL_H + const PANEL_GAP: 6PANEL_GAP;
// The ducking panel spans the last two columns.
const const DUCK_PANEL_X: numberDUCK_PANEL_X = const MARGIN_X: 4MARGIN_X + const BUS_PANEL_W: 100BUS_PANEL_W + const PANEL_GAP: 6PANEL_GAP;
const const DUCK_PANEL_W: numberDUCK_PANEL_W = 2 * const BUS_PANEL_W: 100BUS_PANEL_W + const PANEL_GAP: 6PANEL_GAP;
// Widgets inside a panel are this wide: the panel minus the kit's 10-pixel text inset on
// each side.
const const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET = 10;
const const BUS_CONTENT_W: numberBUS_CONTENT_W = const BUS_PANEL_W: 100BUS_PANEL_W - 2 * const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
const const DUCK_CONTENT_W: numberDUCK_CONTENT_W = const DUCK_PANEL_W: numberDUCK_PANEL_W - 2 * const PANEL_TEXT_INSET: 10PANEL_TEXT_INSET;
// The ducking panel's value column starts after this many characters, so the longest key,
// "Restore", keeps a space after it.
const const DUCK_KV_COLS: 9DUCK_KV_COLS = 9;
// The alert panel's rows are shorter than the ducking panel's, so this much empty space
// at its bottom makes both panels end on the same line.
const const ALERT_PANEL_FILLER: 2ALERT_PANEL_FILLER = 2;
// Ducking behavior: how far the music bus dips, how quickly it dips, how long it stays
// dipped (in ticks - 90 ticks is 1.5 seconds at 60 ticks per second), and how slowly it
// recovers.
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.
class class DemoDemo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Palette slots of the shared UI theme colors, filled by applyTheme() in init().
Demo.theme: nulltheme = 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: booleanisDucking = false;
// Ticks remaining before a ducked music bus starts recovering.
Demo.duckHoldTicksLeft: numberduckHoldTicksLeft = 0;
// Music bus volume captured right before the most recent duck, so it can be restored.
Demo.preDuckMusicVolume: numberpreDuckMusicVolume = 1;
// Sets the logical display size, opens the overlay with its audio meters colored from
// the shared UI theme, and keeps the Space key from scrolling the page.
Demo.configure(): {
displaySize: Vector2i;
isOverlayVisibleAtStart: boolean;
isOverlayAudioMetersEnabled: boolean;
overlayStyle: any;
overlayAudioMeterStyle: any;
isCapturingKeyboardScroll: boolean;
}
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: 260DISPLAY_H),
isOverlayVisibleAtStart: booleanisOverlayVisibleAtStart: true,
// Live per-bus level bars and a voices/steal/drop readout in the overlay. Off
// by default.
isOverlayAudioMetersEnabled: booleanisOverlayAudioMetersEnabled: true,
// The overlay's default colors live in palette slots 1 and 2, which this demo
// never fills - they would draw solid black. These two shared styles point
// the bars, text, and meters at the theme's colors instead.
overlayStyle: anyoverlayStyle: import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE,
overlayAudioMeterStyle: anyoverlayAudioMeterStyle: import THEME_OVERLAY_AUDIO_METER_STYLETHEME_OVERLAY_AUDIO_METER_STYLE,
// Space is the Alert shortcut - keep the page from scrolling when it is
// pressed.
isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true,
};
}
// Loads the background music, builds the alert sound, sets up the shared UI theme,
// and starts the music loop.
async Demo.init(): unknowninit() {
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. AudioClip.synth()
// turns the recipe into a playable clip without downloading any file.
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;
... 124 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;
... 124 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: nulltheme = 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;
... 124 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;
... 124 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 in
// render(). Keyboard presses can only be read reliably here in update(), never in
// render() - keyboard-input explains why in detail.
Demo.update(): voidupdate() {
import uiui.tick();
// While ducked, count the hold down one tick at a time. When it reaches zero,
// fade the music bus back up to the remembered volume and end the duck.
if (this.Demo.isDucking: booleanisDucking) {
this.Demo.duckHoldTicksLeft: numberduckHoldTicksLeft -= 1;
if (this.Demo.duckHoldTicksLeft: numberduckHoldTicksLeft <= 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;
... 124 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: numberpreDuckMusicVolume, { fadeMs?: number | undefinedfadeMs: const DUCK_RECOVER_FADE_MS: 600DUCK_RECOVER_FADE_MS });
this.Demo.isDucking: booleanisDucking = false;
}
}
}
// Clears the screen and declares the whole UI: the status line, one panel per bus
// with a volume slider and a mute checkbox, then the alert button and the ducking
// readout.
//
// 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(): voidrender() {
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;
... 124 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: nulltheme.bg);
this.Demo.renderStatusLine(): voidrenderStatusLine();
// The alert panel goes first even though it sits lower on screen: its button may
// duck the music this very frame, and the Music slider drawn after it should
// already show the dip.
this.Demo.renderAlertPanel(): voidrenderAlertPanel();
const BUSES: {}BUSES.forEach((row: anyrow, column: anycolumn) => {
this.Demo.renderBusPanel(row: any, x: any): voidrenderBusPanel(row: anyrow, const MARGIN_X: 4MARGIN_X + column: anycolumn * (const BUS_PANEL_W: 100BUS_PANEL_W + const PANEL_GAP: 6PANEL_GAP));
});
this.Demo.renderDuckPanel(): voidrenderDuckPanel();
}
// One centered line just under the overlay bars: the shared unlock reminder in warm
// orange until sound is unlocked, then a dim reminder of what to try.
Demo.renderStatusLine(): voidrenderStatusLine() {
const const isLocked: booleanisLocked = !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;
... 124 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;
const const text: "Click or press a key to enable sound" | "Drag a slider, toggle a Mute, try the Alert."text = const isLocked: booleanisLocked ? 'Click or press a key to enable sound' : 'Drag a slider, toggle a Mute, try the Alert.';
// FONT_W is the width of one character; the last one's empty spacing column is
// left out, so the visible letters are what gets centered.
const const x: numberx = Math.floor(const DISPLAY_W: 320DISPLAY_W / 2) - Math.floor((const text: "Click or press a key to enable sound" | "Drag a slider, toggle a Mute, try the Alert."text.length * import FONT_WFONT_W - 1) / 2);
import uiui.caption(const x: numberx, const STATUS_Y: 64STATUS_Y, const text: "Click or press a key to enable sound" | "Drag a slider, toggle a Mute, try the Alert."text, { color: stringcolor: const isLocked: booleanisLocked ? 'warm' : 'dim' });
}
// One bus's panel at column `x`: what the bus carries, a volume slider, and a mute
// checkbox. `row` is a BUSES entry.
//
// 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).
Demo.renderBusPanel(row: any, x: any): voidrenderBusPanel(row: anyrow, x: anyx) {
import uiui.begin('topLeft', { x: anyx, y: numbery: const BUS_ROW_Y: 76BUS_ROW_Y, width: numberwidth: const BUS_PANEL_W: 100BUS_PANEL_W });
import uiui.panel(row: anyrow.label);
import uiui.label(row: anyrow.carries, { color: stringcolor: 'dim' });
import uiui.spacer(4);
// All three sliders are labeled "Volume", so each gets its bus name as its
// identity - the kit tells widgets apart by that, not by what they show.
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;
... 124 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: anyrow.bus);
const const nextVolume: anynextVolume = import uiui.slider('Volume', const volume: numbervolume, { id: anyid: row: anyrow.bus, width: numberwidth: const BUS_CONTENT_W: numberBUS_CONTENT_W });
// The slider returns the dragged value. A different number means the user moved
// it, so push the new volume to the engine right away, with no fade.
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;
... 124 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: anyrow.bus, 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: anyrow.bus === 'music' && this.Demo.isDucking: booleanisDucking) {
this.Demo.preDuckMusicVolume: numberpreDuckMusicVolume = 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;
... 124 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: anyrow.bus);
const const nextMuted: anynextMuted = import uiui.checkbox(`Mute (${row: anyrow.muteHint})`, const muted: booleanmuted, { key: anykey: row: anyrow.muteKey });
// Same idea for the checkbox: a changed value means a click, tap, or key press.
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;
... 124 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: anyrow.bus, const nextMuted: anynextMuted);
}
import uiui.end();
}
// The alert panel under the Main column: one button that plays the alert and ducks
// the music. It reports a click, a tap, or its Space shortcut - all three the same
// way.
Demo.renderAlertPanel(): voidrenderAlertPanel() {
import uiui.begin('topLeft', { x: numberx: const MARGIN_X: 4MARGIN_X, y: numbery: const DUCK_ROW_Y: numberDUCK_ROW_Y, width: numberwidth: const BUS_PANEL_W: 100BUS_PANEL_W });
import uiui.panel('Alert');
import uiui.label('Space or tap', { color: stringcolor: 'dim' });
import uiui.spacer(4);
if (import uiui.button('Alert', { key: stringkey: 'Space', width: numberwidth: const BUS_CONTENT_W: numberBUS_CONTENT_W })) {
this.Demo.triggerAlert(): voidtriggerAlert();
}
import uiui.spacer(const ALERT_PANEL_FILLER: 2ALERT_PANEL_FILLER);
import uiui.end();
}
// The ducking panel under the Music and Sfx columns: whether the music is ducked, the
// volume it will come back to, and how much of the hold is left.
Demo.renderDuckPanel(): voidrenderDuckPanel() {
const const holdLeft: numberholdLeft = this.Demo.isDucking: booleanisDucking ? this.Demo.duckHoldTicksLeft: numberduckHoldTicksLeft / const DUCK_HOLD_TICKS: 90DUCK_HOLD_TICKS : 0;
const const state: stringstate = this.Demo.isDucking: booleanisDucking ? `ducked to ${Math.round(const DUCK_VOLUME_FACTOR: 0.25DUCK_VOLUME_FACTOR * 100)}%` : 'not ducked';
import uiui.begin('topLeft', { x: numberx: const DUCK_PANEL_X: numberDUCK_PANEL_X, y: numbery: const DUCK_ROW_Y: numberDUCK_ROW_Y, width: numberwidth: const DUCK_PANEL_W: numberDUCK_PANEL_W, kvCols: numberkvCols: const DUCK_KV_COLS: 9DUCK_KV_COLS });
import uiui.panel('Ducking');
import uiui.kv('Music', const state: stringstate);
import uiui.kv('Restore', this.Demo.preDuckMusicVolume: numberpreDuckMusicVolume.toFixed(2));
import uiui.meter('Hold', const holdLeft: numberholdLeft, { color: stringcolor: 'header', width: numberwidth: const DUCK_CONTENT_W: numberDUCK_CONTENT_W });
import uiui.end();
}
// Plays the alert sound and ducks the music bus.
Demo.triggerAlert(): voidtriggerAlert() {
// 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: booleanisDucking) {
return;
}
// Remember where the music was, dip it to a quarter of that over 150 ms, and fire
// the alert sound through the sfx bus.
this.Demo.preDuckMusicVolume: numberpreDuckMusicVolume = 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;
... 124 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;
... 124 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: numberpreDuckMusicVolume * 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;
... 124 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: booleanisDucking = true;
this.Demo.duckHoldTicksLeft: numberduckHoldTicksLeft = 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 DemoDemo);