Files
skills/docs/media/audio/audio-engine-browser/src/fallback/html5-fallback.md
2026-05-29 08:20:56 +01:00

4.2 KiB

Html5AudioFallback

(no submodules)

Purpose

Html5AudioFallback is the graceful-degradation path for browsers and runtimes without the Web Audio API. It implements the same TimelineAdapter interface as BrowserTimelineAdapter so consumers swap engines without conditional branching, but every operation that the underlying HTMLAudioElement cannot honour — per-track volume / pan / solo / mute, setTempo, FX hosting, sample-accurate scheduling — is a defined no-op. Unsupported per-track operations on unknown track ids still raise AUDIO_ENGINE_BROWSER.TRACK_NOT_FOUND so caller bugs are detected.

The fallback never constructs an AudioContext. It is feature-bounded by design — <audio> plays a single source at a time, so the adapter honours the first track's first clip and ignores the rest. Consumers that need polyphony or per-track FX use BrowserTimelineAdapter.

Features

  • Implements TimelineAdapter (every member callable).
  • Real transport (play / pause / stop / seek / getCurrentTime) through an injectable HTMLAudioElement-shaped value.
  • setMasterVolume clamped to [0, 1].
  • setLoop honours the enabled flag by toggling the element's loop property and records the window for getLoopWindow() debugging.
  • Per-track ops validate the track id and then no-op.
  • dispose() is idempotent.
  • Methods after dispose() raise AUDIO_ENGINE_BROWSER.ALREADY_DISPOSED.

Usage: Basic Example

import { Html5AudioFallback } from "@suaveplan/audio-engine-browser/fallback/html5-fallback";

const fallback = new Html5AudioFallback();
fallback.init();
fallback.setTracks([{ id: "t1", name: "lead", clips: [{ id: "c1", documentId: "/song.mp3", startSample: 0, offsetSamples: 0, durationSamples: 0 }], volume: 1, muted: false, soloed: false, pan: 0 }]);
fallback.play();

Usage: Advanced Example

import { Html5AudioFallback } from "@suaveplan/audio-engine-browser/fallback/html5-fallback";

// Inject a custom element (e.g. a test double or a pre-buffered element)
const fallback = new Html5AudioFallback({
    element: customAudioElement,
    volume: 0.8,
    loop: true,
});

fallback.init({ signal: controller.signal });
fallback.setMasterVolume(0.5);
fallback.setLoop(0, 8, true);

fallback.play(2);     // start at 2s
console.log(fallback.getCurrentTime());

fallback.dispose();   // idempotent — subsequent ops throw ALREADY_DISPOSED

API Reference

new Html5AudioFallback(options?)

  • options.element — pre-constructed HTMLAudioElement (default new Audio()).
  • options.volume — initial master volume.
  • options.loop — initial loop flag.
  • options.lookAhead / options.ppqn — readonly properties surfaced for TimelineAdapter parity.

init(options?) → void

Synchronous in this implementation. Honours a pre-aborted AbortSignal by throwing AUDIO_ENGINE_BROWSER.CONTEXT_FAILED.

setTracks(tracks), addTrack(track), removeTrack(trackId), updateTrack(trackId, track)

Maintain the track table; the first track's first clip becomes the element's src.

play(time?), pause(time?), stop(time?), seek(time), getCurrentTime()

Drive the underlying element.

setLoop(start, end, enabled), setMasterVolume(volume)

Honoured operations.

setTrackVolume(trackId, _), setTrackMute, setTrackSolo, setTrackPan, setTempo

Defined no-ops with track-id validation.

dispose() → void

Idempotent. Pauses the element and marks the fallback disposed.

getLoopWindow() → { start, end, enabled }

Returns the configured loop window for tests / debug.

Implementation Notes

  • The fallback chooses the first clip of the first track as src. A full multi-clip mix is out of scope — <audio> is single-source.
  • setMasterVolume clamps to [0, 1]; some platforms throw on out-of-range values, so the clamp is the caller-friendly behaviour.
  • The Promise returned by HTMLAudioElement.play() rejects on autoplay-policy violations; the rejection is swallowed because the spec contract is synchronous and reporting a rejection here would surface a different shape per platform.
  • dispose() swallows pause errors because some embedded <audio> shims throw after detachment.