Components
Time and sequencing
Blocks
A chorus as a device you can plug in: a rate, a depth, a delay and a mix as a state, a panel that draws the sweep of the delay, a Web Audio engine and the tools an agent drives it with.
Audio UI
"use client";
import { useState } from "react";Thickens the sound with a short delay that a slow oscillator sweeps up and down. The panel draws the delay time over two seconds as the oscillator moves it. 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 { ChorusPanel } from "@/components/audio-ui/devices/chorus/panel";
import { chorusDevice } from "@/components/audio-ui/devices/chorus/device";
const [state, setState] = useState(() => chorusDevice.defaults());
<ChorusPanel 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. chorusSweep(state) is the delay time at each moment, which is what the panel draws.
import { createChorusEngine } from "@/components/audio-ui/devices/chorus/engine";
const engine = createChorusEngine(ctx, state);
source.connect(engine.input);
engine.output.connect(ctx.destination);
engine.update(nextState); // parameters ramp: no click
engine.dispose();The oscillator's output, scaled by the sweep, is added to the delay time. The sweep is kept inside the delay, so the time never goes negative.
Switch Sync on and the rate is a note value instead of hertz: one cycle of the oscillator for each 1/4, 1/8t (a triplet), 1/2d (dotted) and so on, from 1/32 to a whole note, read against the host's tempo. 1/4 at 120 BPM is 2 Hz, and the rate is held between a cycle in 50 seconds and 40 Hz. The tempo is the host's: give it to the engine with the first state or with any update.
const engine = createChorusEngine(ctx, state, { tempo }); // { bpm: 100, beatsPerBar: 4, beatUnit: 4 }
engine.update(nextState, { tempo: { ...tempo, bpm: 120 } }); // the rate follows
engine.update(otherState); // no context: the last tempo holdsThe panel takes the same tempo as a prop, tempo, and draws the oscillator at the rate it really has. With Sync on the rate knob is drawn off, with it off the note list is. The tempo does not come from an agent's tool call: audioui_chorus_set can switch Sync on and pick a note, and the host decides the tempo it is read against.
import { deviceTools } from "@audio-ui/core/device";
import { chorusDevice } from "@/components/audio-ui/devices/chorus/device";
const [set, reset] = deviceTools(chorusDevice);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.