add smoothing to split eq mode transitions
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
60
util/smoother.h
Normal 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
|
||||||
Reference in New Issue
Block a user