A Web Audio scene graph, extendable 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. This repo also has
editor/, an example app that is not in the npm package.
[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
npm install audiocrateNo bundler plugin. No peer dependencies.
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.
| 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.
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.
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.
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.
A second interpreter exists in Swift
(examples/CrateASL). 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.
Minified, bundled by esbuild from the published tarball. Run
node scripts/measure-size.mjs to reproduce. The imports behind each row
are in that script.
| 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.
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.
| Theory | Notes, scales, chords, key detection, pitch tracking. Standalone, no audio engine. |
| Spatial | Positional sources and listener maths. |
| Collaboration | Shared session state over a transport you supply. Audiocrate does not open a socket. |
| Hooks | Named extension points for a host application. |
| Testing | Offline render-diff harness and wav encode/decode. |
docs/ is the reading order. Hosted copy: https://homecrate.app/docs/crate/.
| 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 |
| Editor | Example AudioMaterial graph editor (not published) |
| Examples | Custom-kernel plugins, the XR host adapter, and the Swift interpreter |
- Not a DAW. The published package has no UI.
editor/lives in this repository and is 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. Custom kernels that need a worklet
entry live in
examples/and are not published.
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.
MIT. See LICENSE.
