Home
Guides

Architecture

The package is four focused layers with no application singleton:

Responsibilities

The package is four focused layers with no application singleton:

  1. init.ts owns the gesture-gated start transaction. Tone unlock and caller bootstrap either both complete or the next gesture may retry.
  2. buses.ts owns continuous Tone signal flow. The first caller-provided bus is the master and routes through a -1 dBFS limiter to the destination.
  3. sprite-resolver.ts owns Howler sample sheets. It validates and flattens the map, creates one Howl per file, and tracks active sound IDs by target bus.
  4. preferences-bridge.ts translates persisted percentages and mute policy into the two runtime engines without owning persistence.

unlock-controller.ts holds the engine-agnostic half of init.ts (one shared in-flight attempt, retry, generation-based reset, gesture listeners). init.ts adds the Tone unlock step and is imported only by the root entry; howler-unlock.ts adds the Howler unlock step for the gesture-audio/howler entry, whose module graph must never reach tone.

The Node-only build-tools export is a separate entry point so filesystem and process modules cannot enter a browser bundle.

Invariants

  • Audio initialization is never started during module evaluation.
  • started means both Web Audio unlock and caller bootstrap succeeded.
  • A bus topology cannot change silently; dispose before rebuilding it.
  • Configured gain, duck attenuation, and mute reasons are independent state.
  • Manual, focus-loss, and global-preference mutes cannot undo one another.
  • Resolver initialization is idempotent for identical options and rejects different options until disposal.
  • Sprite resolver preload: false still validates the complete JSON map, but defers each sheet's fetch and decode until its first cue. Pending cue IDs are tracked before public Howler loading starts, so transport, mix, spatial, and teardown controls remain effective without private Howler queue access.
  • Preference read/update transactions are serialized per store so concurrent sliders cannot overwrite a newer runtime or persisted value with stale state.
  • Every active Howler sound retains its target bus so live volume and mute changes affect loops and long samples, not only future playback.
  • Cue gain and cue fades are logical layers: their output is recomposed with current resolver master/bus gains and mute reasons on every update. A paused cue freezes its fade clock and resumes with the same Howler ID.
  • Per-file codec declarations are consistent across all sprites that share a Howl; the resolver never treats a failed codec HTTP request as a fallback.
  • Runtime-only mute layers apply by name to both Tone buses and the Howler resolver without modifying persistence or manual mute layers. Tone duck state remains independently remembered while a bus is muted, so it restores consistently when the mute layer clears; Howler has no fake duck layer.
  • Public runtime imports do not evaluate Node-only build tooling.

Lifecycle and cleanup

registerAudioGestureTrigger returns a listener cleanup. disposeBuses clears duck timers and Tone nodes. disposeSpriteResolver unloads Howls, clears active sound bookkeeping (including pending lazy-sheet callbacks), and allows initialization with new options. _resetAudioEngine is retained for existing test/hot-reload integrations; application shutdown normally needs the returned listener cleanup plus the two disposal functions.

Error policy

Programmer errors such as invalid topology, non-finite gain, path traversal, or malformed verifier configuration throw immediately. Runtime sprite acquisition degrades to an empty resolver by default because audio may be optional; strict mode turns the same failure into a rejected bootstrap transaction. Persistence errors are rethrown after restoring the prior runtime mix.