@codex-music/audio-sdk

The public entry point: engine selection, host bridge, and shared browser audio/MIDI sessions.

createAudioEngine() throws today. Backends are being wired in order — shared-WASM in the browser first, then the native codexHost transport. The types below are real and stable; only the runtime is pending.

This is the one package an application imports. It exists to answer a single question — which transport carries my audio in this environment? — and to keep that answer out of your component code.

What it exports

The module surface includes createAudioEngine, isNativeHost, AudioSessionController, the CodexHostBridge interface, and an export * from "@codex-music/audio-contracts" re-export. That last line matters more than it looks: you never need to add @codex-music/audio-contracts to your package.json. ParamSchema, ParamDescriptor, AudioKernel, KernelIO, AudioEvent, GraphPlan, AudioEngine, EngineBackend, plus bridgeParams, normalize, denormalize and sortEvents all come through the SDK unchanged. One import specifier for the whole authoring surface.

Shared browser audio and MIDI session

AudioSessionController normalizes the browser-facing controls shared by the main website and local plugin workbench. It owns input and output selection, start and stop state, Web MIDI discovery, MIDI input binding, MIDI output, sample and microphone sources, monitor recording, analyser access, status, and errors.

import { AudioSessionController } from "@codex-music/audio-sdk";

const session = new AudioSessionController({
  inputs: [
    { id: "mic", label: "Microphone", kind: "microphone" },
    { id: "keys", label: "Computer keyboard", kind: "keyboard" },
  ],
  outputs: [{ id: "speakers", label: "System output", kind: "speakers" }],
});

await session.start();
session.noteOn(60, 0.8);
session.noteOff(60);

The built-in runtime is appropriate for the local workbench. A website or other host that already owns an engine supplies runtime.start, stop, selectInput, selectOutput, noteOn, and noteOff. The state machine and UI stay the same while the audio implementation changes.

@codex-music/site-ui/components/audio-session-controls is the shared shadcn view for this controller. Updating its labels, device menus, error handling, or accessibility updates both the website and runner.

createAudioEngine

import { createAudioEngine } from "@codex-music/audio-sdk";
import type { AudioEngineOptions } from "@codex-music/audio-sdk";

const options: AudioEngineOptions = {
  backend: "shared-wasm",
  sampleRate: 48000,
  graph: {
    schema: MY_PEDAL_SCHEMA,
    nodes: [{ kind: "scfx_", params: {} }],
  },
};

const engine = createAudioEngine(options);

AudioEngineOptions has exactly three fields: backend (required), sampleRate (optional — the transport picks a device rate otherwise), and graph, a GraphPlan pairing your ParamSchema with an ordered node list.

The returned AudioEngine is host-neutral: send(events) for a batch of frame-offset AudioEvents, setParam(path, value) with the dotted path from your schema, noteOn / noteOff, render(frames) for an offline pull, and start() / close() for the real-time lifecycle. Every backend implements the same interface, which is what lets a UI component be written once.

Backend selection

EngineBackend is a closed union of five values, and you pass one explicitly:

BackendTransport
"tone"Tone.js reference path, for A/B against the legacy graph.
"shared-wasm"AudioWorklet + the ScriptC WASM reactor. The browser default.
"hybrid"Web Audio nodes with kernel inserts where they exist.
"native"Inside the plugin shell — bridged to the compiled engine.
"offline-native"Node/CI renders through @codex-music/audio-native.

There is no "auto". Selection is a decision your app makes, usually one ternary against isNativeHost(), because "which transport am I on" is information your UI often wants anyway — for a badge, for a preset-export button, for a hidden dev panel.

isNativeHost and the codexHost bridge

import { createAudioEngine, isNativeHost } from "@codex-music/audio-sdk";

const engine = createAudioEngine({
  backend: isNativeHost() ? "native" : "shared-wasm",
  graph: { schema: MY_PEDAL_SCHEMA, nodes: [{ kind: "scfx_", params: {} }] },
});

isNativeHost() is a one-line existence check for window.codexHost. The native plugin shell injects that object into the webview before your bundle loads, so the check is already true by the time your first module evaluates — no race, no await, no readiness event to subscribe to.

The bridge itself is two methods:

export interface CodexHostBridge {
  postMessage(message: unknown): void;
  onMessage(handler: (message: unknown) => void): void;
}

The SDK augments the global Window type with an optional codexHost?: CodexHostBridge, so TypeScript knows about it in any file that imports the SDK. Messages crossing that boundary are the JSON protocol union documented in plugin-shell-kitcodex.ready, codex.setParam, codex.hello, and the rest. You will rarely touch postMessage yourself; the native backend speaks the protocol on your behalf. It is exported because tooling, dev overlays, and tests occasionally need to listen in.

What is real today

createAudioEngine() throws with a message naming the workstream. Everything around it is landed: the contracts are complete, the native shell exists and passes the VST3 validator, and @codex-music/plugin-shell-kit generates its param table and bridge from a real manifest. Write your UI against this API now and it will keep compiling when the transports arrive — that is the point of shipping the types first. See How it works for the pipeline and audio-contracts for the type reference.