WakaDSound
WakaDSound is an audio plugin for wakaPAC modelled after DirectSound. It manages static buffers for sound effects and streaming buffers for music and ambience. One playback slot per handle - play() is a no-op if the handle is already playing.
Getting Started
Include the script after wakaPAC and register the plugin using the instance:
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakadsound.min.js"></script>
<script>
wakaPAC.use(wakaDSound); // must be called before any wakaPAC() calls
</script>
Once registered, WakaDSound (constructor) and wakaDSound (singleton instance) are available globally. All API calls go through wakaDSound.
Basic Usage
Load buffers and streams during component initialisation. Store handles at the abstraction root so WakaDSound can route per-handle messages to the owning component:
wakaPAC('myComponent', {
snd: null,
music: null,
async init() {
this.snd = await wakaDSound.loadBuffer('/sfx/click.wav');
this.music = await wakaDSound.loadStream('/music/theme.ogg', { loop: true });
},
msgProc(event) {
switch (event.message) {
case wakaPAC.MSG_LCLICK:
wakaDSound.play(this.snd);
break;
case WakaDSound.MSG_BUFFER_STARTED:
console.log('click sound started');
break;
case WakaDSound.MSG_STREAM_STOPPED:
console.log('music track ended');
break;
}
}
});
Usage
Handles
Every loaded buffer or stream returns an opaque handle. The handle is the single playback slot for that sound - calling play() on an already-playing handle is a no-op. Handles remain valid across multiple play/stop cycles until free() is called.
All generic playback functions - play(), stop(), setVolume(), isPlaying(), free() - accept both buffer and stream handles.
Autoplay
Browsers require a user gesture before audio can play. WakaDSound handles this automatically - calling play() from within a user interaction is sufficient. No manual setup is needed. To suspend audio when the page is hidden and resume when it becomes visible again, react to browserVisible - a reactive property injected into every component's abstraction by wakaPAC:
wakaPAC('myComponent', {
watch: {
browserVisible(isVisible) {
if (isVisible) {
wakaDSound.resume();
} else {
wakaDSound.suspend();
}
}
}
});
Static Buffers
Static buffers are loaded once and held in memory. They are suited to short sound effects that need low latency and may be played repeatedly. loadBuffer() is asynchronous - it fetches and prepares the file, then returns a handle. Once loaded, play() is immediate.
// Default options
const snd = await wakaDSound.loadBuffer('/sfx/shoot.wav');
// With initial volume and pan
const snd = await wakaDSound.loadBuffer('/sfx/shoot.wav', { volume: 80, pan: -0.5 });
// With 3D positional audio
const snd = await wakaDSound.loadBuffer('/sfx/explosion.wav', { positional: true });
Streaming Buffers
Streaming buffers play audio progressively without loading it into memory. They are suited to long-form audio - music, ambience, narration. loadStream() is asynchronous - it prepares the source, then returns a handle.
// Default options
const music = await wakaDSound.loadStream('/music/theme.ogg');
// With initial volume and looping
const music = await wakaDSound.loadStream('/music/theme.ogg', { volume: 60, loop: true });
3D Positional Audio
Positional audio is opt-in per buffer via the positional: true load option. Sound sources and the listener are positioned in 3D space - WakaDSound calculates panning and attenuation automatically. The listener defaults to the origin facing -Z.
const snd = await wakaDSound.loadBuffer('/sfx/engine.wav', { positional: true });
wakaDSound.play(snd);
// Move the sound source
wakaDSound.setPosition(snd, 5, 0, -3);
// Move the listener
wakaDSound.setListenerPosition(0, 0, 0);
wakaDSound.setListenerOrientation(0, 0, -1); // forward vector
setPan() is a no-op for positional buffers. Use setPosition() instead. Positional audio is not available for streams.
Analyser
The analyser delivers waveform data to a callback on every frame. It captures all playing sounds simultaneously - buffers, streams, and any combination thereof.
const analyser = wakaDSound.createAnalyser(data => {
// data is a Uint8Array of time-domain samples (128 values by default)
// called on every animation frame while the analyser is active
drawOscilloscope(data);
});
// When done
wakaDSound.destroyAnalyser(analyser);
The fftSize option controls the resolution. It must be a power of 2 between 32 and 32768. The callback receives fftSize / 2 samples per frame. The default of 2048 is sufficient for a smooth oscilloscope display.
const analyser = wakaDSound.createAnalyser(data => {
drawOscilloscope(data);
}, { fftSize: 1024 });
The callback receives the same buffer on every frame. Copy the data if you need it to persist beyond the callback:
const analyser = wakaDSound.createAnalyser(data => {
const snapshot = new Uint8Array(data); // copy if needed beyond this call
});
The callback pauses automatically when audio is suspended and resumes when it runs again. Call destroyAnalyser() when the visualiser is no longer needed.
Message Routing
WakaDSound sends two categories of messages. Global messages are broadcast to all components. Per-handle messages are sent only to the component that owns the handle. Ownership is determined automatically - WakaDSound scans each component's abstraction root for a matching handle reference. For this to work, handles must be stored as top-level properties of the abstraction, not nested inside sub-objects.
// Correct - handle at abstraction root
wakaPAC('myComponent', {
snd: null,
async init() { this.snd = await wakaDSound.loadBuffer('/sfx/hit.wav'); }
});
// Won't receive per-handle messages - handle is nested
wakaPAC('myComponent', {
audio: { snd: null },
async init() { this.audio.snd = await wakaDSound.loadBuffer('/sfx/hit.wav'); }
});
API
Device
wakaDSound.masterVolume(volume)
Sets the master volume for all sounds and streams. Takes effect immediately.
| Parameter | Type | Description |
|---|---|---|
volume | number | Master volume, clamped to 0–100 |
Returns void | ||
wakaDSound.suspend()
Suspends audio output. All playing sounds go silent. No-op if already suspended.
| Parameter | Type | Description |
|---|---|---|
Returns Promise<void> | ||
wakaDSound.resume()
Resumes audio output. Must be called from within a user gesture if audio was suspended by the browser's autoplay policy.
| Parameter | Type | Description |
|---|---|---|
Returns Promise<void> | ||
Loading
wakaDSound.loadBuffer(url, options?)
Loads an audio file and returns a buffer handle. The audio is held in memory for the lifetime of the handle. Broadcasts MSG_BUFFER_LOADED on success, MSG_BUFFER_FAILED on failure.
| Parameter | Type | Description |
|---|---|---|
url | string | URL of the audio file to load |
options.volume | number | Initial volume, 0–100. Default: 1 |
options.pan | number | Initial stereo pan, -1–1. Default: 0. Ignored for positional buffers. |
options.positional | boolean | Enable 3D positional audio. Default: false |
Returns Promise<Object | null> | Buffer handle, or null on failure | |
wakaDSound.loadStream(url, options?)
Prepares a stream handle for long-form audio. Broadcasts MSG_STREAM_LOADED on success, MSG_STREAM_FAILED on failure. The handle remains valid across multiple play/stop cycles — call free() to release it when no longer needed.
| Parameter | Type | Description |
|---|---|---|
url | string | URL of the audio file to stream |
options.volume | number | Initial volume, 0–100. Default: 1 |
options.loop | boolean | Whether the stream loops. Default: false |
Returns Promise<Object | null> | Stream handle, or null on failure | |
Generic Playback
These functions accept both buffer and stream handles.
wakaDSound.play(handle)
Starts playback. No-op if the handle is already playing. Resumes audio automatically if suspended by the browser.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer or stream handle |
Returns void | ||
wakaDSound.stop(handle)
Stops playback. No-op if not playing. Resets playback to the beginning. The handle remains valid and can be played again. To release all resources, call free() instead.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer or stream handle |
Returns void | ||
wakaDSound.setVolume(handle, volume)
Sets the volume for a handle. Takes effect immediately, whether playing or not.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer or stream handle |
volume | number | Volume, clamped to 0–100 |
Returns void | ||
wakaDSound.isPlaying(handle)
Returns true if the handle is currently playing.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer or stream handle |
Returns boolean | ||
wakaDSound.free(handle)
Stops playback and releases all Web Audio nodes. The handle must not be used after this call.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer or stream handle |
Returns void | ||
Buffer-only
wakaDSound.setPan(handle, pan)
Sets the stereo pan for a non-positional buffer. No-op for positional buffers and streams.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer handle (non-positional) |
pan | number | Pan value, clamped to -1 (left) to +1 (right). 0 = center. |
Returns void | ||
Stream-only
wakaDSound.loop(handle, loop)
Enables or disables looping for a stream. Takes effect immediately, whether playing or not. No-op for buffer handles.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Stream handle |
loop | boolean | Whether the stream should loop |
Returns void | ||
wakaDSound.seek(handle, seconds)
Seeks to a position in a stream. No-op for buffer handles.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Stream handle |
seconds | number | Playback position in seconds |
Returns void | ||
Analyser
wakaDSound.createAnalyser(callback, options?)
Starts delivering waveform data to a callback on every frame, capturing all currently playing sounds. Returns an analyser handle that must be passed to destroyAnalyser() when done.
| Parameter | Type | Description |
|---|---|---|
callback | Function | Called each frame with (data: Uint8Array). The same buffer is reused across frames. |
options.fftSize | number | FFT size. Must be a power of 2 between 32 and 32768. Default: 2048 |
Returns Object | null | Analyser handle, or null if the context is unavailable | |
wakaDSound.destroyAnalyser(handle)
Stops the analyser. Audio playback continues unaffected. The handle must not be used after this call.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Analyser handle from createAnalyser() |
Returns void | ||
3D Positional Audio
wakaDSound.setPosition(handle, x, y, z)
Sets the 3D position of a positional buffer's sound source. No-op for non-positional buffers and streams.
| Parameter | Type | Description |
|---|---|---|
handle | Object | Buffer handle loaded with { positional: true } |
x, y, z | number | Position in 3D space |
Returns void | ||
wakaDSound.setListenerPosition(x, y, z)
Sets the listener position in 3D space. Affects all positional buffers simultaneously.
| Parameter | Type | Description |
|---|---|---|
x, y, z | number | Listener position in 3D space |
Returns void | ||
wakaDSound.setListenerOrientation(x, y, z)
Sets the listener's forward orientation vector. The up vector is fixed at (0, 1, 0).
| Parameter | Type | Description |
|---|---|---|
x, y, z | number | Forward direction vector |
Returns void | ||
Messages
WakaDSound sends eleven message types. Global messages are broadcast to all components. Per-handle messages are sent only to the component whose abstraction root contains the handle - the handle must be a top-level property of the abstraction for routing to work.
Buffer Loaded (MSG_BUFFER_LOADED)
Broadcast to all components when a buffer finishes loading successfully.
case WakaDSound.MSG_BUFFER_LOADED: {
// Buffer is ready - event.detail.buffer is the handle
this.snd = event.detail.buffer;
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { url, buffer } - the source URL and the buffer handle |
Buffer Failed (MSG_BUFFER_FAILED)
Broadcast to all components when a buffer fails to load. The returned handle is null.
case WakaDSound.MSG_BUFFER_FAILED: {
console.warn('Buffer failed to load:', event.detail.url);
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { url, error } - the source URL and the error |
Stream Loaded (MSG_STREAM_LOADED)
Broadcast to all components when a stream is ready to play.
case WakaDSound.MSG_STREAM_LOADED: {
// Stream is ready - event.detail.stream is the handle
this.music = event.detail.stream;
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { url, stream } - the source URL and the stream handle |
Stream Failed (MSG_STREAM_FAILED)
Broadcast to all components when a stream fails to load or encounters a mid-playback error such as a network abort. The handle is null for load failures.
case WakaDSound.MSG_STREAM_FAILED: {
console.warn('Stream failed to load:', event.detail.url);
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { url, error } - the source URL and the error |
Buffer Started (MSG_BUFFER_STARTED)
Sent to the owning component when a buffer begins playing.
case WakaDSound.MSG_BUFFER_STARTED: {
// event.detail.handle is the buffer that started
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the buffer handle that started playing |
Buffer Stopped (MSG_BUFFER_STOPPED)
Sent to the owning component when a buffer stops - either via stop() or by reaching the end of the audio data naturally.
case WakaDSound.MSG_BUFFER_STOPPED: {
// event.detail.handle is the buffer that stopped
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the buffer handle that stopped |
Stream Started (MSG_STREAM_STARTED)
Sent to the owning component when a stream begins playing.
case WakaDSound.MSG_STREAM_STARTED: {
// event.detail.handle is the stream that started
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the stream handle that started playing |
Stream Stopped (MSG_STREAM_STOPPED)
Sent to the owning component when a stream stops - either via stop() or by reaching the end of the audio naturally.
case WakaDSound.MSG_STREAM_STOPPED: {
// event.detail.handle is the stream that was stopped
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the stream handle that was stopped |
Stream Ready (MSG_STREAM_READY)
Sent to the owning component when data resumes flowing after a stall. Only sent if MSG_STREAM_BUFFERING was previously sent.
case WakaDSound.MSG_STREAM_READY: {
// Hide buffering indicator
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the stream handle that resumed |
Stream Error (MSG_STREAM_ERROR)
Sent to the owning component when a stream encounters an error during playback - for example a network failure or media decode error. Distinct from MSG_STREAM_FAILED which signals a load failure. The stream handle remains valid after this event but playback has stopped.
case WakaDSound.MSG_STREAM_ERROR: {
console.warn('Stream error:', event.detail.error);
// Optionally attempt recovery or notify the user
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle, error } - the stream handle and the media error |
Stream Buffering (MSG_STREAM_BUFFERING)
Sent to the owning component when the browser cannot fetch stream data. This is transient and often recovers on its own - the component may choose to show a buffering indicator or wait. If data resumes flowing, MSG_STREAM_READY follows. If the stream does not recover, MSG_STREAM_ERROR will follow.
case WakaDSound.MSG_STREAM_BUFFERING: {
// Show buffering indicator
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle } - the stream handle that stalled |
Volume Changed (MSG_VOLUME_CHANGED)
Sent to the owning component when setVolume() is called on a handle.
case WakaDSound.MSG_VOLUME_CHANGED: {
console.log('Volume:', event.detail.volume);
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle, volume } - the handle and the new volume value (0–1) |
Pan Changed (MSG_PAN_CHANGED)
Sent to the owning component when setPan() is called on a buffer handle.
case WakaDSound.MSG_PAN_CHANGED: {
console.log('Pan:', event.detail.pan);
break;
}
Message Parameters
| Parameter | Type | Description |
|---|---|---|
wParam | number | Always 0 |
lParam | number | Always 0 |
event.detail | object | { handle, pan } - the handle and the new pan value (-1–1) |
Notes
masterVolume()affects all buffers and streams uniformly - it is a global level control, not per-handle.- Calling
stop()on a buffer resets it to the beginning — the handle remains valid and can be played again. Callingstop()on a stream pauses it; the nextplay()call reloads the stream from the beginning automatically. - For 2D games and spatial UI, use
setListenerOrientation(0, 0, -1)with the listener at the origin and sound sources positioned on the XZ plane.
Implementation Notes
- Static buffers are decoded entirely into memory on load via the Web Audio API. This gives them very low playback latency -
play()is synchronous once the buffer is loaded.AudioBufferSourceNodeis single-use in the Web Audio API and cannot be restarted once it ends. WakaDSound hides this by creating a fresh source node on everyplay()call while keeping the gain and pan nodes alive across plays. - Streaming buffers use an
HTMLAudioElementfed into the audio graph via aMediaElementSourceNode. On eachplay()call the underlying element is replaced with a fresh one loaded from the same URL. This avoids the decoder-resume click that browsers produce when resuming a pausedMediaElementAudioSourceNode. The gain node persists across reloads so volume control and audio graph routing are unaffected. This approach avoids the complexity ofAudioWorkletwhile still routing the stream through the shared audio graph for master volume control. - Positional buffers use a
PannerNodewith HRTF panning and an inverse distance model. The up vector is fixed at(0, 1, 0)- sufficient for the common case of 2D-plane positional audio. - The analyser inserts an
AnalyserNodebetween the master gain and the audio destination, capturing all output. It drives its ownrequestAnimationFrameloop and callsgetByteTimeDomainData()into a pre-allocatedUint8Arrayeach frame, reusing the same buffer to avoid garbage collection pressure. - Per-handle message routing works by scanning each registered component's abstraction root for a handle that matches by reference. Handles stored in the abstraction are wrapped in a reactive proxy by wakaPAC - WakaDSound unwraps them via
proxy.unwrap()before comparing.
Best Practices
- Register before creating components - call
wakaPAC.use(wakaDSound)before anywakaPAC()calls. Note: pass the instancewakaDSound, not the constructorWakaDSound. - Store handles at the abstraction root - per-handle messages are only routed to the owning component if the handle is a direct property of the abstraction, not nested inside a sub-object.
- Load once, play many times -
loadBuffer()andloadStream()are async operations. Call them duringinit()or at application startup, not on every user interaction. - Free handles when done - call
free()when a component is destroyed or a sound is no longer needed. - Use streams for music, buffers for effects - buffers decode the entire file into memory and are suited for short, frequently triggered sounds. Streams are suited for long audio that should not occupy memory.
- Destroy analysers when done - call
destroyAnalyser()when the visualiser is no longer visible. - Handle suspend/resume for tab visibility - use a
browserVisiblewatcher to callwakaDSound.suspend()andwakaDSound.resume()so audio stops when the user switches tabs.