Soundorv0.13.0

Parameters and host automation

Typed parameter values, subscriptions, gestures, and the audio boundary.

Parameters are the values the plugin exposes to its host: a gain, a bypass switch, a mode selector. Declare them in soundor.config.ts, use the generated soundor:parameters module from the UI, and connect them to audio processing in each runtime.

Declare parameters

The config accepts four parameter types:

parameters: [
  { type: 'float', id: 'gain', label: 'Gain', min: 0, max: 1, default: 0.5 },
  { type: 'int', id: 'voices', label: 'Voices', min: 1, max: 16, default: 4 },
  { type: 'bool', id: 'bypass', label: 'Bypass', default: false },
  {
    type: 'enum',
    id: 'mode',
    label: 'Mode',
    values: ['clean', 'warm'],
    default: 'clean',
  },
],

Float and integer parameters may also have a display unit. IDs are unique within the plugin. Generated declarations expose the corresponding value types: numbers, booleans, and enum string unions.

Read, write, and subscribe

import { parameters } from 'soundor:parameters';

const gain = parameters.gain.get();
parameters.gain.set(0.75);
const unsubscribe = parameters.gain.subscribe((value) => {
  console.log('Gain changed:', value);
});

// Call when this subscription is no longer needed.
unsubscribe();

info exposes the declaration, including the label, range where applicable, and default. Numeric writes are clamped to their range; integer values are rounded. Wrong value types fail, and enum writes must be declared choices. Subscribers hear changes rather than repeated writes of the same value.

In React, use useParameter(parameters.gain) to read and subscribe with the component's lifecycle. Keep the parameter as the value source so host automation, preset restoration, and other views can update the displayed value.

One parameter of each type, in React:

import { Pressable, Text, View, useParameter } from '@soundor/react';
import type { ReactNode } from 'react';
import { parameters } from 'soundor:parameters';

function Button({
  label,
  selected = false,
  onPress,
}: {
  label: string;
  selected?: boolean;
  onPress: () => void;
}) {
  return (
    <Pressable
      onPress={onPress}
      accessibilityState={{ selected }}
      style={({ pressed }) => ({
        minWidth: 32,
        alignItems: 'center',
        paddingVertical: 4,
        paddingHorizontal: 10,
        borderRadius: 6,
        backgroundColor: selected || pressed ? '#3a7bfd' : '#2a2c33',
      })}
    >
      <Text style={{ color: 'white' }}>{label}</Text>
    </Pressable>
  );
}

function Row({ label, children }: { label: string; children: ReactNode }) {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
      <Text style={{ width: 64, color: '#8b8f98' }}>{label}</Text>
      {children}
    </View>
  );
}

// One parameter of each type. Values are plain: a number in range (an int
// is rounded), a boolean, or one of an enum's values.
export default function Parameters() {
  const gain = useParameter(parameters.gain);
  const voices = useParameter(parameters.voices);
  const bypass = useParameter(parameters.bypass);
  const mode = useParameter(parameters.mode);

  return (
    <View style={{ gap: 10 }}>
      <Row label="Gain">
        <Button label="−" onPress={() => parameters.gain.set(gain - 0.1)} />
        <Text style={{ width: 40, color: '#e6e7ea' }}>{gain.toFixed(2)}</Text>
        <Button label="+" onPress={() => parameters.gain.set(gain + 0.1)} />
      </Row>
      <Row label="Voices">
        <Button label="−" onPress={() => parameters.voices.set(voices - 1)} />
        <Text style={{ width: 40, color: '#e6e7ea' }}>{voices}</Text>
        <Button label="+" onPress={() => parameters.voices.set(voices + 1)} />
      </Row>
      <Row label="Bypass">
        <Button
          label={bypass ? 'On' : 'Off'}
          selected={bypass}
          onPress={() => parameters.bypass.set(!bypass)}
        />
      </Row>
      <Row label="Mode">
        {parameters.mode.info.values.map((value) => (
          <Button
            key={value}
            label={value}
            selected={value === mode}
            onPress={() => parameters.mode.set(value)}
          />
        ))}
      </Row>
    </View>
  );
}
parameters.tsxSource

Group a user edit into a gesture

A gesture tells the host when a user starts and finishes changing a parameter. For a discrete action, bracket the write:

const gain = parameters.gain;
gain.beginGesture();
try {
  gain.set(gain.info.default);
} finally {
  gain.endGesture();
}

For a drag, begin on pointer down, set values as the pointer moves, and end when the interaction finishes or is cancelled. Balance gestures across every exit path. Gestures can nest; only the outermost boundaries reach the host. In the JUCE runtime these map to JUCE's parameter change gestures, allowing a DAW to record the interaction as automation.

Connect parameters to audio

In a JUCE plugin the parameter API is backed by the processor's AudioProcessorValueTreeState. Host automation, preset restoration, and UI changes use that same state. Parameter listeners only set lock-free flags; JavaScript subscribers run later on the message thread.

Your processor reads the parameter state and applies it to audio buffers. JavaScript does not process the audio or run on the audio thread. The basic example shows the C++ gain implementation alongside its browser implementation.

In the browser the UI, audio setup, and host controls share one parameter store. Subscribe from runtimes/web/src/audio.ts to update a Web Audio node, and return a cleanup function that unsubscribes and disconnects the graph. See Web Audio setup.

Test parameter behavior

The test runtime implements parameter validation, changes, and nested gestures. Use the testing helpers to simulate host-side values and inspect UI behavior. A component test can verify that a fader displays an automated change or that Reset restores the declared default. Audio processing and real DAW automation require their own integration checks.

On this page