From 63a00e17442ad02ffb9bff89440bdf494ccc7cf1 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 21 Oct 2025 11:06:00 +0200 Subject: [PATCH] add smoothing to split eq mode transitions --- filter/spliteq.h | 180 ++++++++++++++++++++++++++++------------------ util/audio_math.h | 2 + util/smoother.h | 60 ++++++++++++++++ 3 files changed, 174 insertions(+), 68 deletions(-) create mode 100644 util/smoother.h diff --git a/filter/spliteq.h b/filter/spliteq.h index b3803f6..4a8475e 100644 --- a/filter/spliteq.h +++ b/filter/spliteq.h @@ -1,5 +1,6 @@ #pragma once #include "audio_math.h" +#include "smoother.h" #include #include @@ -224,6 +225,11 @@ inline void aw_filter_process_block(aw_filter& f, float* audio, int frames) } } +enum spliteq_mode { + CASCADE_SUM, + LINKWITZ_RILEY +}; + struct spliteq { aw_filter lp_l, lp_r, hp_l, hp_r; // lowpass and highpass filters @@ -257,7 +263,10 @@ struct spliteq { float mid_gain_adj = 1.0f; float treble_gain_adj = 1.0f; - bool linkwitz_riley_enabled = false; + spliteq_mode current_mode = LINKWITZ_RILEY; + spliteq_mode target_mode = LINKWITZ_RILEY; + bool transitioning = false; + smoother transition_smoother; }; inline void spliteq_init(spliteq& eq, double samplerate, double low_mid_crossover, double mid_high_crossover) @@ -367,86 +376,121 @@ inline void spliteq_init(spliteq& eq, double samplerate, double low_mid_crossove eq.treble2_r.cutoff = mid_high_crossover; eq.treble2_r.x1 = eq.treble2_r.x2 = eq.treble2_r.y1 = eq.treble2_r.y2 = 0.0; butterworth_biquad_coeffs(eq.treble2_r, samplerate); + + smoother_init(eq.transition_smoother, samplerate, 50.0f, 1.0f); +} + +inline void spliteq_set_mode(spliteq& eq, spliteq_mode mode) +{ + if (eq.target_mode != mode) { + eq.target_mode = mode; + eq.transitioning = true; + smoother_set_target(eq.transition_smoother, 0.0f); + } +} + +inline void linkwitz_riley_process(spliteq& eq, float**& audio, int& i) +{ + // left channel + double input_l = audio[0][i]; + // bass + double bass_l = butterworth_biquad_process(eq.bass1_l, input_l); + bass_l = butterworth_biquad_process(eq.bass2_l, bass_l); + // mid + double mid_l = butterworth_biquad_process(eq.mid_hp1_l, input_l); + mid_l = butterworth_biquad_process(eq.mid_hp2_l, mid_l); + mid_l = butterworth_biquad_process(eq.mid_lp1_l, mid_l); + mid_l = butterworth_biquad_process(eq.mid_lp2_l, mid_l); + // treble + double treble_l = butterworth_biquad_process(eq.treble1_l, input_l); + treble_l = butterworth_biquad_process(eq.treble2_l, treble_l); + + // apply gains + bass_l *= eq.bass_gain; + mid_l *= eq.mid_gain; + treble_l *= eq.treble_gain; + + // sum bands + audio[0][i] = bass_l + mid_l + treble_l; + + // right channel + double input_r = audio[1][i]; + double bass_r = butterworth_biquad_process(eq.bass1_r, input_r); + bass_r = butterworth_biquad_process(eq.bass2_r, bass_r); + + double mid_r = butterworth_biquad_process(eq.mid_hp1_r, input_r); + mid_r = butterworth_biquad_process(eq.mid_hp2_r, mid_r); + mid_r = butterworth_biquad_process(eq.mid_lp1_r, mid_r); + mid_r = butterworth_biquad_process(eq.mid_lp2_r, mid_r); + + double treble_r = butterworth_biquad_process(eq.treble1_r, input_r); + treble_r = butterworth_biquad_process(eq.treble2_r, treble_r); + + bass_r *= eq.bass_gain; + mid_r *= eq.mid_gain; + treble_r *= eq.treble_gain; + + audio[1][i] = bass_r + mid_r + treble_r; +} + +inline void cascade_sum_process(spliteq& eq, float**& audio, int& i) +{ + double input_l = audio[0][i]; + double input_r = audio[1][i]; + + double bass_l = cascade_filter_process(eq.bass_l, input_l); + double bass_r = cascade_filter_process(eq.bass_r, input_r); + + double treble_l = cascade_filter_process(eq.treble_l, input_l); + double treble_r = cascade_filter_process(eq.treble_r, input_r); + + double mid_l = input_l - bass_l - treble_l; + double mid_r = input_r - bass_r - treble_r; + + // apply gains + bass_l *= eq.bass_gain_adj; + bass_r *= eq.bass_gain_adj; + mid_l *= eq.mid_gain_adj; + mid_r *= eq.mid_gain_adj; + treble_l *= eq.treble_gain_adj; + treble_r *= eq.treble_gain_adj; + + // sum bands + audio[0][i] = bass_l + mid_l + treble_l; + audio[1][i] = bass_r + mid_r + treble_r; } -// Process block (stereo) inline void spliteq_process_block(spliteq& eq, float** audio, int frames) { + // highpass filters aw_filter_process_block(eq.hp_l, audio[0], frames); aw_filter_process_block(eq.hp_r, audio[1], frames); for (int i = 0; i < frames; i++) { - if (eq.linkwitz_riley_enabled) { - // butterwort/linkwitz-riley + float smooth_gain = 1.0f; - // Left channel - double input_l = audio[0][i]; - // Bass - double bass_l = butterworth_biquad_process(eq.bass1_l, input_l); - bass_l = butterworth_biquad_process(eq.bass2_l, bass_l); - // Mid - double mid_l = butterworth_biquad_process(eq.mid_hp1_l, input_l); - mid_l = butterworth_biquad_process(eq.mid_hp2_l, mid_l); - mid_l = butterworth_biquad_process(eq.mid_lp1_l, mid_l); - mid_l = butterworth_biquad_process(eq.mid_lp2_l, mid_l); - // Treble - double treble_l = butterworth_biquad_process(eq.treble1_l, input_l); - treble_l = butterworth_biquad_process(eq.treble2_l, treble_l); + if (eq.transitioning) { + smooth_gain = smoother_process_sample(eq.transition_smoother); - // Apply gains - bass_l *= eq.bass_gain; - mid_l *= eq.mid_gain; - treble_l *= eq.treble_gain; - - // Sum bands - audio[0][i] = bass_l + mid_l + treble_l; - - // Right channel - double input_r = audio[1][i]; - double bass_r = butterworth_biquad_process(eq.bass1_r, input_r); - bass_r = butterworth_biquad_process(eq.bass2_r, bass_r); - - double mid_r = butterworth_biquad_process(eq.mid_hp1_r, input_r); - mid_r = butterworth_biquad_process(eq.mid_hp2_r, mid_r); - mid_r = butterworth_biquad_process(eq.mid_lp1_r, mid_r); - mid_r = butterworth_biquad_process(eq.mid_lp2_r, mid_r); - - double treble_r = butterworth_biquad_process(eq.treble1_r, input_r); - treble_r = butterworth_biquad_process(eq.treble2_r, treble_r); - - bass_r *= eq.bass_gain; - mid_r *= eq.mid_gain; - treble_r *= eq.treble_gain; - - audio[1][i] = bass_r + mid_r + treble_r; - } else { - // cascade/sum - double input_l = audio[0][i]; - double input_r = audio[1][i]; - - double bass_l = cascade_filter_process(eq.bass_l, input_l); - double bass_r = cascade_filter_process(eq.bass_r, input_r); - - double treble_l = cascade_filter_process(eq.treble_l, input_l); - double treble_r = cascade_filter_process(eq.treble_r, input_r); - - double mid_l = input_l - bass_l - treble_l; - double mid_r = input_r - bass_r - treble_r; - - // Apply gains - bass_l *= eq.bass_gain_adj; - bass_r *= eq.bass_gain_adj; - mid_l *= eq.mid_gain_adj; - mid_r *= eq.mid_gain_adj; - treble_l *= eq.treble_gain_adj; - treble_r *= eq.treble_gain_adj; - - // Sum bands - audio[0][i] = bass_l + mid_l + treble_l; - audio[1][i] = bass_r + mid_r + treble_r; + if (smooth_gain == 0.f) { + smoother_set_target(eq.transition_smoother, 1.0); + eq.current_mode = eq.target_mode; + } else if (smooth_gain == 1.f) { + eq.transitioning = false; + } } + + if (eq.current_mode == LINKWITZ_RILEY) { + linkwitz_riley_process(eq, audio, i); + } else if (eq.current_mode == CASCADE_SUM) { + cascade_sum_process(eq, audio, i); + } + + audio[0][i] *= smooth_gain; + audio[1][i] *= smooth_gain; } + // lowpass filters aw_filter_process_block(eq.lp_l, audio[0], frames); aw_filter_process_block(eq.lp_r, audio[1], frames); } diff --git a/util/audio_math.h b/util/audio_math.h index 30a2b73..d99bd85 100644 --- a/util/audio_math.h +++ b/util/audio_math.h @@ -12,4 +12,6 @@ inline double lin_2_db(double lin) inline double db_2_lin(double db) { return pow(10.0, db / 20.0); } inline float midi_to_frequency(float midi_note) { return 440.0 * powf(2.0, ((float)midi_note - 69.0) / 12.0); } + +inline float ms_to_samples(float ms, double sample_rate) { return (ms * 0.001f) * (float)sample_rate; } } // namespace trnr \ No newline at end of file diff --git a/util/smoother.h b/util/smoother.h new file mode 100644 index 0000000..b0fcedd --- /dev/null +++ b/util/smoother.h @@ -0,0 +1,60 @@ +#pragma once + +#include "audio_math.h" + +namespace trnr { + +struct smoother { + float samplerate; + float time_samples; + + float current; + float target; + float increment; + int32_t remaining; +}; + +inline void smoother_init(smoother& s, double samplerate, float time_ms, float initial_value = 0.0f) +{ + s.samplerate = fmax(0.0, samplerate); + s.time_samples = ms_to_samples(time_ms, s.samplerate); + s.current = initial_value; + s.target = initial_value; + s.increment = 0.0f; + s.remaining = 0; +} + +inline void smoother_set_target(smoother& s, float newTarget) +{ + s.target = newTarget; + + // immediate if time is zero or too short + if (s.time_samples <= 1.0f) { + s.current = s.target; + s.increment = 0.0f; + s.remaining = 0; + return; + } + + int32_t n = static_cast(fmax(1.0f, ceilf(s.time_samples))); + s.remaining = n; + // protect against denormals / tiny differences, but we want exact reach at the end + s.increment = (s.target - s.current) / static_cast(s.remaining); +} + +// process a single sample and return the smoothed value +inline float smoother_process_sample(smoother& s) +{ + if (s.remaining > 0) { + s.current += s.increment; + --s.remaining; + if (s.remaining == 0) { + // ensure exact target at the end to avoid FP drift + s.current = s.target; + s.increment = 0.0f; + } + } + return s.current; +} + +} // namespace trnr \ No newline at end of file