DOCS / AUDIO & NATIVE
Audio parameters
Parameter binding is two-way and automation-safe. C++ owns the truth (the AudioProcessorValueTreeState); JS reads, sets, and subscribes through vsreact::ParameterBridge. All values are normalized 0..1.
useParameter(id)
const gain = useParameter("gain");
gain.value // 0..1, live — updates when the DAW automates it
gain.text // host-formatted display text, e.g. "-3.2 dB"
gain.name // parameter name from the APVTS
gain.label // unit label, e.g. "dB"
gain.defaultValue // the host's normalized default — double-click reset target
gain.begin(); // start an automation gesture
gain.set(0.75); // ...as many times as you like (drag)
gain.end(); // end the gesture — hosts record clean automation
// Since 0.0.28 the handle also carries the natural range, straight from the
// C++ NormalisableRange — never mirror an APVTS range in TS again:
gain.min; gain.max; gain.interval; gain.skew
import { normalizedToNatural, naturalToNormalized } from "@vsreact/core";
normalizedToNatural(delay.value, delay) // 0.346 → 347 (ms), honoring skew
naturalToNormalized(500, delay) // 500 ms → 0.4995Always bracket drags with begin()/end() — that is what makes host automation recording and touch/latch modes behave. The built-in controls do it for you.
For custom drag surfaces — e.g. an invisible hit zone over reference art — useParamGestures(handle, opts?) (since 0.0.27) returns that whole handler set headlessly: vertical drag inside a begin/end gesture, double-click reset, wheel nudge. Spread it onto any View; options tune sensitivity, wheelStep (0 disables) and the resetTo target.
Ready-made controls
Every built-in control has a Param* twin that wraps useParameter with correct begin/set/end gestures and takes its label from the parameter name:
<ParamKnob paramId="gain" size={88} />
<ParamSlider paramId="mix" width={220} />
<ParamSlider paramId="level" vertical height={140} /> // fader
<ParamToggle paramId="bypass" /> // on = value ≥ 0.5
<ParamXYPad paramX="cutoff" paramY="resonance" /> // two params, one drag
<ParamSegmented paramId="shape" options={["SINE", "SAW", "SQR"]} />
<ParamSelect paramId="mode" options={MODES} /> // dropdown for long lists
<ParamNumberBox paramId="freq" /> // host text, drag/wheel/reset
<ParamCheckbox paramId="oversample" /> // bool, settings-panel style
<ParamRadioGroup paramId="os" options={["OFF","2X","4X"]} />
<ParamMacroPad paramX="granulation" paramY="deepFx" /> // the Output-style centerpiece
<ParamHardwareKnob paramId="drive" /> // skeuomorphic cap + notch
<ParamCrossfader paramId="mix" /> // DRY/WET strip- ParamToggle — bool-style parameters; a click writes a full begin/set/end gesture so hosts record it cleanly.
- ParamXYPad — drives two parameters at once; both gestures open on drag-start and close on release.
- ParamSegmented — choice-style parameters; the normalized value maps to an option index (
index / (count − 1)), matchingAudioParameterChoice.
The unbound versions (Knob, Slider, Toggle, XYPad, Segmented) are exported too, for values that are not host parameters — UI zoom, tab selection, list filters.
The one-line editor
useParameterList() enumerates every parameter in the APVTS (via param:list), which makes a complete, working editor exactly one line:
import { render, GenericEditor } from "@vsreact/core";
render(<GenericEditor />);<GenericEditor columns? size? trackColor? valueColor? /> lays out one ParamKnob per parameter in rows — the fastest path from a processor to a usable UI, and a solid starting point you can replace control by control. For custom generic UIs, build on the hook directly:
const params = useParameterList();
// [{ id, name, label, value, text }, …] — one entry per APVTS parameter
return params.map((p) => <ParamSlider key={p.id} paramId={p.id} />);C++ wiring
Everything above assumes an APVTS. The parameter definition is where useParameter().text, its label, the value scale, and double-click defaults all come from — ranges, skew, and string formatting live in the ParameterLayout, not in JS:
static juce::AudioProcessorValueTreeState::ParameterLayout createLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
// 1–1000 ms with finer travel at the low end. NOTE: useParameter().value
// is the NORMALIZED 0–1 including this skew — map through the range if
// your UI wants display-linear knob angles.
auto ms = juce::NormalisableRange<float> (1.0f, 1000.0f, 0.1f);
ms.setSkewForCentre (200.0f);
layout.add (std::make_unique<juce::AudioParameterFloat> (
juce::ParameterID { "time", 1 }, "Time", ms, 320.0f,
juce::AudioParameterFloatAttributes()
.withLabel ("ms") // useParameter().label
.withStringFromValueFunction ( // .text
[] (float v, int) { return juce::String (juce::roundToInt (v)) + " ms"; })));
layout.add (std::make_unique<juce::AudioParameterBool> (
juce::ParameterID { "bypass", 1 }, "Bypass", false));
return layout;
}
// in the processor's constructor:
// state (*this, nullptr, "PARAMS", createLayout())vsreact::ParameterBridge bridge { processor.apvts };
// 1. chain param:* calls first in onNativeCall
options.onNativeCall = [this] (const juce::String& name, const juce::var& args) -> juce::var {
if (auto handled = bridge.handleNativeCall (name, args))
return *handled;
return {};
};
// 2. attach the RootView so DAW-side changes reach useParameter
bridge.attach (*root);DAW-side changes (automation, preset loads, other UI) are coalesced on the message thread — the bridge is a juce::AsyncUpdater, so a burst of automation becomes one batched push per message-loop tick — and delivered to JS as param events. The useParameter hook subscribes for you.
The wire protocol
| MESSAGE | PAYLOAD | DIRECTION |
|---|---|---|
param:list | {} → [{id, name, label, value, text}, …] | JS → C++ (enumeration) |
param:get | {id} → {value, text, name, label} | JS → C++ |
param:set | {id, value} | JS → C++ |
param:begin / param:end | {id} | JS → C++ (gesture brackets) |
param event | {id, value, text} | C++ → JS |