VSTDOCSv0.0.34

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)

TSX
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.4995

Always 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:

TSX
<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)), matching AudioParameterChoice.

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:

ui/src/main.tsx — an entire plugin UI
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:

TSX
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:

PluginProcessor.cpp — the layout JS sees
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())
PluginEditor.h / .cpp
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

MESSAGEPAYLOADDIRECTION
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