@codex-music/audio-kernels

Stock restricted-TypeScript DSP kernels, their ScriptC library profiles, and the rules kernel sources follow.

The stock DSP kernels: sources you can import, extend, or read for style. Each one is written in restricted TypeScript and paired with a ScriptC library profile, which is what lets the same file become both the JavaScript you run in a browser and the native archive a plugin links.

Dual-purpose by design

This package is two things at once, and understanding that resolves most confusion about it.

It is an npm workspace TypeScript module. Other packages import @codex-music/audio-kernels for types, for a JS oracle to compare a compiled build against, or as a plain fallback path. Subpath exports (./dsp-kernel, ./audio-engine-kernel, ./fx1-kernel) let you reach one kernel without pulling the rest.

It is also ScriptC compilation input. Each profiles/*.profile.json names a kernel as the entry graph for a native library build, declaring an ABI prefix, init/collect/panic-sink symbols, and an export table mapping TypeScript function names to C symbols with their parameter and return kinds:

{
  "profile_format": 1,
  "name": "scriptc-fx1-offline-batch",
  "entry": "../../../apps/offline-batch/native-entry.ts",
  "emission": "llvm",
  "abi": {
    "prefix": "scfx_",
    "init_symbol": "scfx_init",
    "sink_register_symbol": "scfx_set_panic_sink",
    "collect_symbol": "scfx_collect",
    "result_reset_symbol": null
  },
  "exports": [
    { "export": "renderFx1Variation", "symbol": "scfx_render_variation", "params": ["bytes", "f64", "u32"], "returns": "bytes" }
  ]
}

The abi.prefix is the same string your ParamSchema.kernelId carries and the same one plugin.config.json declares as kernel.abiPrefix. Those three must agree; the codegen refuses to run when they do not.

What ships

Three kernels are present today, each with a profile and a corresponding build/<stem>/<stem>.lib.a archive after pnpm --filter @codex-music/audio-kernels build:

SourceProfileABI prefix
dsp-kernel.tsvertical-slice-vector (+ -c variant)scav_
audio-engine-kernel.tsaudio-engine-graph
fx1-kernel.tsfx1-chain (+ -c variant)scfx_

The root export surfaces MonoVoiceKernel (a phase-accumulating sine into a rational soft clip into a one-pole lowpass — the smallest stateful proof that exercises ScriptC's static DSP-math path), renderAudioGraph, renderFx1Variation (an FX-1 insert chain: immutable PCM source → distortion → tone filter → feedback delay), and PCM helpers floatToPcm16, renderTestPcm16, renderTestPcm16Partitioned, alongside the deterministic test signal generators testFrequency, testGain, and testGate.

The osc1-plugin port — oscillator → ADSR → biquad → gain, fused into a voice-subtractive kernel behind the scsub_ prefix — lands later. apps/examples/osc-mini already references profiles/subtractive-voice.profile.json, which stays dangling until it does.

Rules for kernel sources

Four constraints govern every file here, and they are not stylistic:

  • No allocation inside process(). No new, no literals that allocate, no growing. Delay lines, coefficient tables and wavetables are init(sampleRate) work.
  • Math.fround at stateful accumulations. Anywhere a value survives to the next sample — filter memory, delay feedback, envelope state — the explicit f32 boundary is what keeps WASM and native rounding identical. That is the mechanism behind matching output hashes, not a hope about IEEE 754.
  • Public fields only. No private state, no getters hiding computation. ScriptC lowers fields to a fixed struct layout shared by both backends.
  • init(sampleRate) instead of constructors. Setup is an explicit call the host makes, so the host owns the state and its lifetime.

The kernel sources are also deliberately import-free: no DOM, no Web Audio, no Node, no Tone, no native-host imports. dsp-kernel.ts compiles into a native library while the browser worklet runs JavaScript emitted from the same file, and that only works if the file names nothing environment-specific.

The native-entry indirection

One wrinkle worth knowing before you write a profile. A profile's entry points at an app's native-entry.ts, not at the kernel source directly — ScriptC library mode names one module, and the app's thin entry re-exports the kernel's functions under the names and signatures the profile's export table declares.

Those entry files import the kernel by relative path into src/, not by the @codex-music/audio-kernels package specifier. ScriptC's --lib mode compiles workspace packages statically or refuses, and this package's exports map points at bare .ts sources with no compiled JS entry. Ordinary TypeScript consumers — Vite apps, tsc --noEmit — use the package specifier normally.

For the authoring interface these kernels implement, see audio-contracts; for how a compiled kernel reaches a DAW, see plugin-shell-kit.