audioJS

@audio/compile

Status pending calibration

npm0.1.4licenseMIT

JavaScript audio processors to AudioWorklet nodes and experimental macOS VST3 plugins

Overview

Install
Updated
Family
2 packages under it

Run JavaScript audio processors in an AudioWorklet, or build experimental macOS VST3 plugins from the same source.

A factory function holds DSP state; attached metadata describes its parameters and identity. This package exports the implemented adapters. The contract describes the common shape; the guide covers composition and migration.

Status

Target Implemented scope Still to establish
@audio/compile-wam JavaScript AudioWorklet, numeric automation, enum/bool parameters; WAM-shaped metadata Independent WAM host interoperability
@audio/compile-vst Experimental source: macOS stereo builder; parameters, state, latency and tails Build/release work waits for JZ v1, then independent validator/DAW acceptance
audio/batch Offline/stream hosting in the audio engine Separate package and release lifecycle
AU, CLAP, LV2, general CLI Planned No target adapters yet; Logic requires AU

The npm @audio/compile@0.1.3 release contains only documentation. The package checks prepare @audio/compile@0.1.4, @audio/compile-wam@0.1.2 and @audio/compile-vst@0.0.1 archives. Only the AudioWorklet path is being prepared for use now; native release/DAW work waits for JZ v1. Testing the VST package's exports is not evidence that a released compiler can build it.

Try the packed preview

From this repository, with Node and Python 3 installed:

npm ci
npx playwright install chromium firefox webkit
npm run pack:preview

This installs real tarballs in temporary consumers without workspace links, checks Node exports and the standalone adapter, renders gain in Chromium/Firefox/WebKit and exercises the shipped demo. On success, the AudioWorklet archive is saved in .work/preview/; native archives remain internal packaging fixtures until the JZ v1 gate passes.

Install the standalone AudioWorklet archive into a new directory (replace /path/to/compile with this checkout):

mkdir audio-preview
cd audio-preview
npm init -y
npm install /path/to/compile/.work/preview/audio-compile-wam-0.1.2.tgz
python3 -m http.server 8080 --bind 127.0.0.1

Open http://127.0.0.1:8080/node_modules/@audio/compile-wam/demo/index.html. Start the tone, change gain/compressor controls, then stop. The demo and processors are included in the package and use local imports; no CDN or bundler is needed. This is a development example, not a finished effect UI.

Release checks

npm test                      # workspace suites + installed tarballs in three browsers
npm run test:package          # package/export checks and installed browser demo only

Native release gate: JZ v1. Published jz@0.9.2 rejects host: 'native'. Wait for v1 and verify its installed package before adding the native package-build CI job or claiming a working native release. Development checkouts and existing fixtures are not a substitute for that check. No compiler snapshot is pinned by this preview.

The AudioWorklet leaf can be released independently. Hold the native leaf and the umbrella's native dependency until the JZ v1 gate passes; then release verified leaves before the umbrella and verify exact public versions in a fresh consumer. npm run pack:preview and the tests do not publish anything.

Packages

Target adapters live in packages/compile-*, following the other audiojs families. Use the umbrella or depend directly on a target:

import { toWam } from '@audio/compile'
// Or: import { toWam } from '@audio/compile-wam'

@audio/compile-wam has no JZ or native-tool dependency. The Node entry also exports toVst3; browser consumers use toWam. The former @audio/wam source has moved here. Migrate consumers when the replacement release is available; do not remove a working dependency in favor of an unpublished package.

@audio/compile-vst contains the native builder; see its scope and API. AU, CLAP and LV2 adapters remain planned.

Run npm test for all implemented target suites. The native fixture is a separate npm run test:vst because it needs the local native toolchain and host.

The contract

export const gain = (ctx) => (inputs, outputs, params) => {
  let g = 10 ** (params.value[0] / 20)
  let inp = inputs[0], out = outputs[0]
  for (let c = 0; c < inp.length; c++)
    for (let i = 0; i < inp[c].length; i++)
      out[c][i] = inp[c][i] * g
}
gain.params = {
  value: { type: 'number', min: -60, max: 6, default: 0 }
}

Ship it as audio.js in your package with an "audio": "./audio.js" field — every host and this compiler finds it. See CONTRACT.md for the full shape and GUIDE.md for examples, coverage, and migration.

