Smanager
Overview
Section titled “Overview”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.
Features
Section titled “Features”- 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 Access —
requestBufferSync()returns anAudioBufferimmediately (silent placeholder if still loading, filled in-place when the network request completes), whilerequestBufferAsync()returns a thenableSoundPromise. Scheduled playback stays in sync either way. - Reversed Playback Buffers —
requestBufferReversedAsync()creates a sample-reversed copy of any loaded buffer, working around browsers that don’t support negativeplaybackRate. - 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
TypedEventTargetemits events likefileloaded,fileloaderror,languagechanged, andpackagechanged, with support for one-shot listeners and clean disposal. - iOS Compatibility — Configurable file extension (
.webmor.mp4) lets you serve Opus in WebM on modern browsers and fall back to MP4 for older iOS versions.
How It Works
Section titled “How It Works”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 firstmanager.setPriorityList(["voice_player", "effect_riser"]);await manager.loadEverything();
// Play a soundconst 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 variantmanager.setLanguage("swedish");The SoundPromise Pattern
Section titled “The SoundPromise Pattern”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)Testing
Section titled “Testing”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.
Tech Stack
Section titled “Tech Stack”| Language | TypeScript |
| Platform | Web (Web Audio API) |
| Build | esbuild (minified ESM bundle) + TypeScript compiler (declarations) |
| Testing | Vitest + @vitest/coverage-v8 |
| Linting | oxlint |
| Target | ES2017+ browsers |
| License | MIT |
Source Code
Section titled “Source Code”The source code is available on the project’s GitHub repository.