audioJS

@audio/compile-vst

Status pending calibration

npm0.0.1licenseMIT

Compile audio.js atoms to native VST3 plugins with JZ

Overview

Install
Updated
Family
compile, 2 packages under it

Compile audio.js atoms to native VST3 plugins with JZ. The compiler is a build-time choice; Porffor is planned and build({ compiler: 'porffor' }) refuses until it lands.

The same factory-with-metadata that @audio/compile-wam runs as an AudioWorklet becomes a .vst3 bundle. The plugin's identity, parameters, buses, latency and tail come from the manifest exactly as the contract specifies; the DSP is the atom itself, compiled ahead of time and linked into one fixed, native shell.

Use

Native build and release work waits for JZ v1. Published jz@0.9.2 lacks the native host profile. The API below describes the experimental source; it is not an install-and-build promise against the current registry release. Once v1 is released, verify it from an installed tarball before resuming native CI and DAW validation. Do not pin an interim compiler snapshot or use a sibling checkout as release evidence.

import { toVst3 } from '@audio/compile-vst'
import { gain } from '@audio/compile-wam/examples/gain.js'
import { fileURLToPath } from 'node:url'

const plugin = toVst3(gain)          // synchronous: derivation only, no file or process access
plugin.id                            // 'gain'
plugin.native.vst3                   // the class id, derived from vendor + id unless native.vst3 is pinned
plugin.paramSpecs                    // normalized parameter specs, host-facing stable ids included

await plugin.build({
  atom: fileURLToPath(import.meta.resolve('@audio/compile-wam/examples/gain.js')),
  out: './dist',
  compiler: 'jz',
  jz: fileURLToPath(new URL('./', import.meta.resolve('jz')))
})   // macOS: writes a .vst3 bundle in dist

toVst3 refuses what it cannot ship faithfully instead of guessing: an atom without a vendor (the class id derived from it is permanent), streaming: false, declared events (no event transport yet), a number parameter without a finite range, bus layouts beyond mono and stereo, and every parameter-metadata collision the contract names. curve, unit and step are display hints, as the contract says; the wire value always maps linearly.

The real-time gate

Before anything native runs, the JZ backend drives the compiled atom through hundreds of blocks in plain Node with every host import replaced by a stub that throws. The heap cursor and memory size must not move, no stub may fire, and every output sample must be finite. Short, repeated, zero-length and full callbacks are included. The backend exposes the actual callback length on channel and sample-parameter arrays while retaining their allocated capacity. A build that fails this does not link. Install JZ explicitly (npm install jz), or select an exact checkout with build({ jz: '/path/to/jz', ... }) / JZ_ROOT. The compiler version must include the fixes your atom needs; an ambient sibling checkout is a development fallback.

JZ's static final-Wasm facts (compile(…, { inspect: true }).inspect.runtime) are reported alongside as advisory.

Other native shells

The VST3 bundle is one shell around the compiled atom. buildDsp from @audio/compile-vst/dsp runs the same gate and compile, and returns the objects and compiler flags behind native/dsp-abi.h for any host to link. The calls a host makes per block are declared nonblocking, so a Core Audio IOProc can call them under clang's realtime checks. @audio/tap links them into a Core Audio process tap.

import { buildDsp } from '@audio/compile-vst/dsp'
const { objects, flags } = await buildDsp(toVst3(gain), { atom: './gain.js', out: './dsp' })

Scope of v1

macOS bundles built with JZ; mono and stereo buses, negotiated independently for each input and output bus; number, enum and bool parameters, block-rate or sample-rate, with automation inside a block interpolated between the host's points; latency and tail, literal or computed once per instance; flags: ['restart'] applied at the host's next setupProcessing or activation, since the vendored interface declares no component handler to request one. Events, transport and wider speaker layouts are refused rather than silently ignored.