Components
Time and sequencing
Blocks
A delay as a device you can plug in: a time, a feedback, a tone and a mix as a state, a panel that draws the echoes with a handle, a Web Audio engine and the tools an agent drives it with.
Audio UI
"use client";
import { useState } from "react";Repeats the sound. The panel draws the echoes the state makes, one line each, fainter as they fade: drag the handle on the first one across for the time and up for the feedback. It is a device like the compressor: a state, a panel that draws it, an engine that plays it and tools an agent calls, each its own file.
import { DelayPanel } from "@/components/audio-ui/devices/delay/panel";
import { delayDevice } from "@/components/audio-ui/devices/delay/device";
const [state, setState] = useState(() => delayDevice.defaults());
<DelayPanel onValueChange={setState} value={state} />;The state is a plain object keyed by parameter id, so a host stores it, saves it with its project and sends it where it likes. delayTaps(state, windowMs) returns the echoes a state makes, which is what the panel draws.
import { createDelayEngine } from "@/components/audio-ui/devices/delay/engine";
const engine = createDelayEngine(ctx, state);
source.connect(engine.input);
engine.output.connect(ctx.destination);
engine.update(nextState); // parameters ramp: no click
engine.dispose();The repeats go round a low-pass tone filter, so each is darker than the one before. A change of time glides slowly, so the echo bends like a tape instead of clicking, and the feedback stops short of 95 % so it cannot run away.
Switch Sync on and the time is a note value instead of milliseconds: 1/8, a triplet 1/8t, a dotted 1/8d, from 1/32 to a whole note, read against the host's tempo. The tempo is the host's, not the delay's: give it to the engine with the first state or with any update.
const engine = createDelayEngine(ctx, state, { tempo }); // { bpm: 100, beatsPerBar: 4, beatUnit: 4 }
engine.update(nextState, { tempo: { ...tempo, bpm: 120 } }); // the echo follows
engine.update(otherState); // no context: the last tempo holdsThe panel takes the same tempo as a prop, tempo, and draws the echoes where the notes fall. With Sync on, the handle lands on the nearest note value and the time knob is drawn off; with it off, the note list is. A note longer than the delay can hold (two seconds) is held to that. The tempo does not come from an agent's tool call: delay_set can switch Sync on and pick a note, and the host decides the tempo it is read against. The 1/8d the block starts on is a dotted eighth at the loop's 100 BPM, 450 ms.
import { deviceTools } from "@audio-ui/core/device";
import { delayDevice } from "@/components/audio-ui/devices/delay/device";
const [set, reset] = deviceTools(delayDevice);set and reset change the state, with the ranges the device enforces: a value out of range is brought in and reported, a wrong one is refused by name, and a call never throws.
Every control is a native input: the knobs are sliders named after their parameter, with the value and unit as the text value, the switch is a toggle button, and everything takes the keyboard.