Lab

device

A device as a description, from @audio-ui/engine: defineDevice and its parameters, a state and apply with what each change did, the bindings an engine implements, and the tools an agent drives it with.

pnpm add @audio-ui/engine
import { DeviceDefinitionError, defineDevice, createFeed, createWorkletStage, FEED_RELEASE_MS, /* … */ } from "@audio-ui/engine/device";
Used byarrangement, arrangement-engine, dawproject, device-library, drum-rack, instrument-rack, polyphony, rack, use-device-engines, use-device-loop, use-noise-through-eq, use-polyphony, voice-pool
audio.js@audio/measure-latency, @audio/measure-response, @audio/synth-noise
ExportKindWhat it is
ChoiceOptioninterfaceOne entry of a choice: the value that goes in the state and the label a panel shows.
ChoiceParameterinterfaceA pick from a short list: a waveform, a filter type, a mode.
DeviceinterfaceA defined device: its parameters, and everything that reads or changes a state of it, with no sound and no view.
DeviceAppliedinterfaceWhat apply returns: the new state, the changes made, and the changes refused with their reason.
DeviceBindinginterfaceWhat a host gives a device to drive: an engine, a view, a store.
DeviceChangeinterfaceOne parameter apply changed.
DeviceContextinterfaceWhat the host knows and the device does not: the tempo a time parameter that follows it is read against.
DeviceDefinitioninterfaceWhat defineDevice takes: the device's id, name, description and parameters, and whether it is an instrument, takes a sidechain or adds latency.
DeviceDefinitionErrorclassRaised when a device is defined with parameters that cannot work.
DeviceDescriptioninterfaceA device as an agent or a catalog reads it.
DeviceReadingsinterfaceWhat a device reports about what it is doing right now, for a meter.
DeviceRejectioninterfaceOne parameter apply did not change, and why.
DeviceStatetypeThe state of a device: each parameter's id to its value.
defineDevicefunctionDefines a device from its parameters.
NumberParameterinterfaceA continuous parameter: a number in a range, on a scale, in steps.
ParameterDescriptioninterfaceA parameter as an agent or a catalog reads it: JSON-safe, with no function in it.
ParameterDescriptortypeA parameter of a device: a number, a toggle or a choice.
ParameterValuetypeWhat a parameter holds: a number, a boolean or the value of a choice.
ToggleParameterinterfaceAn on or off parameter: a bypass, a mode, an auto switch.
createFeedfunctionFeeds a worklet only while the path it plays is heard: a worklet fed nothing costs nothing.
createWorkletStagefunctionA stage of an engine that runs a worklet: the sound passes clean while the worklet loads, then fades over to it.
EffectEngineinterfaceThe engine of an effect on the Web Audio API: the binding of its state (update, dispose, readings) between an input and an output.
EngineFactorytypeWhat every create<Name>Engine is: an engine on a context, for the state it starts with and the host's tempo.
FEED_RELEASE_MSconstHow long after a path stops a worklet is still fed, in ms: its fade is over by then.
FeedinterfaceConnections made while a path plays and taken away once it has faded out.
FeedConnectiontypeA connection a feed makes: from into to, at an output and an input of their own if given.
InstrumentEngineinterfaceThe engine of an instrument: one voice, notes in, sound out.
NoiseColortypeThe colors of noise a buffer is made in, as @audio/synth-noise makes them.
noiseBufferfunctionTwo 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.
WorkletStageinterfaceA worklet that takes over from the clean sound once it is in the graph.
InstrumentBindinginterfaceWhat a host gives an instrument to play: a DeviceBinding that also takes notes.
NoteTiminginterfaceWhen a note starts or ends, and which note it is.
playNotefunctionPlays a note for seconds on an instrument: noteOn now, noteOff when the time is up.
SidechainBindinginterfaceWhat 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.
measureLatencyfunctionHow long an engine delays a signal, in milliseconds: where what comes out of an impulse lines up with it.
measureResponsefunctionHow far an engine's frequency response is from curve, the gain in dB the device draws at each frequency.
ResponseMeasurementinterfaceWhat a measurement of a frequency response found.
DeviceToolinterfaceA tool a model can call to drive a device: agent-agnostic, with no SDK in it.
DeviceToolResultinterfaceWhat 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.
deviceToolsfunctionThe 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.
JsonSchemainterfaceThe subset of JSON Schema the tools use: what OpenAI, Anthropic, MCP and the AI SDK all accept as an input schema.
NOTE_DURATIONconstHow 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 dispose

noiseBuffer

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.
Built by Ouest Labs, the source code is available on GitHub.
RSS