Development fixtures (macOS)

The following experiments predate the public VST builder. Their private fixture limitations are separate from the current builder scope.

npm run test:vst -- /tmp/audiojs-gain

Builds the existing packages/compile-wam/examples/gain.js atom with local JZ, translates its WASM with wasm2c, and packages a native Gain.vst3. Loads it through ../host/packages/host-vst and compares stereo samples with both JS and WASM. Covers 44.1/48 kHz, six block sizes, short blocks, silence, repeated processing, heap growth, overlapping instances, and gain changes from −60 to +6 dB. No DAW or audio device required.

Requires the sibling built host-vst project, local JZ, Xcode command line tools, and Homebrew WABT. Override paths with AUDIO_ROOT, JZ_ROOT, WABT_ROOT, and SDKROOT. WASM and native build outputs stay in the supplied output directory (a temporary directory by default).

This is a gain automation fixture, not the general plugin compiler. The atom source is unchanged. The native shell exposes its gain parameter; controller writes reach DSP only through VST3 parameter queues. Its state blob exposes heap diagnostics and the number of delivered updates for the test. Padding short blocks is valid here because gain is stateless. Compiler-specific value decoding stays in the small DSP adapters behind the common VST shell. SIMD is disabled for this first integration check.

The host applies setParam(id, normalizedValue) at the next block boundary; multiple writes coalesce, and consumed updates are not replayed. Within-block sample-offset automation is not implemented by this fixture. Validate with an independent VST3 host/validator before treating the bundle as distributable.

Stateful lifecycle

npm run test:vst -- --stateful --compiler=jz
npm run test:vst -- --stateful --compiler=porffor

The stereo one-pole fixture keeps closure state between callbacks and receives actual frame counts, including short blocks. Both backends pass 12,438 checks against the same JS processor: parameter changes, independent overlapping instances, close/reopen, last-close arena release, and zero callback heap growth. The frame-count argument is private to this fixture, not a new audio atom API. Porffor's shared arena remains alive until its final instance closes. Concurrent audio threads and within-block parameter automation are still unvalidated.

Porffor and native timing

npm run test:vst -- /tmp/gain-jz --compiler=jz --bench
npm run test:vst -- /tmp/gain-porffor --compiler=porffor --bench

--compiler=jz|porffor selects the complete build chain (default: jz). The older --porffor spelling remains an alias. Unknown compiler names fail before building. Compiler selection belongs to the target builder, never to atom metadata or the host.

The current @audio/compile-vst builder accepts { compiler: 'jz' } and rejects Porffor. The dual-backend fixture below is separate experimental work; the general CLI is still planned. Keep the processor contract, plugin identity, parameter behavior and lifecycle tests common; keep compiler layouts inside adapters. A compiler option selects a supported chain, not an arbitrary list of passes.

Both builds use the unchanged gain atom, the same VST shell and sample tests, and Clang -O2 -ffp-contract=off. JZ emits scalar WASM, translated by WABT; Porffor emits C directly. PORFFOR_ROOT selects its source checkout (defaults to the sibling porffor); its npm package is not used. The Porffor mode does not require JZ or WABT. Generated C symbol signatures are checked before linking.

--bench builds a native host loop and writes benchmark.json with compiler and host revisions, CPU, flags, binary size, and block timings. At 48 kHz it warms 2,000 blocks, then measures 20,000 blocks at each of 64/128/512/1024 frames. Buffers are preallocated and output is checked. Timing includes native host, plugin and buffer-copy costs; compilation, loading, JS/NAPI calls and audio hardware are outside the measured loop. Compare median and p99, plus the worst observed time against the block deadline. Gain is a small integration fixture, so these results do not establish performance on larger DSP programs or prove hard real-time deadlines. Run comparisons sequentially on an otherwise idle machine.

The experimental Porffor adapter uses --no-gc: each instance has independent buffers and closure state, but the shared arena retains allocations until the last plugin instance closes. Concurrent processing threads and general runtime embedding are not validated. This is an offline feasibility test, not a complete Porffor plugin backend.

The stateful VST fixture also renders 4,000 blocks across two simultaneous worker audio threads, comparing each instance against the JavaScript atom and checking fixed callback heaps. Setup and teardown are serialized. This does not yet cover automation points inside a block or a public VST builder.