Features Studio Pricing Docs — Studio Docs — Music Player Docs Tunings Reference Compare Blog About Support
↓ Download iOS / iPadOS macOS visionOS Companion App

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.

One graph, multi renderer, multi player: browser editor, mobile Safari, native macOS and Android, three.js via XR Publisher

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.jsAudiocrate
SceneAudioScene
Mesh (geometry + material)Clip (buffer + AudioMaterial)
Material / ShaderMaterialAudioMaterial / one with an ASL graph
WebGLRendererWebAudioRenderer

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 AudioNode per 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 importBundled
audiocrate/theory alone1.1 KB
Scene graph plus offline rendering73 KB
Everything including real-time audio181 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

TheoryNotes, scales, chords, key detection, pitch tracking. Standalone, no audio engine.
SpatialFirst-order ambisonic sources and a listener decode. Positions are AudioParams.
CollaborationShared 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.
HooksNamed extension points for a host application.
TestingOffline render-diff harness and wav encode/decode.

Documentation

PhilosophyDesign rules and scope
Scenes and timeScenes, tracks, clips, transport, tempo maps
AudioMaterialsParameters, graphs, plugins, assets
The Audio Shader LanguageNodes, graphs, live inputs, channels, measurement taps
KernelsBlock-shaped DSP, portable wasm modules
Component libraryShipped AudioMaterials and nodes
TheoryNotes, scales, chords, keys, audio to notes
SpatialPlacing sound in 3D, listener rotation, binaural decode
CollaborationSession state, discrete edits, the shared clock, and host-local devices
HooksExtending a host without forking Audiocrate
ConformanceHow cross-implementation agreement is enforced
ExamplesCustom-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.