npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@forgeax/engine-audio

v0.1.21

Published

Realm-neutral ForgeaX audio: ECS components, tick/plugin, backend protocol, POD clips, and closed Host intent transport.

Readme

@forgeax/engine-audio

Realm-neutral declarative audio subsystem. Owns AudioSource, AudioListener, the ECS tick/plugin, the AudioBackend protocol, POD clip bytes, and the closed intent vocabulary. Browser playback lives in @forgeax/engine-audio-webaudio and always remains Host-owned.

Core surface

import { AudioSource, AudioListener, audioPlugin, type AudioClipAsset } from '@forgeax/engine-audio';

Layer 1 (play now): spawn AudioSource with playing: true. Layer 2 (mix): bus volume/mute via AudioEngine Resource. Layer 3 (3D): spatialBlend: 1 + AudioListener.

Minimal BGM playback

import { AudioSource, AudioListener, audioPlugin, type AudioClipAsset, type AudioBackend } from '@forgeax/engine-audio';
const created = await createApp(canvas, { plugins: [audioPlugin()] });
if (!created.ok) throw created.error;
const app = created.value;

// Load clip via asset system
const loaded = await app.renderer.assets.loadByGuid<AudioClipAsset>(bgmGuid);
if (!loaded.ok) throw loaded.error;
const clip = app.world.allocSharedRef('AudioClipAsset', loaded.value);

// Spawn BGM source
app.world.spawn({
  component: AudioSource,
  data: { clip, playing: true, loop: true, volume: 0.8, spatialBlend: 0, bus: 'music' },
}).unwrap();

// Spawn listener on camera entity
app.world.spawn(
  { component: Transform, data: {} },
  { component: Camera, data: {} },
  { component: AudioListener, data: {} },
).unwrap();

Realm boundary

| Tier | Engine-side backend | Host-side owner | |:--|:--|:--| | main-serial | Direct intent adapter | WebAudioEngine in the Host realm | | engine-worker | createAudioIntentBackend batches POD intents with frame completion | createHostAudioConsumer owns decode, cache, nodes, and AudioContext | | shared | Same Engine Worker intent path | Same Host consumer; Kernel Workers never receive audio state |

AudioClipAsset is { kind: 'audio', sourceKey, bytes }. The first play intent for a sourceKey carries bytes; identical later plays reuse the Host decode cache, while changed bytes under the stable key are republished. Intents cover play, stop, per-source volume, bus volume/mute, listener pose, and destroy. Stale async decode completion is fenced by both the entity play epoch and the current source-key content, so it cannot resurrect a stopped, replaced, or superseded source.

ECS component schema

AudioSource (6 fields)

| Field | Type | Default | Description | |:--|:--|:--|:--| | clip | Handle<'AudioClipAsset', 'shared'> | required | World shared-ref handle for the loaded POD clip | | playing | boolean | false | Edge-detected: false->true starts playback, true->false stops | | loop | boolean | false | When true, AudioBufferSourceNode.loop is set; one-shot otherwise | | volume | number | 1.0 | Per-source GainNode gain.value; range 0..+Inf (amplification allowed) | | spatialBlend | number | 0 | 0 = 2D (direct to bus), 1 = 3D (PannerNode with equalpower model) | | bus | 'sfx' \| 'music' | 'sfx' | Target bus in the fixed two-bus topology (SFX + Music -> Master) |

AudioListener (marker component)

| Field | Type | Description | |:--|:--|:--| | (none) | -- | Marker component. Attach to the entity whose Transform.world (16-float column-major mat4, written by propagateTransforms) drives Web Audio listener position/orientation. Only the first AudioListener entity in the World is synced per frame (E-3). |

Bus control via AudioEngine Resource

const audio = world.getResource<AudioBackend>('AudioEngine');
audio.setBusVolume('music', 0.3);
audio.setBusMute('sfx', true);
audio.setBusMute('sfx', false); // restores previous volume
const { contextState, activeSourceCount } = audio.getState();

Error model (charter P3 explicit failure)

All public-facing failure paths return Result<T, AudioError>. AI users consume via exhaustive switch (err.code) -- no default branch needed (TypeScript strict enforces completeness).

| code | trigger | hint | |:--|:--|:--| | context-creation-failed | new AudioContext() threw or returned null | check browser supports AudioContext; verify no privacy extension blocks audio | | decode-failed | decodeAudioData(arrayBuffer) rejected | ensure audio file is a valid wav/mp3/ogg/flac at the GUID path | | context-suspended | play() called while AudioContext is suspended and gesture listener failed | call play after user gesture (click/tap/keydown) to trigger resume() | | invalid-clip-handle | AudioSource.clip handle is dangling | verify clip was registered via AssetRegistry.register() before spawning | | bus-not-found | AudioSource.bus is outside 'sfx' \| 'music' | use 'sfx' or 'music' bus literal; custom bus names not supported in v1 |

Each error carries 4-field structured surface: .code / .expected / .hint / .detail. .detail is narrowed per-code via discriminated union (e.g. decode-failed carries reason: string).

Known limitations

  • bounded live gain transitions -- WebAudioEngine setVolume, setBusVolume, and setBusMute cancel prior automation at AudioContext.currentTime and schedule one 10 ms linear transition to each finite, non-negative target. Initial pre-start gain setup remains immediate.
  • No nested bus routing -- fixed two-bus topology only (OOS-2).
  • No playback speed control -- deferred (OOS-7).
  • No audio-specific Inspector method -- use app.execution.report().audio or the existing Remote execution root.

Related packages

Simulation participant boundary

The realm-neutral audio package may contribute a ready participant whose state is portable data. ECS remains the single owner of record, restore, trace, comparison report, numeric tolerance, and closed error values.

Use the minimum path with a source World and a fresh target. Compare semantic audio intent counts and cleanup invariants; never serialize AudioContext, AudioBuffer, source nodes, or a host consumer. The Web Audio package remains Host-owned and consumes intents after the simulation boundary.

When restore or comparison fails, switch on the returned code and follow its expected, hint, and detail; do not parse messages or silently drop audio. This seam is not RHI tape replay and does not define a game replay format.