@codex-music/audio-native
The native and offline backends: a compiled-TypeScript AudioContext control API over the native ScriptC audio engine.
The native side of the transport. This package is a compiled-TypeScript
control API shaped like Web Audio, sitting over a native audio engine — the
thing the plugin shell's processBlock pulls from, and the thing CI renders
through when a test needs deterministic audio with no browser in the room.
Compiled TypeScript, not a binding
api.ts is not a Node addon wrapper. It is restricted TypeScript that ScriptC
compiles to native code, and its bottom edge is a set of ambient declarations:
declare function nativeAudioContextCreate(sampleRate: number, channels: number): number;
declare function nativeAudioCreateOscillator(context: number): number;
declare function nativeAudioScheduleParam(
context: number, node: number, param: number,
eventType: number, value: number, when: number,
): number;
ffi.json maps each of those sixteen declarations to a C symbol with its
parameter and return kinds — nativeAudioContextCreate → sc_audio_context_create,
(f64, f64) → f64 — and names the archive that provides them,
build/libscriptc_audio_probe.a, built from native/audio_context.c. Handles
are opaque numbers across the boundary; nothing structured crosses it.
The consequence worth internalizing: the control API is written in the same language as your kernel and your UI, and it is not interpreted at runtime. It compiles.
The AudioContext-shaped surface
The exported classes will look familiar, because looking familiar is the point:
import { AudioContext, OfflineAudioContext, encodeAudioBufferToWav } from "@codex-music/audio-native";
const context = new OfflineAudioContext({ numberOfChannels: 2, length: 480, sampleRate: 48000 });
const osc = context.createOscillator();
const gain = context.createGain();
osc.connect(gain).connect(context.destination);
gain.gain.setValueAtTime(0.25, 0);
gain.gain.linearRampToValueAtTime(0.5, 0.01);
osc.start(0);
const buffer = await context.startRendering();
const wav = encodeAudioBufferToWav(buffer);
AudioParam carries value plus setValueAtTime,
linearRampToValueAtTime, and exponentialRampToValueAtTime, each returning
this for chaining. AudioNode.connect returns its destination and refuses to
join nodes from different contexts. GainNode and OscillatorNode expose
gain and frequency. AudioBuffer implements the planar subset — length,
numberOfChannels, sampleRate, duration, getChannelData,
copyFromChannel, copyToChannel — with real validation (positive integer
lengths, one or two channels, sample rates between 8000 and 192000).
OfflineAudioContext.startRendering() returns a Promise<AudioBuffer> and
rejects if called twice, and AudioContext.renderMetrics(frames) returns
{ peak, rms } for tests that only need to know a graph produced signal.
encodeAudioBufferToWav writes a canonical interleaved PCM16 RIFF/WAVE file.
Note that AudioContext.resume() throws with a clear message on non-macOS
platforms: real-time device playback is macOS-only in the current probe.
What the probe proves
./verify.sh builds everything and byte-compares fixed expected output. It runs
four render lanes — offline-test and offline-compat against both the LLVM
and the C backend — plus a WAV generation check and a webaudio-node contract
test. Every expectation is a literal string in the script:
single 0.2500 0.1768 1.0000
scheduled 0.5000 0.2500 0.0100
and the compat lane pins linear automation to 0.3125 -0.4375 0.5000 and
exponential to 0.3536 -0.7071 1.0000. The claim being verified is narrow and
strong: two independent compilation backends produce bit-identical numbers for
the same graph, and the automation ramps behave exactly as the Web Audio
formulas specify. That is the foundation the parity story stands on — see
Parity and determinism.
Backends it powers
Two EngineBackend values route here. "native" is the plugin shell path: the
DAW's processBlock pulls the engine with the plugin's ScriptC-compiled kernel
archive linked in, and audio never touches the webview or any JavaScript. Host
MIDI and sample-accurate automation are translated into the common AudioEvent
ABI first, so the event stream reaching the kernel is the same shape the browser
produces. "offline-native" is the CI and batch path — deterministic renders,
no device, no clock.
Roadmap
The package description calls itself a probe that "grows into the quantum engine per the M1-M4 roadmap," and the milestones are explicit. M1 moves to 128-frame f32 planar rendering with reusable scratch arenas, adds disconnect, fan-out caching, topological validation, oscillator waveforms, constant source and biquad, and requires zero allocations during a render quantum on both backends, plain and sanitized. M2 integrates one production real-time device layer with an SPSC command queue, state transitions, sample-rate negotiation and underrun counters, exiting on an hour-long clean stress run. M3 adds the compiler effect/profile check and the borrowed f32 slice ABI, emitting one direct native kernel per statically imported processor, and exits when user TS transforms a signal in real time with no callback allocation and matches offline output sample-for-sample. M4 is Linux and Windows device backends, hot-plug handling, denormals, and SIMD where measurements justify it.
The plugin shell that consumes this today targets V1 scope: VST3 plus standalone on macOS.