05_05_04_parameter_system - Parameter Control & Smoothing¶
Purpose¶
Manages external parameters (frequency, gain, Q, etc.), binds them to internal topology nodes with transformations, and provides automatic smoothing to prevent audio clicks. Essential for making topologies controllable from outside.
Key Concepts¶
External vs Internal Parameters¶
External Parameters: User-facing controls
- cutoff_frequency (Hz)
- resonance (Q factor)
- gain (dB)
Internal Parameters: Node-specific values
- omega0 (normalized frequency)
- Q (as-is)
- linear_gain (linear scale)
Bindings: Map external → internal with transformations
Parameter Transformations¶
Convert between different representations:
| From | To | Use Case |
|---|---|---|
| Hz | ω₀ | Filter cutoff → normalized frequency |
| dB | linear | Gain control → multiplication factor |
| MIDI | Hz | Note number → oscillator frequency |
| ms | samples | Delay time → buffer size |
Parameter Smoothing¶
Problem: Instant parameter changes cause audio clicks/pops
Solution: Ramp smoothly over time (typically 5-20ms)
Value change: 1000 Hz → 2000 Hz
Without smoothing: ────────┐
└──────── (CLICK!)
With smoothing: ────────╱──────── (smooth)
10ms ramp
Smoothing Types: - Linear: Constant rate - Exponential: Fast start, slow end - S-Curve: Smooth acceleration/deceleration
API Overview¶
Basic Usage¶
#include "parameter_manager.hpp"
// Create manager
Topology topology = /* ... */;
ParameterManager params(topology);
// Define external parameter
ExternalParameter cutoff("cutoff_frequency", 1000.0f);
cutoff.min_value = 20.0f;
cutoff.max_value = 20000.0f;
cutoff.unit = "Hz";
cutoff.description = "Filter cutoff frequency";
params.addParameter(cutoff);
// Bind to internal node parameter (with transformation)
params.addBinding("cutoff_frequency", // External param
"lowpass_filter", // Target node
"fc", // Node parameter
transforms::fc_to_omega0); // Hz → ω₀
// Set value (triggers smoothing + propagation)
params.setParameter("cutoff_frequency", 2000.0f);
// Process smoothing per audio sample
for (int i = 0; i < buffer_size; ++i) {
params.processSample(); // Update smoothed values
// ... process audio ...
}
Multiple Bindings¶
One external parameter can control multiple nodes:
// Single "frequency" parameter controls both filters
params.addBinding("frequency", "filter_left", "fc", transforms::fc_to_omega0);
params.addBinding("frequency", "filter_right", "fc", transforms::fc_to_omega0);
params.setParameter("frequency", 1000.0f);
// Both filters update simultaneously
Smoothing Configuration¶
// Configure smoothing
SmoothingConfig config;
config.type = SmoothingType::SCurve; // Smooth acceleration
config.ramp_time_ms = 15.0f; // 15ms transition
config.sample_rate = 48000.0f;
params.setSmoothingConfig("cutoff_frequency", config);
// Disable smoothing for specific parameter
params.enableSmoothing("bypass", false); // Instant change
Parameter Callbacks¶
// Monitor parameter changes
params.onParameterChange([](const ParameterChangeEvent& event) {
std::cout << event.parameter_name << " changed from ";
if (auto* old_val = std::get_if<float>(&event.old_value)) {
std::cout << *old_val;
}
std::cout << " to ";
if (auto* new_val = std::get_if<float>(&event.new_value)) {
std::cout << *new_val;
}
std::cout << " at t=" << event.timestamp << "\n";
});
Snapshot/Restore (Presets)¶
// Save current state
auto preset = params.snapshot();
// ... change parameters ...
// Restore saved state
params.restore(preset);
Common Transformations¶
Frequency Conversions¶
// Hz → normalized frequency (ω₀ = 2π·fc/fs)
params.addBinding("cutoff", "filter", "omega0",
transforms::fc_to_omega0);
// Hz → [0, 1] range
params.addBinding("cutoff", "filter", "normalized_fc",
transforms::fc_to_normalized);
Gain Conversions¶
// dB → linear (10^(dB/20))
params.addBinding("gain_db", "amplifier", "linear_gain",
transforms::db_to_linear);
// linear → dB (20·log₁₀(x))
params.addBinding("gain_linear", "meter", "gain_db",
transforms::linear_to_db);
MIDI Conversions¶
// MIDI note → frequency (f = 440 × 2^((m-69)/12))
params.addBinding("note", "oscillator", "frequency",
transforms::midi_to_freq);
// Example: MIDI 69 (A4) → 440 Hz
params.setParameter("note", 69);
Time Conversions¶
// Milliseconds → samples (samples = ms × sr / 1000)
params.addBinding("delay_time_ms", "delay_line", "delay_samples",
transforms::ms_to_samples);
// Example: 10ms @ 48kHz → 480 samples
params.setParameter("delay_time_ms", 10.0f);
Custom Transformations¶
// Custom transformation function
auto custom_transform = [](ParameterValue val) -> ParameterValue {
if (auto* f = std::get_if<float>(&val)) {
// Custom math: exponential curve
return std::exp(*f) - 1.0f;
}
return val;
};
params.addBinding("control", "node", "param", custom_transform);
Smoothing Strategies¶
Linear Smoothing¶
SmoothingConfig linear;
linear.type = SmoothingType::Linear;
linear.ramp_time_ms = 10.0f;
// Value ramps at constant rate
// Good for: Most parameters
Exponential Smoothing¶
SmoothingConfig exponential;
exponential.type = SmoothingType::Exponential;
exponential.ramp_time_ms = 20.0f;
// Fast start, slow approach to target
// Good for: Envelope-like changes
S-Curve Smoothing¶
SmoothingConfig scurve;
scurve.type = SmoothingType::SCurve;
scurve.ramp_time_ms = 15.0f;
// Smooth acceleration and deceleration
// Good for: Musical parameter changes (cutoff sweeps)
No Smoothing¶
params.enableSmoothing("bypass", false);
// Instant change, no ramp
// Good for: Switches, discrete values
Examples¶
Example 1: Biquad Filter Control¶
// Create topology with biquad filter
auto topology = TopologyBuilder()
.addNode("input", "external_input", NodeType::Source)
.addNode("biquad", "biquad_filter", NodeType::Processing)
.addNode("output", "external_output", NodeType::Sink)
.connect("input", "out", "biquad", "in")
.connect("biquad", "out", "output", "in")
.build();
ParameterManager params(topology);
// Define external parameters
ExternalParameter fc("frequency", 1000.0f);
fc.min_value = 20.0f;
fc.max_value = 20000.0f;
fc.unit = "Hz";
ExternalParameter q("resonance", 0.707f);
q.min_value = 0.5f;
q.max_value = 20.0f;
q.unit = "Q";
params.addParameter(fc);
params.addParameter(q);
// Bind with transformation
params.addBinding("frequency", "biquad", "fc", transforms::fc_to_omega0);
params.addBinding("resonance", "biquad", "Q", transforms::identity);
// Configure smoothing
SmoothingConfig smooth;
smooth.type = SmoothingType::SCurve;
smooth.ramp_time_ms = 10.0f;
params.setSmoothingConfig("frequency", smooth);
params.setSmoothingConfig("resonance", smooth);
// Use in audio loop
while (processing) {
// User changes frequency
if (user_moved_knob) {
params.setParameter("frequency", new_frequency);
}
// Process audio buffer
for (int i = 0; i < buffer_size; ++i) {
params.processSample(); // Smooth parameters
// Audio processing happens here
// Filter uses smoothed fc and Q values
}
}
Example 2: Stereo Gain with dB Control¶
// Stereo gain topology
auto topology = TopologyBuilder()
.addNode("input_L", "external_input", NodeType::Source)
.addNode("input_R", "external_input", NodeType::Source)
.addNode("gain_L", "multiply_scalar", NodeType::Processing)
.addNode("gain_R", "multiply_scalar", NodeType::Processing)
.addNode("output_L", "external_output", NodeType::Sink)
.addNode("output_R", "external_output", NodeType::Sink)
.connect("input_L", "out", "gain_L", "in")
.connect("input_R", "out", "gain_R", "in")
.connect("gain_L", "out", "output_L", "in")
.connect("gain_R", "out", "output_R", "in")
.build();
ParameterManager params(topology);
// dB control parameter
ExternalParameter gain_db("gain", 0.0f); // 0 dB default
gain_db.min_value = -60.0f; // -60 dB min
gain_db.max_value = 12.0f; // +12 dB max
gain_db.unit = "dB";
params.addParameter(gain_db);
// Bind to both channels (dB → linear)
params.addBinding("gain", "gain_L", "scalar", transforms::db_to_linear);
params.addBinding("gain", "gain_R", "scalar", transforms::db_to_linear);
// Set gain
params.setParameter("gain", -6.0f); // -6 dB
// Both channels get linear factor = 10^(-6/20) ≈ 0.501
Example 3: Parameter Presets¶
ParameterManager params(topology);
// Setup parameters...
params.addParameter(/* ... */);
// Create preset 1: Bright
params.setParameter("cutoff", 8000.0f);
params.setParameter("resonance", 2.0f);
params.setParameter("gain", 3.0f);
auto preset_bright = params.snapshot();
// Create preset 2: Dark
params.setParameter("cutoff", 500.0f);
params.setParameter("resonance", 0.7f);
params.setParameter("gain", 0.0f);
auto preset_dark = params.snapshot();
// Switch presets
params.restore(preset_bright); // Instant recall
Example 4: MIDI-Controlled Oscillator¶
// Oscillator topology
auto topology = TopologyBuilder()
.addNode("osc", "sine_oscillator", NodeType::Source)
.addNode("output", "external_output", NodeType::Sink)
.connect("osc", "out", "output", "in")
.build();
ParameterManager params(topology);
// MIDI note parameter
ExternalParameter note("midi_note", 69); // A4
note.min_value = 0;
note.max_value = 127;
note.unit = "MIDI";
params.addParameter(note);
params.addBinding("midi_note", "osc", "frequency", transforms::midi_to_freq);
// Configure fast pitch smoothing
SmoothingConfig pitch_smooth;
pitch_smooth.type = SmoothingType::Exponential;
pitch_smooth.ramp_time_ms = 5.0f; // Fast glide
params.setSmoothingConfig("midi_note", pitch_smooth);
// Play notes
params.setParameter("midi_note", 60); // C4 → 261.63 Hz
params.setParameter("midi_note", 64); // E4 → 329.63 Hz
params.setParameter("midi_note", 67); // G4 → 392.00 Hz
Performance¶
| Operation | Complexity | Notes |
|---|---|---|
| Set parameter | O(B) | B = bindings for that parameter |
| Process sample | O(S) | S = active smoothers |
| Add binding | O(1) | Constant time |
| Snapshot | O(P) | P = total parameters |
| Restore | O(P × B) | All parameters × bindings |
Memory: O(P + B + S) where P=parameters, B=bindings, S=smoothers
Typical performance: - Parameter change: <10 µs - Sample processing: <1 µs per active smoother - Negligible overhead in audio loop
Integration¶
Input Dependencies¶
- Topology from
00_graph_representation(to bind parameters)
Output Consumers¶
- Code generation (
06_code_generation) generates parameter update code - Preset system (
14_preset_system) uses snapshot/restore - UI reads parameter definitions for control generation
Best Practices¶
1. Always Use Smoothing for Audio Parameters¶
// ✅ Good
params.setSmoothingConfig("cutoff", {SmoothingType::SCurve, 10.0f, 48000.0f});
// ❌ Bad (clicks!)
params.enableSmoothing("cutoff", false);
2. Choose Appropriate Ramp Times¶
// Fast-changing (pitch): 5-10ms
params.setSmoothingConfig("frequency", {SmoothingType::Exponential, 5.0f, sr});
// Medium (filter): 10-20ms
params.setSmoothingConfig("cutoff", {SmoothingType::SCurve, 15.0f, sr});
// Slow (gain): 20-50ms
params.setSmoothingConfig("gain", {SmoothingType::Linear, 30.0f, sr});
3. Group Related Parameters¶
// Create parameter group for clarity
void setupFilterParams(ParameterManager& params) {
params.addParameter(ExternalParameter("fc", 1000.0f));
params.addParameter(ExternalParameter("Q", 0.707f));
params.addParameter(ExternalParameter("gain", 0.0f));
params.addBinding("fc", "filter", "cutoff", transforms::fc_to_omega0);
params.addBinding("Q", "filter", "resonance", transforms::identity);
params.addBinding("gain", "filter", "gain_db", transforms::db_to_linear);
}
4. Validate Ranges¶
// Set meaningful ranges
ExternalParameter cutoff("cutoff", 1000.0f);
cutoff.min_value = 20.0f; // Minimum audible
cutoff.max_value = 20000.0f; // Maximum audible
cutoff.unit = "Hz";
params.addParameter(cutoff);
// Manager auto-clamps out-of-range values
params.setParameter("cutoff", 50000.0f); // Clamped to 20000
Next Steps¶
After parameter system: 1. 05_topology_templates - Use parameters in templates 2. 06_code_generation - Generate parameter update code 3. Preset system - Build on snapshot/restore
Status: ✅ Core implementation complete Test Coverage: ~90% Smoothing Latency: <1ms per parameter