Quickstart
From an empty folder to a pedal making sound in your browser, then the same pedal as a VST3.
The
codexCLI is proposed, not shipped. The commands below describe the intended developer loop; today the same steps are run through the workspace's own scripts andplugin-shell-kit generate.
Scaffold a project
Pick a template and a name. You get a workspace with a kernel stub, a parameter
schema, a dev server that hot-reloads the UI, and a plugin.config.json that
already knows how to become a plugin.
npx codex create my-pedal --template fx-react
cd my-pedal
npm run dev # http://localhost:5173 — audio on first click
Five templates ship with the scaffolder: fx-react and fx-vue for
audio-in/audio-out effects, instrument-react and instrument-vue for
note-driven instruments with voice allocation already wired, and midi-tool
for tools with no audio path at all.
Write the kernel
The kernel is the only part that has to be careful. It is a TypeScript object
with three methods — init, process, reset — and process is called once
per audio block with planar Float32Array channels to read and write in place.
No audio context, no nodes, no graph. Just samples.
// src/kernel.ts
import type { AudioKernel, KernelIO } from "@codex-music/audio-contracts";
interface ToneState {
sampleRate: number;
/** One-pole lowpass memory, per channel. Allocated in init(), never here. */
lp: Float32Array;
}
export const toneKernel: AudioKernel<ToneState> = {
init(sampleRate: number): ToneState {
return { sampleRate, lp: new Float32Array(2) };
},
process(state, io: KernelIO, params, _events): void {
const cutoff = params["tone.cutoff"];
const coeff = Math.min(1, Math.max(0.001, cutoff / state.sampleRate));
const level = params["master.level"];
for (let ch = 0; ch < io.outputs.length; ch = ch + 1) {
const input = io.inputs[ch];
const output = io.outputs[ch];
let z = state.lp[ch];
for (let i = 0; i < io.frameCount; i = i + 1) {
// fround at the accumulation keeps WASM and native bit-identical.
z = Math.fround(z + coeff * (input[i] - z));
output[i] = Math.fround(z * level);
}
state.lp[ch] = z;
}
},
reset(state): void {
state.lp.fill(0);
},
};
Note what is not there: no new inside process, no await, no callbacks, no
console.log. Those are not style preferences — the compiler enforces them, and
it names the call in your chain that broke the rule.
Declare your parameters
Parameters live in one schema, and that schema is the single source of truth for
three surfaces at once: the knobs in your web UI, the automation lanes your DAW
sees, and the values handed to process().
// src/schema.ts
import type { ParamSchema } from "@codex-music/audio-contracts";
export const MY_PEDAL_SCHEMA: ParamSchema = {
kernelId: "sctone_",
params: [
{
path: "tone.cutoff",
label: "Tone",
group: "Filter",
type: "float",
min: 200,
max: 8000,
default: 2500,
units: "Hz",
},
{
path: "master.level",
label: "Level",
group: "Master",
type: "float",
min: 0,
max: 1,
default: 0.9,
},
],
};
Put a UI on it
The UI is a web page. Map over the schema, render whatever control you want, and
call setParam with the dotted path.
// src/App.tsx
import { createAudioEngine, isNativeHost } from "@codex-music/audio-sdk";
import { MY_PEDAL_SCHEMA } from "./schema";
const engine = createAudioEngine({
backend: "shared-wasm",
graph: { schema: MY_PEDAL_SCHEMA, nodes: [{ kind: "sctone_", params: {} }] },
});
export function App() {
return (
<main>
<h1>My Pedal {isNativeHost() ? "(in your DAW)" : "(in your browser)"}</h1>
{MY_PEDAL_SCHEMA.params.map((p) => (
<label key={p.path}>
{p.label}
<input
type="range"
min={p.min}
max={p.max}
step={p.step ?? ((p.max ?? 1) - (p.min ?? 0)) / 200}
defaultValue={p.default}
onChange={(e) => engine.setParam(p.path, e.target.valueAsNumber)}
/>
{p.units}
</label>
))}
</main>
);
}
isNativeHost() checks for the window.codexHost bridge the plugin shell
injects before your bundle loads; the engine picks its transport accordingly.
One component, two places.
Export a plugin
npx codex export vst3
# → build/MyPedal.vst3
# → build/MyPedal.app (standalone, same kernel)
Drop the .vst3 in your plugins folder and open your DAW. The knobs you built
are the knobs the host automates.
What is real today
@codex-music/audio-contracts is complete and stable — every type above exists
as written. The native plugin shell exists and passes the VST3 validator, and
@codex-music/plugin-shell-kit generates its param table, state codec, bridge
JS, and CMake targets from a real manifest. createAudioEngine() currently
throws: the shared-WASM browser backend is being wired first, then the native
codexHost transport. Read How it works next for the
shape of the whole pipeline.