Skip to main content

Properties

Properties are real-time parameters that can be dynamically changed during audio processing. They allow your custom node to be controlled from the application layer, making your node interactive and configurable at runtime.

Why Use Properties?

Properties enable:

  • Real-time control of audio parameters (gain, frequency, mix, etc.)
  • UI integration - properties can be bound to sliders, knobs, and other controls
  • Automation - properties can be automated over time
  • Serialization - properties can be saved and restored with audio graphs
  • Introspection - applications can discover available parameters and their ranges

Registering Properties

Properties are registered in your node's constructor using the registerProperty() method. Each property requires a unique name and a metadata map that describes the property.

Basic Example

Here's a complete example of registering a gain property with getter and setter:

class GainNode : public SingleBusAudioProcessorNode {
public:
GainNode() : gain(1.0f) {
type = "GainNode";

registerProperty(
"gain",
{
{ PROPERTY_FIELD_DESCRIPTION, "The gain of the audio signal. The range is [0, 1]." },
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_FLOAT },
{ PROPERTY_FIELD_MIN_VALUE, 0.0f },
{ PROPERTY_FIELD_MAX_VALUE, 1.0f },
{ PROPERTY_FIELD_DEFAULT_VALUE, gain.load() },
{ PROPERTY_FIELD_GETTER, std::function([this]() -> Result<SBAny> {
return makeSuccess<SBAny>(getGain());
}) },
{ PROPERTY_FIELD_SETTER, std::function([this](const SBAny& value) -> Result<void> {
const auto newGain = SBAny::convert<float>(value);
setGain(newGain);
return makeSuccess();
}) }
}
);
}

float getGain() const { return gain.load(); }
void setGain(float newValue) { gain.store(newValue); }

private:
std::atomic<float> gain;
};

Property Metadata Fields

Properties are described using a metadata map with the following fields:

Required Fields

FieldConstantDescription
TypePROPERTY_FIELD_TYPEData type: PROPERTY_TYPE_FLOAT, PROPERTY_TYPE_INT, PROPERTY_TYPE_BOOLEAN, PROPERTY_TYPE_STRING
GetterPROPERTY_FIELD_GETTERFunction that returns the current property value as Result<SBAny>
SetterPROPERTY_FIELD_SETTERFunction that sets a new property value, takes const SBAny&, returns Result<void>

Optional Fields

FieldConstantDescription
DescriptionPROPERTY_FIELD_DESCRIPTIONHuman-readable description of the property
Default ValuePROPERTY_FIELD_DEFAULT_VALUEInitial/default value for the property
Min ValuePROPERTY_FIELD_MIN_VALUEMinimum allowed value (for numeric types)
Max ValuePROPERTY_FIELD_MAX_VALUEMaximum allowed value (for numeric types)
UnitPROPERTY_FIELD_UNITUnit of measurement (see Units section below)
OptionsPROPERTY_FIELD_OPTIONSList of valid options (for string enumerations)
Read OnlyPROPERTY_FIELD_READ_ONLYWhether the property can only be read, not written

Supported Units

Use these constants for the PROPERTY_FIELD_UNIT field:

  • PROPERTY_UNIT_MS - Milliseconds
  • PROPERTY_UNIT_DB - Decibels
  • PROPERTY_UNIT_PERCENT - Percentage (%)
  • PROPERTY_UNIT_HZ - Hertz (frequency)
  • PROPERTY_UNIT_SEMITONES - Semitones (pitch)
  • PROPERTY_UNIT_BPM - Beats per minute
  • PROPERTY_UNIT_SAMPLES - Audio samples
  • PROPERTY_UNIT_FRAMES - Audio frames
  • PROPERTY_UNIT_CHANNELS - Channel count
  • PROPERTY_UNIT_BYTES - Bytes
  • PROPERTY_UNIT_BITS - Bits

Property Types

Float Properties

registerProperty(
"frequency",
{
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_FLOAT },
{ PROPERTY_FIELD_DESCRIPTION, "The frequency of the sine wave in Hz." },
{ PROPERTY_FIELD_UNIT, PROPERTY_UNIT_HZ },
{ PROPERTY_FIELD_MIN_VALUE, 20.0f },
{ PROPERTY_FIELD_MAX_VALUE, 20000.0f },
{ PROPERTY_FIELD_DEFAULT_VALUE, 440.0f },
{ PROPERTY_FIELD_GETTER, std::function([this]() -> Result<SBAny> {
return makeSuccess<SBAny>(getFrequency());
}) },
{ PROPERTY_FIELD_SETTER, std::function([this](const SBAny& value) -> Result<void> {
setFrequency(SBAny::convert<float>(value));
return makeSuccess();
}) }
}
);

