audiocrate
A Web Audio scene graph, an extensible AudioMaterial system, and a serializable DSP graph that the same interpreter runs offline and in an AudioWorklet.
Inspired by how three.js is organized: a scene of objects, materials you can write yourself, and loaders. Not an editor.

npm install audiocrate
No bundler plugin. No peer dependencies. It works in Vite, webpack, Next.js, esbuild, Rollup, and Node.
audiocrate is the open source audio engine underneath homecrate. MIT licensed. Source: github.com/antpb/audiocrate.
[Demo] Editor live demo: audiocrate.homecrate.app
[Demo] Three.js implementation via XR Publisher Plugin demo: audiocrate.homecrate.app/xr-publisher
homecrate for iOS/macOS/visionOS: homecrate.app
Android App running audiocrate in development 90% feature parity with iOS using the audiocrate engine fully.
What it is
Web Audio is a graph of platform nodes (BiquadFilterNode, GainNode, and
the rest). Custom DSP means writing an AudioWorkletProcessor, then a
parameter system, a message protocol, and an offline path around it.
Libraries on top of that often ship an application: their sounds, their timeline, their track model.
Audiocrate is the layer underneath that.
Shape
| three.js | Audiocrate |
|---|---|
Scene | AudioScene |
Mesh (geometry + material) | Clip (buffer + AudioMaterial) |
Material / ShaderMaterial | AudioMaterial / one with an ASL graph |
WebGLRenderer | WebAudioRenderer |
The mapping is structural, not a port. Object3D is a transform hierarchy;
Track and Bus are mixer nodes (volume, pan, mute, inserts). Positions
live on scene.spatial. You do not call renderer.render(scene): live
playback is on the scene, and OfflineRenderer takes an AudioMaterial graph.
Audio also needs a transport with one scheduling origin and a tempo map. The audio thread is an isolated context with no imports and a hard deadline, which is why kernels and measurement work the way they do.
AudioMaterials
import { AudioMaterial, param, filter, osc, env } from 'audiocrate';
const bell = new AudioMaterial({
name: 'Bell',
params: {
pitch: param.range(50, 2000, { default: 440, unit: 'Hz' }),
cutoff: param.range(200, 12000, { default: 3000, unit: 'Hz' }),
},
graph: ({ params }) =>
filter.lowpass(
osc({ freq: params.pitch, type: 'sine' })
.mul(env.adsr({ attack: 0.005, decay: 0.4, sustain: 0, release: 0.1 }).trigger(1)),
{ cutoff: params.cutoff },
),
});
A parameter schema and a signal graph, both plain data. From that:
- Real-time playback, compiled into one AudioWorklet per voice, not one
AudioNodeper operation. - Offline rendering through the same interpreter.
- An inspector model.
describeAudioMaterial(bell)returns the faders, ranges, and units. Audiocrate does not draw them. - Serialization. The graph is JSON.
A new AudioMaterial does not require a change inside this library. Almost every core AudioMaterial is ASL. The one that is not is the IR convolver, which ships as a kernel. DSP that still is not a per-sample expression can go out as a portable wasm module; see Kernels.
Rendering
import { OfflineRenderer } from 'audiocrate';
const { samples, sampleRate } = OfflineRenderer.render(bell.graph, {
duration: 0.5,
sampleRate: 48000,
params: { pitch: 440, cutoff: 3000 },
});
import { WebAudioRenderer } from 'audiocrate';
const ctx = new AudioContext();
const renderer = new WebAudioRenderer(ctx);
const voice = await renderer.createVoice(bell.graph);
voice.node.connect(ctx.destination);
voice.noteOn({ pitch: 440 });
Same graph. Same interpreter. The offline path is not a second implementation.
Scenes
import { AudioScene, Track, Clip, Time } from 'audiocrate';
const scene = new AudioScene();
await scene.start();
const guitar = scene.addTrack(new Track({ name: 'Guitar' }));
guitar.pan = 0.3;
guitar.materials.add(bell);
guitar.addClip(new Clip({ buffer }), { at: Time.bars(2, 1, 0) });
scene.transport.play();
Time is structured (Time.bars(2, 1, 0)). One scheduling origin, a tempo map,
and a transport that offline and live rendering share.
Same graph, more than one place
A second interpreter exists in Swift. A conformance gate renders 83 graphs covering all 70 node kinds through both, comparing sample by sample to a tolerance of 5e-6. Adding a node kind to one implementation and not the other fails the build on both sides. Conformance is the walkthrough.
The same graph has also been hosted in someone else’s engine: AudioMaterials
drive THREE.Audio sources through setNodeSource(), with no change to
three.js and no change to audiocrate.
Size
Minified, bundled by esbuild from the published tarball. Run
node scripts/measure-size.mjs in the package to reproduce.
| What you import | Bundled |
|---|---|
audiocrate/theory alone | 1.1 KB |
| Scene graph plus offline rendering | 73 KB |
| Everything including real-time audio | 181 KB |
The AudioWorklet is 101 KB of that last row. A worklet realm cannot import, so it carries the whole interpreter as a string and minifying does not shrink it. Apps that never play real-time audio tree-shake it away. That is the difference between the second row and the third.
The worklet and your bundler
An AudioWorkletProcessor runs with no imports and no network. It has to
arrive as one self-contained script fetched from a URL, and bundlers do not
agree on how to produce one.
Audiocrate ships the worklet as a string and mints URLs at runtime, trying
blob: then two flavours of data: in order. Engines disagree about which
schemes a worklet may load. There is nothing to configure and no plugin to
install.
To serve it as a cacheable asset, import audiocrate/worklet and pass
workletUrl to WebAudioRenderer.
Also included
| Theory | Notes, scales, chords, key detection, pitch tracking. Standalone, no audio engine. |
| Spatial | First-order ambisonic sources and a listener decode. Positions are AudioParams. |
| Collaboration | Shared session state over a transport you supply. Discrete edits are last-write-wins. Device ids stay on the machine. Audiocrate does not open a socket. |
| Hooks | Named extension points for a host application. |
| Testing | Offline render-diff harness and wav encode/decode. |
Documentation
| Philosophy | Design rules and scope |
| Scenes and time | Scenes, tracks, clips, transport, tempo maps |
| AudioMaterials | Parameters, graphs, plugins, assets |
| The Audio Shader Language | Nodes, graphs, live inputs, channels, measurement taps |
| Kernels | Block-shaped DSP, portable wasm modules |
| Component library | Shipped AudioMaterials and nodes |
| Theory | Notes, scales, chords, keys, audio to notes |
| Spatial | Placing sound in 3D, listener rotation, binaural decode |
| Collaboration | Session state, discrete edits, the shared clock, and host-local devices |
| Hooks | Extending a host without forking Audiocrate |
| Conformance | How cross-implementation agreement is enforced |
| Examples | Custom-kernel plugins the editor plays (amp, grain, synth, room) |
What Audiocrate is not
- Not a DAW. The published package has no UI. An example graph editor and custom-kernel plugins live in the repository and are not shipped on npm.
- Not a plugin format. It hosts DSP. It does not replace AU, VST, or similar standards.
- Not a cloud service. Nothing here requires a server. The collaboration layer takes a transport you supply.
- Not a sample library. Core ships primitives: filters, envelopes, delays, the usual building blocks.
Status
0.1.0, first public release. Expect breaking changes in minor versions until 1.0.
Known limits, also listed in the docs: the conformance gate renders only fixed parameters, mobile voice budgets are estimated rather than profiled, and the kernel path has one shipped implementation.