Skip to content

Smanager

Smanager solves a common problem in games and interactive web apps: managing dozens (or hundreds) of sound files without drowning in boilerplate. It wraps the Web Audio API in a structured lifecycle — atlas loading, prioritised fetching, language switching, and deterministic disposal — so you can focus on the experience instead of low-level buffer plumbing.

The library is built around a sound atlas concept: a JSON manifest that maps human-readable source names to encoded audio files, grouping them into packages with per-item metadata (bitrate, channel count, sample count, language). This makes it trivial to swap localised voice-overs, load only what the current page needs, and free everything when the user navigates away.

  • Sound Atlas System — Organise sounds into named packages via a JSON manifest. Each entry carries source name, encoded filename, sample count, and language tag, enabling fine-grained control over what gets loaded and when.
  • Priority Loading — Define a priority list of source names so critical sounds (UI feedback, first-interaction audio) are fetched before background music or secondary effects.
  • Multilingual Audio — Switch languages at runtime with setLanguage(). The manager resolves the correct file per source name based on the active language stack, falling back gracefully to non-localised assets.
  • Sync & Async Buffer AccessrequestBufferSync() returns an AudioBuffer immediately (silent placeholder if still loading, filled in-place when the network request completes), while requestBufferAsync() returns a thenable SoundPromise. Scheduled playback stays in sync either way.
  • Reversed Playback BuffersrequestBufferReversedAsync() creates a sample-reversed copy of any loaded buffer, working around browsers that don’t support negative playbackRate.
  • Granular Disposal — Dispose a single item, an entire package, a specific language, or everything at once. The reload() / reloadWithAtlas() methods handle the full teardown-and-reinit cycle for SPA page transitions.
  • Typed Event System — A lightweight, generic TypedEventTarget emits events like fileloaded, fileloaderror, languagechanged, and packagechanged, with support for one-shot listeners and clean disposal.
  • iOS Compatibility — Configurable file extension (.webm or .mp4) lets you serve Opus in WebM on modern browsers and fall back to MP4 for older iOS versions.

The atlas JSON maps packages to arrays of sound items:

{
"localised": [
["voice_player", "24kb.1ch.12833676159346608193", 54128, "english"],
["voice_banker", "24kb.1ch.16205214575666683713", 78000, "english"],
["voice_player", "24kb.1ch.13700607245644122936", 60500, "swedish"],
["effect_riser", "48kb.2ch.6176321738731274452", 23700, "_"]
],
"template": [
["music_drums", "24kb.2ch.12372919168763747631", 192000, "_"]
]
}

Each tuple is [sourceName, encodedFilename, numSamples, language]. The filename encodes bitrate and channel count (e.g. 24kb.2ch.…), which the manager parses at runtime — no extra metadata files needed.

A typical setup looks like this:

const manager = new SoundManager(
new AudioContext({ sampleRate: 48_000 }),
atlas,
"https://cdn.example.com/sounds/",
["localised", "template"], // active packages
["english", "_"], // language priority
".webm"
);
// Load critical sounds first
manager.setPriorityList(["voice_player", "effect_riser"]);
await manager.loadEverything();
// Play a sound
const source = manager.context.createBufferSource();
source.buffer = manager.requestBufferSync("voice_player");
source.connect(manager.context.destination);
source.start();
// Switch to Swedish — next requestBuffer calls resolve the Swedish variant
manager.setLanguage("swedish");

Rather than a plain Promise<AudioBuffer>, the library uses a custom SoundPromise class that tracks loading state (UNLOADED → LOADING → LOADED | REJECTED | DISPOSED) and exposes the decoded buffer synchronously via .value. This enables a “play now, fill later” pattern: the manager creates an empty AudioBuffer of the correct duration, hands it back immediately, then overwrites its channel data in-place once the network fetch completes.

const sp = manager.requestBufferAsync("music_drums");
console.log(sp.state); // 0 (UNLOADED) or 1 (LOADING)
console.log(sp.value); // AudioBuffer (silent placeholder)
await sp;
console.log(sp.state); // 2 (LOADED)
console.log(sp.value); // AudioBuffer (now filled with decoded audio)

The test suite uses Vitest with mock implementations of AudioContext and AudioBuffer (via node-web-audio-api as a dev dependency and custom mocks in fixtures/mocks.ts). Tests cover the full surface: language switching, package prioritisation, atlas loading, item disposal, reload cycles, and the SoundPromise state machine. Coverage is configured with @vitest/coverage-v8, excluding index re-exports and the event target base class.

LanguageTypeScript
PlatformWeb (Web Audio API)
Buildesbuild (minified ESM bundle) + TypeScript compiler (declarations)
TestingVitest + @vitest/coverage-v8
Lintingoxlint
TargetES2017+ browsers
LicenseMIT

The source code is available on the project’s GitHub repository.