add smoothing to split eq mode transitions

This commit is contained in:
2025-10-21 11:06:00 +02:00
parent 6fb32fbcf6
commit 63a00e1744
3 changed files with 174 additions and 68 deletions

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "audio_math.h" #include "audio_math.h"
#include "smoother.h"
#include <cmath> #include <cmath>
#include <vector> #include <vector>
@@ -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 { struct spliteq {
aw_filter lp_l, lp_r, hp_l, hp_r; // lowpass and highpass filters 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 mid_gain_adj = 1.0f;
float treble_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) inline void spliteq_init(spliteq& eq, double samplerate, double low_mid_crossover, double mid_high_crossover)
@@ -367,41 +376,44 @@ inline void spliteq_init(spliteq& eq, double samplerate, double low_mid_crossove
eq.treble2_r.cutoff = mid_high_crossover; 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; 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); butterworth_biquad_coeffs(eq.treble2_r, samplerate);
smoother_init(eq.transition_smoother, samplerate, 50.0f, 1.0f);
} }
// Process block (stereo) inline void spliteq_set_mode(spliteq& eq, spliteq_mode mode)
inline void spliteq_process_block(spliteq& eq, float** audio, int frames)
{ {
aw_filter_process_block(eq.hp_l, audio[0], frames); if (eq.target_mode != mode) {
aw_filter_process_block(eq.hp_r, audio[1], frames); eq.target_mode = mode;
eq.transitioning = true;
smoother_set_target(eq.transition_smoother, 0.0f);
}
}
for (int i = 0; i < frames; i++) { inline void linkwitz_riley_process(spliteq& eq, float**& audio, int& i)
if (eq.linkwitz_riley_enabled) { {
// butterwort/linkwitz-riley // left channel
// Left channel
double input_l = audio[0][i]; double input_l = audio[0][i];
// Bass // bass
double bass_l = butterworth_biquad_process(eq.bass1_l, input_l); double bass_l = butterworth_biquad_process(eq.bass1_l, input_l);
bass_l = butterworth_biquad_process(eq.bass2_l, bass_l); bass_l = butterworth_biquad_process(eq.bass2_l, bass_l);
// Mid // mid
double mid_l = butterworth_biquad_process(eq.mid_hp1_l, input_l); 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_hp2_l, mid_l);
mid_l = butterworth_biquad_process(eq.mid_lp1_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); mid_l = butterworth_biquad_process(eq.mid_lp2_l, mid_l);
// Treble // treble
double treble_l = butterworth_biquad_process(eq.treble1_l, input_l); double treble_l = butterworth_biquad_process(eq.treble1_l, input_l);
treble_l = butterworth_biquad_process(eq.treble2_l, treble_l); treble_l = butterworth_biquad_process(eq.treble2_l, treble_l);
// Apply gains // apply gains
bass_l *= eq.bass_gain; bass_l *= eq.bass_gain;
mid_l *= eq.mid_gain; mid_l *= eq.mid_gain;
treble_l *= eq.treble_gain; treble_l *= eq.treble_gain;
// Sum bands // sum bands
audio[0][i] = bass_l + mid_l + treble_l; audio[0][i] = bass_l + mid_l + treble_l;
// Right channel // right channel
double input_r = audio[1][i]; double input_r = audio[1][i];
double bass_r = butterworth_biquad_process(eq.bass1_r, input_r); double bass_r = butterworth_biquad_process(eq.bass1_r, input_r);
bass_r = butterworth_biquad_process(eq.bass2_r, bass_r); bass_r = butterworth_biquad_process(eq.bass2_r, bass_r);
@@ -419,8 +431,10 @@ inline void spliteq_process_block(spliteq& eq, float** audio, int frames)
treble_r *= eq.treble_gain; treble_r *= eq.treble_gain;
audio[1][i] = bass_r + mid_r + treble_r; audio[1][i] = bass_r + mid_r + treble_r;
} else { }
// cascade/sum
inline void cascade_sum_process(spliteq& eq, float**& audio, int& i)
{
double input_l = audio[0][i]; double input_l = audio[0][i];
double input_r = audio[1][i]; double input_r = audio[1][i];
@@ -433,7 +447,7 @@ inline void spliteq_process_block(spliteq& eq, float** audio, int frames)
double mid_l = input_l - bass_l - treble_l; double mid_l = input_l - bass_l - treble_l;
double mid_r = input_r - bass_r - treble_r; double mid_r = input_r - bass_r - treble_r;
// Apply gains // apply gains
bass_l *= eq.bass_gain_adj; bass_l *= eq.bass_gain_adj;
bass_r *= eq.bass_gain_adj; bass_r *= eq.bass_gain_adj;
mid_l *= eq.mid_gain_adj; mid_l *= eq.mid_gain_adj;
@@ -441,12 +455,42 @@ inline void spliteq_process_block(spliteq& eq, float** audio, int frames)
treble_l *= eq.treble_gain_adj; treble_l *= eq.treble_gain_adj;
treble_r *= eq.treble_gain_adj; treble_r *= eq.treble_gain_adj;
// Sum bands // sum bands
audio[0][i] = bass_l + mid_l + treble_l; audio[0][i] = bass_l + mid_l + treble_l;
audio[1][i] = bass_r + mid_r + treble_r; audio[1][i] = bass_r + mid_r + treble_r;
} }
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++) {
float smooth_gain = 1.0f;
if (eq.transitioning) {
smooth_gain = smoother_process_sample(eq.transition_smoother);
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_l, audio[0], frames);
aw_filter_process_block(eq.lp_r, audio[1], frames); aw_filter_process_block(eq.lp_r, audio[1], frames);
} }

View File

@@ -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 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 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 } // namespace trnr

60
util/smoother.h Normal file
View File

@@ -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<int32_t>(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<float>(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