Vital
Loading...
Searching...
No Matches
ladder_filter.cpp
Go to the documentation of this file.
1#include "ladder_filter.h"
2
3#include "futils.h"
4
5namespace vital {
6
13
18 void LadderFilter::reset(poly_mask reset_mask) {
19 // Reset the temporary filter input
20 filter_input_ = utils::maskLoad(filter_input_, 0.0f, reset_mask);
21
22 // Reset each of the four one-pole stages
23 for (int i = 0; i < kNumStages; ++i)
24 stages_[i].reset(reset_mask);
25 }
26
32 resonance_ = 0.0f;
33 drive_ = 0.0f;
34 post_multiply_ = 0.0f;
35 }
36
44 void LadderFilter::process(int num_samples) {
46
47 // Cache the current parameters to smooth them over num_samples
48 poly_float current_resonance = resonance_;
49 poly_float current_drive = drive_;
50 poly_float current_post_multiply = post_multiply_;
51
52 // Cache stage scale factors
53 poly_float current_stage_scales[kNumStages + 1];
54 for (int i = 0; i <= kNumStages; ++i)
55 current_stage_scales[i] = stage_scales_[i];
56
57 // Pull in latest filter settings
60
61 // Check if we need to reset (e.g., new note, or parameter ramp events)
62 poly_mask reset_mask = getResetMask(kReset);
63 if (reset_mask.anyMask()) {
64 reset(reset_mask);
65
66 // Reload parameters for the reset voices
67 current_resonance = utils::maskLoad(current_resonance, resonance_, reset_mask);
68 current_drive = utils::maskLoad(current_drive, drive_, reset_mask);
69 current_post_multiply = utils::maskLoad(current_post_multiply, post_multiply_, reset_mask);
70 for (int i = 0; i <= kNumStages; ++i)
71 current_stage_scales[i] = utils::maskLoad(current_stage_scales[i], stage_scales_[i], reset_mask);
72 }
73
74 // Compute incremental changes for smooth parameter transitions
75 mono_float tick_increment = 1.0f / num_samples;
76 poly_float delta_resonance = (resonance_ - current_resonance) * tick_increment;
77 poly_float delta_drive = (drive_ - current_drive) * tick_increment;
78 poly_float delta_post_multiply = (post_multiply_ - current_post_multiply) * tick_increment;
79
80 poly_float delta_stage_scales[kNumStages + 1];
81 for (int i = 0; i <= kNumStages; ++i)
82 delta_stage_scales[i] = (stage_scales_[i] - current_stage_scales[i]) * tick_increment;
83
84 // Prepare buffers and coefficient lookup
85 const poly_float* audio_in = input(kAudio)->source->buffer;
86 poly_float* audio_out = output()->buffer;
87 const CoefficientLookup* coefficient_lookup = getCoefficientLookup();
88 const poly_float* midi_cutoff_buffer = filter_state_.midi_cutoff_buffer;
89
90 // Pre-calculate frequency-related constants
91 poly_float base_midi = midi_cutoff_buffer[num_samples - 1];
92 poly_float base_frequency = utils::midiNoteToFrequency(base_midi) * (1.0f / getSampleRate());
93 poly_float max_frequency = kMaxCutoff / getSampleRate();
94
95 // Process each sample
96 for (int i = 0; i < num_samples; ++i) {
97 // Compute current cutoff from MIDI pitch
98 poly_float midi_delta = midi_cutoff_buffer[i] - base_midi;
99 poly_float frequency = utils::min(base_frequency * futils::midiOffsetToRatio(midi_delta), max_frequency);
100 poly_float coefficient = coefficient_lookup->cubicLookup(frequency);
101
102 // Smoothly update parameters
103 current_resonance += delta_resonance;
104 current_drive += delta_drive;
105 current_post_multiply += delta_post_multiply;
106 for (int stage = 0; stage <= kNumStages; ++stage)
107 current_stage_scales[stage] += delta_stage_scales[stage];
108
109 // Process one sample through the ladder filter
110 tick(audio_in[i], coefficient, current_resonance, current_drive);
111
112 // Sum up all stage outputs, each scaled appropriately
113 poly_float total = current_stage_scales[0] * filter_input_;
114 for (int stage = 0; stage < kNumStages; ++stage)
115 total += current_stage_scales[stage + 1] * stages_[stage].getCurrentState();
116
117 // Multiply the final sum by the post-multiply factor and write to output
118 audio_out[i] = total * current_post_multiply;
119 }
120 }
121
126 void LadderFilter::setupFilter(const FilterState& filter_state) {
127 // Convert user-specified resonance percent to a local resonance value
128 poly_float resonance_percent = utils::clamp(filter_state.resonance_percent, 0.0f, 1.0f);
129 poly_float resonance_adjust = resonance_percent;
130 if (filter_state.style) {
131 // Optional alternative style: sine-based scaling
132 resonance_adjust = utils::sin(resonance_percent * (0.5f * kPi));
133 }
134
135 // Interpolate between minimum and maximum resonance
136 resonance_ = utils::interpolate(kMinResonance, kMaxResonance, resonance_adjust);
137 // Boost resonance further based on drive
138 resonance_ += filter_state.drive_percent * filter_state.resonance_percent * kDriveResonanceBoost;
139
140 // Adjust stage scales (low-pass, high-pass, etc.) based on style
141 setStageScales(filter_state);
142 }
143
151 void LadderFilter::setStageScales(const FilterState& filter_state) {
152 // Polynomial coefficients for different filter slopes (biquad expansions, etc.)
153 static const mono_float low_pass24[kNumStages + 1] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
154 static const mono_float band_pass24[kNumStages + 1] = { 0.0f, 0.0f, -1.0f, 2.0f, -1.0f };
155 static const mono_float high_pass24[kNumStages + 1] = { 1.0f, -4.0f, 6.0f, -4.0f, 1.0f };
156 static const mono_float low_pass12[kNumStages + 1] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f };
157 static const mono_float band_pass12[kNumStages + 1] = { 0.0f, 1.0f, -1.0f, 0.0f, 0.0f };
158 static const mono_float high_pass12[kNumStages + 1] = { 1.0f, -2.0f, 1.0f, 0.0f, 0.0f };
159
160 // For pass blend, map to -1..1
161 poly_float blend = utils::clamp(filter_state.pass_blend - 1.0f, -1.0f, 1.0f);
162 // band_pass is the sqrt(1 - blend^2) portion of the circle
163 poly_float band_pass = utils::sqrt(-blend * blend + 1.0f);
164
165 // For partial crossfade between low-pass and high-pass
166 poly_mask blend_mask = poly_float::lessThan(blend, 0.0f);
167 poly_float low_pass = (-blend) & blend_mask; // Active if blend < 0
168 poly_float high_pass = blend & ~blend_mask; // Active if blend > 0
169
170 // Drive and resonance scaling
171 poly_float resonance_percent = utils::clamp(filter_state.resonance_percent, 0.0f, 1.0f);
172 poly_float drive_mult = resonance_percent + 1.0f;
173 if (filter_state.style)
174 drive_mult = utils::sin(resonance_percent) + 1.0f;
175
176 poly_float resonance_scale = utils::interpolate(drive_mult, 1.0f, high_pass);
177 drive_ = filter_state.drive * resonance_scale;
178
179 // A factor used to adjust volume after applying drive
180 post_multiply_ = poly_float(1.0f)
181 / utils::sqrt((filter_state.drive - 1.0f) * 0.5f + 1.0f);
182
183 // Compute the filter’s output mixing (12 dB or 24 dB, etc.)
184 if (filter_state.style == k12Db) {
185 for (int i = 0; i <= kNumStages; ++i)
186 stage_scales_[i] = (low_pass * low_pass12[i]
187 + band_pass * band_pass12[i]
188 + high_pass * high_pass12[i]);
189 }
190 else if (filter_state.style == k24Db) {
191 // A variation used for a 24 dB slope
192 band_pass = -poly_float::abs(blend) + 1.0f;
193 post_multiply_ = poly_float(1.0f)
194 / utils::sqrt((filter_state.drive - 1.0f) * 0.25f + 1.0f);
195
196 for (int i = 0; i <= kNumStages; ++i)
197 stage_scales_[i] = (low_pass * low_pass24[i]
198 + band_pass * band_pass24[i]
199 + high_pass * high_pass24[i]);
200 }
201 else if (filter_state.style == kDualNotchBand) {
202 // A 'dual notch band' style of ladder mixing
203 drive_ = filter_state.drive; // No scaling for drive in this style
204 poly_float low_pass_fade = utils::min(blend + 1.0f, 1.0f);
205 poly_float high_pass_fade = utils::min(-blend + 1.0f, 1.0f);
206
207 stage_scales_[0] = low_pass_fade;
208 stage_scales_[1] = low_pass_fade * -4.0f;
209 stage_scales_[2] = high_pass_fade * 4.0f + low_pass_fade * 8.0f;
210 stage_scales_[3] = high_pass_fade * -8.0f - low_pass_fade * 8.0f;
211 stage_scales_[4] = high_pass_fade * 4.0f + low_pass_fade * 4.0f;
212 }
213 else if (filter_state.style == kNotchPassSwap) {
214 post_multiply_ = poly_float(1.0f)
215 / utils::sqrt((filter_state.drive - 1.0f) * 0.5f + 1.0f);
216
217 poly_float low_pass_fade = utils::min(blend + 1.0f, 1.0f);
218 poly_float low_pass_fade2 = low_pass_fade * low_pass_fade;
219 poly_float high_pass_fade = utils::min(-blend + 1.0f, 1.0f);
220 poly_float high_pass_fade2 = high_pass_fade * high_pass_fade;
221 poly_float low_high_pass_fade = low_pass_fade * high_pass_fade;
222
223 stage_scales_[0] = low_pass_fade2;
224 stage_scales_[1] = low_pass_fade2 * -4.0f;
225 stage_scales_[2] = low_pass_fade2 * 6.0f + low_high_pass_fade * 2.0f;
226 stage_scales_[3] = low_pass_fade2 * -4.0f - low_high_pass_fade * 4.0f;
227 stage_scales_[4] = low_pass_fade2 + high_pass_fade2 + low_high_pass_fade * 2.0f;
228 }
229 else if (filter_state.style == kBandPeakNotch) {
230 // Another specialized style that uses a band/peak/notch configuration
231 poly_float drive_t = poly_float::min(-blend + 1.0f, 1.0f);
232 drive_ = utils::interpolate(filter_state.drive, drive_, drive_t);
233
234 poly_float drive_inv_t = -drive_t + 1.0f;
235 poly_float mult = utils::sqrt((drive_inv_t * drive_inv_t) * 0.5f + 0.5f);
236 poly_float peak_band_value = -utils::max(-blend, 0.0f);
237 poly_float low_high = mult * (peak_band_value + 1.0f);
238 poly_float band = mult * (peak_band_value - blend + 1.0f) * 2.0f;
239
240 for (int i = 0; i <= kNumStages; ++i)
241 stage_scales_[i] = (low_high * low_pass12[i]
242 + band * band_pass12[i]
243 + low_high * high_pass12[i]);
244 }
245 }
246
256 // Multiply coefficient by a fixed tuning factor to better match classic ladder response
257 poly_float g1 = coefficient * kResonanceTuning;
258 poly_float g2 = g1 * g1;
259 poly_float g3 = g1 * g2;
260
261 // The final stage's output from the previous sample is fed back through g1..g3
262 poly_float filter_state1 = utils::mulAdd(stages_[3].getNextSatState(), g1, stages_[2].getNextSatState());
263 poly_float filter_state2 = utils::mulAdd(filter_state1, g2, stages_[1].getNextSatState());
264 poly_float filter_state = utils::mulAdd(filter_state2, g3, stages_[0].getNextSatState());
265
266 // Combine input (with drive) and negative feedback from the final stage
267 poly_float filter_input = (audio_in * drive - resonance * filter_state);
268
269 // Use a nonlinear function (tanh) for mild saturation
270 filter_input_ = futils::tanh(filter_input);
271
272 // Pass through each stage, each employing algebraic saturation
273 poly_float stage_out = stages_[0].tick(filter_input_, coefficient);
274 stage_out = stages_[1].tick(stage_out, coefficient);
275 stage_out = stages_[2].tick(stage_out, coefficient);
276 stages_[3].tick(stage_out, coefficient); // final stage in the pipeline
277 }
278
279} // namespace vital
A classic transistor ladder-style filter for the Vital synthesizer.
Definition ladder_filter.h:19
static constexpr mono_float kMinResonance
Minimum resonance value.
Definition ladder_filter.h:34
static constexpr int kNumStages
Number of filter stages in the ladder (4-pole ladder).
Definition ladder_filter.h:24
void reset(poly_mask reset_mask) override
Resets internal states of each filter stage according to the given mask.
Definition ladder_filter.cpp:18
static constexpr mono_float kMaxResonance
Maximum resonance value.
Definition ladder_filter.h:39
static constexpr mono_float kDriveResonanceBoost
Boost factor added to the resonance based on drive.
Definition ladder_filter.h:49
static constexpr mono_float kResonanceTuning
Resonance tuning factor to align the filter’s internal response with musical expectations.
Definition ladder_filter.h:29
void setupFilter(const FilterState &filter_state) override
Configures the filter parameters based on a FilterState.
Definition ladder_filter.cpp:126
force_inline void tick(poly_float audio_in, poly_float coefficient, poly_float resonance, poly_float drive)
Processes a single sample through the ladder filter stages.
Definition ladder_filter.cpp:254
LadderFilter()
Constructs a new LadderFilter.
Definition ladder_filter.cpp:10
static constexpr mono_float kMaxCutoff
Maximum cutoff frequency in Hz (used internally).
Definition ladder_filter.h:59
virtual void process(int num_samples) override
Processes the input audio buffer through this ladder filter.
Definition ladder_filter.cpp:44
void hardReset() override
Performs a hard reset of all filter states.
Definition ladder_filter.cpp:30
A one-dimensional lookup table for a given function with a specified resolution.
Definition lookup_table.h:31
force_inline poly_float cubicLookup(poly_float value) const
Performs a cubic interpolation lookup on the precomputed data.
Definition lookup_table.h:61
force_inline poly_float tick(poly_float audio_in, poly_float coefficient)
Processes a single sample, applying the saturation function at each step.
Definition one_pole_filter.h:73
Base class for all signal-processing units in Vital.
Definition processor.h:212
force_inline Input * input(unsigned int index=0) const
Retrieves the Input pointer at a given index.
Definition processor.h:587
force_inline int getSampleRate() const
Retrieves the current (effective) sample rate.
Definition processor.h:326
bool inputMatchesBufferSize(int input=0)
Checks whether the buffer size of a particular input matches the size needed by this Processor.
Definition processor.cpp:42
force_inline poly_mask getResetMask(int input_index) const
Retrieves a mask indicating which voices triggered a note-on event. Compares the input's trigger_valu...
Definition processor.h:360
force_inline Output * output(unsigned int index=0) const
Retrieves the Output pointer at a given index.
Definition processor.h:616
Holds the parameters necessary to configure a SynthFilter at runtime.
Definition synth_filter.h:92
void loadSettings(Processor *processor)
Loads state from a Processor’s input signals (MIDI cutoff, drive, style, etc.).
Definition synth_filter.cpp:30
const poly_float * midi_cutoff_buffer
Pointer to the buffer storing per-sample MIDI cutoff.
Definition synth_filter.h:111
poly_float drive_percent
Normalized drive parameter in [0..1].
Definition synth_filter.h:114
int style
Filter style enum (e.g., k12Db, k24Db)
Definition synth_filter.h:116
poly_float resonance_percent
Resonance parameter in [0..1].
Definition synth_filter.h:112
@ kReset
Reset signal.
Definition synth_filter.h:56
@ kAudio
Audio input index.
Definition synth_filter.h:55
FilterState filter_state_
Internal storage of the most recent FilterState, used by derived filters.
Definition synth_filter.h:151
static const CoefficientLookup * getCoefficientLookup()
Retrieves a pointer to the static coefficient lookup table.
Definition synth_filter.h:48
@ kBandPeakNotch
Definition synth_filter.h:79
@ kNotchPassSwap
Definition synth_filter.h:77
@ k24Db
Definition synth_filter.h:76
@ k12Db
Definition synth_filter.h:75
@ kDualNotchBand
Definition synth_filter.h:78
#define VITAL_ASSERT(x)
Definition common.h:11
#define force_inline
Definition common.h:23
cr::Value resonance
Resonance factor for this formant.
Definition formant_filter.cpp:18
Contains faster but less accurate versions of utility math functions, such as exponential,...
const poly_mask kFullMask
A mask covering all lanes of a poly_float vector.
Definition synth_constants.h:257
force_inline poly_float midiOffsetToRatio(poly_float note_offset)
Converts a MIDI note offset to a frequency ratio.
Definition futils.h:184
force_inline poly_float tanh(poly_float value)
Approximates tanh function using a complex polynomial.
Definition futils.h:347
force_inline poly_float clamp(poly_float value, mono_float min, mono_float max)
Clamps each lane of a vector to [min, max].
Definition poly_utils.h:306
force_inline poly_float mulAdd(poly_float a, poly_float b, poly_float c)
Performs a fused multiply-add on SIMD data: (a * b) + c.
Definition poly_utils.h:61
force_inline poly_float min(poly_float left, poly_float right)
Returns the minimum of two poly_floats lane-by-lane.
Definition poly_utils.h:334
force_inline poly_float max(poly_float left, poly_float right)
Returns the maximum of two poly_floats lane-by-lane.
Definition poly_utils.h:327
force_inline poly_float maskLoad(poly_float zero_value, poly_float one_value, poly_mask reset_mask)
Selects between two values (zero_value or one_value) based on a mask in each lane.
Definition poly_utils.h:351
force_inline poly_float midiNoteToFrequency(poly_float value)
Converts a MIDI note to a frequency (vectorized).
Definition poly_utils.h:123
force_inline poly_float sqrt(poly_float value)
Computes the square root of each element in a poly_float.
Definition poly_utils.h:169
force_inline poly_float sin(poly_float value)
Computes the sine of each element (in radians).
Definition poly_utils.h:159
force_inline poly_float interpolate(poly_float from, poly_float to, mono_float t)
Performs a linear interpolation between two poly_floats using a scalar t in [0..1].
Definition poly_utils.h:182
Contains classes and functions used within the Vital synthesizer framework.
constexpr mono_float kPi
Pi constant.
Definition common.h:36
poly_int poly_mask
Alias for clarity; used as a mask type in poly_float.
Definition poly_values.h:590
float mono_float
Definition common.h:33
const Output * source
The output from which this input reads samples.
Definition processor.h:134
poly_float * buffer
Pointer to the output buffer.
Definition processor.h:110
Represents a vector of floating-point values using SIMD instructions.
Definition poly_values.h:600
static force_inline poly_mask vector_call lessThan(poly_float one, poly_float two)
Definition poly_values.h:1105
static force_inline simd_type vector_call min(simd_type one, simd_type two)
Returns the element-wise minimum of two SIMD float registers.
Definition poly_values.h:920
static force_inline simd_type vector_call abs(simd_type value)
Computes the absolute value of each element in the SIMD float register.
Definition poly_values.h:935
Represents a vector of integer values using SIMD instructions.
Definition poly_values.h:56
static force_inline uint32_t vector_call anyMask(simd_type value)
Returns a bitmask that indicates which bytes/elements in the register are non-zero.
Definition poly_values.h:352