Integer Properties

registerProperty(
"bufferSize",
{
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_INT },
{ PROPERTY_FIELD_DESCRIPTION, "Size of the internal buffer." },
{ PROPERTY_FIELD_UNIT, PROPERTY_UNIT_SAMPLES },
{ PROPERTY_FIELD_MIN_VALUE, 64 },
{ PROPERTY_FIELD_MAX_VALUE, 4096 },
{ PROPERTY_FIELD_DEFAULT_VALUE, 512 },
{ PROPERTY_FIELD_GETTER, std::function([this]() -> Result<SBAny> {
return makeSuccess<SBAny>(getBufferSize());
}) },
{ PROPERTY_FIELD_SETTER, std::function([this](const SBAny& value) -> Result<void> {
setBufferSize(SBAny::convert<int>(value));
return makeSuccess();
}) }
}
);

Boolean Properties

registerProperty(
"bypass",
{
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_BOOLEAN },
{ PROPERTY_FIELD_DESCRIPTION, "Bypass the effect processing." },
{ PROPERTY_FIELD_DEFAULT_VALUE, false },
{ PROPERTY_FIELD_GETTER, std::function([this]() -> Result<SBAny> {
return makeSuccess<SBAny>(isBypassed());
}) },
{ PROPERTY_FIELD_SETTER, std::function([this](const SBAny& value) -> Result<void> {
setBypassed(SBAny::convert<bool>(value));
return makeSuccess();
}) }
}
);

Read-Only Properties

For properties that should only be read (e.g., analysis results):

registerProperty(
"level",
{
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_FLOAT },
{ PROPERTY_FIELD_DESCRIPTION, "The current audio level (RMS)." },
{ PROPERTY_FIELD_READ_ONLY, true },
{ PROPERTY_FIELD_GETTER, std::function([this]() -> Result<SBAny> {
return makeSuccess<SBAny>(getLevel());
}) }
}
);

Thread Safety

Properties can be accessed from both the UI thread (via setter) and the audio thread (in process()). You must ensure thread-safe access:

Use Atomics for Simple Types

For simple numeric types, use std::atomic:

private:
std::atomic<float> gain;
std::atomic<bool> bypass;

Use Lock-Free Data Structures

For complex data, use lock-free structures or double-buffering techniques to avoid blocking the audio thread.

Never Use Locks

Do not use mutexes, locks, or other blocking primitives in code that runs in the audio thread. This violates real-time safety and causes audio glitches.

Best Practices

  1. Always use atomics for properties accessed in process()
  2. Validate input in your setter and return errors for invalid values
  3. Provide meaningful descriptions to help users understand parameters
  4. Set appropriate min/max values to prevent invalid configurations
  5. Use standard units for consistency across the SDK
  6. Keep getters/setters lightweight - avoid complex calculations
  7. Document side effects if setting a property affects other properties

Common Patterns

Smoothed Parameter Changes

For parameters that need smooth transitions (like gain or frequency), use a ramping class:

private:
std::atomic<float> targetGain;
std::unique_ptr<math::Ramp> gainRamp;

// In constructor:
gainRamp = std::make_unique<math::Ramp>();
gainRamp->setRampDurationMs(10); // 10ms smooth transition

// In process():
gainRamp->setTargetValue(targetGain.load());
for (uint frame = 0; frame < numFrames; ++frame) {
float currentGain = gainRamp->process(1);
// Apply currentGain to audio...
}

Enum-Like String Properties

For properties with a fixed set of options:

registerProperty(
"filterType",
{
{ PROPERTY_FIELD_TYPE, PROPERTY_TYPE_STRING },
{ PROPERTY_FIELD_DESCRIPTION, "Type of filter to apply." },
{ PROPERTY_FIELD_OPTIONS, std::vector<std::string>{"lowpass", "highpass", "bandpass"} },
{ PROPERTY_FIELD_DEFAULT_VALUE, std::string("lowpass") },
// getter/setter...
}
);

Next Steps

  • Actions - Learn how to expose methods that can be triggered
  • Events - Learn how to emit notifications from your node