pnpm add @audio-ui/engine
import { DeviceDefinitionError, defineDevice, createFeed, createWorkletStage, FEED_RELEASE_MS, /* … */ } from "@audio-ui/engine/device";| Export | Kind | What it is |
|---|---|---|
ChoiceOption | interface | One entry of a choice: the value that goes in the state and the label a panel shows. |
ChoiceParameter | interface | A pick from a short list: a waveform, a filter type, a mode. |
Device | interface | A defined device: its parameters, and everything that reads or changes a state of it, with no sound and no view. |
DeviceApplied | interface | What apply returns: the new state, the changes made, and the changes refused with their reason. |
DeviceBinding | interface | What a host gives a device to drive: an engine, a view, a store. |
DeviceChange | interface | One parameter apply changed. |
DeviceContext | interface | What the host knows and the device does not: the tempo a time parameter that follows it is read against. |
DeviceDefinition | interface | What defineDevice takes: the device's id, name, description and parameters, and whether it is an instrument, takes a sidechain or adds latency. |
DeviceDefinitionError | class | Raised when a device is defined with parameters that cannot work. |
DeviceDescription | interface | A device as an agent or a catalog reads it. |
DeviceReadings | interface | What a device reports about what it is doing right now, for a meter. |
DeviceRejection | interface | One parameter apply did not change, and why. |
DeviceState | type | The state of a device: each parameter's id to its value. |
defineDevice | function | Defines a device from its parameters. |
NumberParameter | interface | A continuous parameter: a number in a range, on a scale, in steps. |
ParameterDescription | interface | A parameter as an agent or a catalog reads it: JSON-safe, with no function in it. |
ParameterDescriptor | type | A parameter of a device: a number, a toggle or a choice. |
ParameterValue | type | What a parameter holds: a number, a boolean or the value of a choice. |
ToggleParameter | interface | An on or off parameter: a bypass, a mode, an auto switch. |
createFeed | function | Feeds a worklet only while the path it plays is heard: a worklet fed nothing costs nothing. |
createWorkletStage | function | A stage of an engine that runs a worklet: the sound passes clean while the worklet loads, then fades over to it. |
EffectEngine | interface | The engine of an effect on the Web Audio API: the binding of its state (update, dispose, readings) between an input and an output. |
EngineFactory | type | What every create<Name>Engine is: an engine on a context, for the state it starts with and the host's tempo. |
FEED_RELEASE_MS | const | How long after a path stops a worklet is still fed, in ms: its fade is over by then. |
Feed | interface | Connections made while a path plays and taken away once it has faded out. |
FeedConnection | type | A connection a feed makes: from into to, at an output and an input of their own if given. |
InstrumentEngine | interface | The engine of an instrument: one voice, notes in, sound out. |
NoiseColor | type | The colors of noise a buffer is made in, as @audio/synth-noise makes them. |
noiseBuffer | function | Two seconds of noise on a context, from @audio/synth-noise (seeded, its peak at 0.9), made once per color and context and shared by everything that plays it: a synth's noise source, a snare's rattle, the pink noise an equalizer is heard through. |
WorkletStage | interface | A worklet that takes over from the clean sound once it is in the graph. |
InstrumentBinding | interface | What a host gives an instrument to play: a DeviceBinding that also takes notes. |
NoteTiming | interface | When a note starts or ends, and which note it is. |
playNote | function | Plays a note for seconds on an instrument: noteOn now, noteOff when the time is up. |
SidechainBinding | interface | What a host gives a device that takes a key signal: a DeviceBinding whose engine has a second input, sidechain, that the key is connected to. |
measureLatency | function | How long an engine delays a signal, in milliseconds: where what comes out of an impulse lines up with it. |
measureResponse | function | How far an engine's frequency response is from curve, the gain in dB the device draws at each frequency. |
ResponseMeasurement | interface | What a measurement of a frequency response found. |
DeviceTool | interface | A tool a model can call to drive a device: agent-agnostic, with no SDK in it. |
DeviceToolResult | interface | What a tool's run returns: the state after the call, a summary for the model, and the note to play for a <id>_play call. |
deviceTools | function | The tools that drive a device, derived from its parameters: <id>_set takes any of its parameters by id, <id>_reset puts some or all back to their defaults. |
JsonSchema | interface | The subset of JSON Schema the tools use: what OpenAI, Anthropic, MCP and the AI SDK all accept as an input schema. |
NOTE_DURATION | const | How long a note lasts when a call does not say, and the shortest and longest it may ask for, in seconds. |
Usage
defineDevice describes an effect or an instrument by its parameters, with no sound and no view in it. What comes back holds everything that reads or changes a state of it:
import { defineDevice } from "@audio-ui/engine/device";
export const driveDevice = defineDevice({
id: "drive",
name: "Drive",
description: "Soft clipping with a tone control.",
parameters: [
{ id: "amount", label: "Amount", min: 0, max: 100, default: 25, unit: "%" },
{ id: "tone", label: "Tone", min: 200, max: 20000, default: 4000, scale: "log", unit: "Hz" },
{ id: "bypass", label: "Bypass", kind: "toggle", default: false },
],
});
const state = driveDevice.defaults(); // { amount: 25, tone: 4000, bypass: false }A parameter is a number in a range (on a scale, in steps, with a unit), a "toggle", or a "choice" from a list of options. A definition that cannot work, such as a default out of its range, a log scale that starts at 0 or a choice whose default is not one of its options, throws DeviceDefinitionError when the module loads, not when a user turns a knob.
Changing a state
A state is a plain object, so it is stored, sent and undone like any other. set(state, id, value) changes one parameter, clamped and snapped; apply(state, changes) changes several and says what happened to each:
const { state: next, changes, rejected } = driveDevice.apply(state, {
amount: 140,
tone: "bright",
gain: 3,
});
// changes: [{ id: "amount", from: 25, to: 100, adjusted: true }]
// rejected: [{ id: "tone", reason: "not a number" }, { id: "gain", reason: "unknown parameter" }]A value out of range is brought in and marked adjusted; a value of the wrong type, or an id the device does not have, is rejected and the parameter is left as it was. sanitize makes a valid state of anything, such as a preset saved by an older version. normalize and denormalize place a value along its travel from 0 to 1, on its scale, which is what a knob draws, and format prints it with its unit, "4000 Hz".
Bindings
A device makes no sound. An engine does, and it implements a binding: update(state, context) whenever the state changes, dispose() when it goes, and readings() for a meter, such as the gain reduction of a compressor. An InstrumentBinding also takes noteOn and noteOff, and a SidechainBinding has a second input for a key signal. playNote(instrument, note, seconds) plays a note on one and lets it go when the time is up.
latency(state, sampleRate) is how long the device delays the sound, for a host to compensate; describe() is the device as JSON, what a catalog lists.
Tools for an agent
deviceTools(device) derives the tools a model calls to drive it: <id>_set takes any parameters by id, <id>_reset puts some or all back, and an instrument played by notes also gets <id>_play. Each has a JSON Schema input and a run(state, input) that never throws on a model's input:
import { deviceTools } from "@audio-ui/engine/device";
const [set] = deviceTools(driveDevice);
set.run(state, { amount: 60 });
// { state: { amount: 60, tone: 4000, bypass: false }, summary: "Amount: 25 % to 60 %" }The schema is the subset that OpenAI, Anthropic, MCP and the AI SDK all accept, and the ranges it gives are the ones the device enforces.
Engines
What a device's engine is, and the pieces an engine is made of, each the one place its behavior lives.
Engine contracts
Every create<Name>Engine is an EngineFactory: a context, the state to start from and the host's context (its tempo) in, an engine out. An effect's engine is an EffectEngine<State> (an input, an output, update, dispose, and ready, sidechain or readings where it has them); an instrument's is an InstrumentEngine<State, Note>, with noteOn and noteOff and no input. Both are the core's bindings with the nodes to connect, so a host that keeps its own engine implements the same update and dispose.
createWorkletStage
A worklet that takes over from the clean sound once it loads, with a fade, and builds nothing once the engine is disposed.
import { createWorkletStage } from "@audio-ui/engine/device";
const input = ctx.createGain();
const output = ctx.createGain();
let latest = initial;
const stage = createWorkletStage(ctx, {
input,
make: () => new AudioWorkletNode(ctx, "my-processor", { parameterData: settingsOf(latest) }),
ramp: 0.02,
source: MY_PROCESSOR_SOURCE,
});
stage.connect(output);
// update: stage.node?.parameters.get("drive")?.setTargetAtTime(...); dispose: stage.dispose()createFeed
Keeps a worklet fed only while the path it plays is heard: connected at once on set(true), taken away 120 ms after set(false), once the path has faded, unless wanted again before.
import { createFeed } from "@audio-ui/engine/device";
const feed = createFeed([
[input, model, 0, 0],
[sidechain, model, 0, 1], // a key on the worklet's second input
]);
feed.set(wet > 0); // on every update
feed.dispose(); // in the engine's disposenoiseBuffer
Two seconds of noise from @audio/synth-noise, made once per color and context and shared by everything that plays it: noiseBuffer(ctx) for white, noiseBuffer(ctx, "pink") for pink.
Measuring an engine
A host that brings its own engine can check it against what the panel draws. device-measure renders an engine in an offline audio context and compares it with the curve of the device. It works for any device whose curve is a frequency response, such as the equalizer, the filter and the channel EQ.
import { equalizerCurve, equalizerDevice } from "@/components/audio-ui/devices/equalizer/device";
import { measureLatency, measureResponse } from "@audio-ui/engine/device";
const state = equalizerDevice.defaults();
// Your factory: (ctx, state) => { input, output, dispose }.
const { worstDb, at } = await measureResponse(createMyEngine, state, equalizerCurve(state));
// worstDb: how far your engine is from the panel, in dB, and where (at, in Hz).
const latencyMs = await measureLatency(createMyEngine, state);
// What a host reports to compensate it.It is a measurement, not a certification: the curve is the device's own and the tolerance is yours.
Next
- Devices: the four pieces a device of the registry comes in, and what every device shares.
device-engine: the engine contracts and the pieces engines are made of.