diff --git a/dynamics/oneknob.h b/dynamics/oneknob.h new file mode 100644 index 0000000..343dcc6 --- /dev/null +++ b/dynamics/oneknob.h @@ -0,0 +1,118 @@ +#pragma once +#include "audio_math.h" +#include + +namespace trnr { + +struct rms_detector { + float alpha; + float rms_squared; +}; + +inline void rms_init(rms_detector& det, float samplerate, float window_ms) +{ + float window_seconds = 0.001f * window_ms; + det.alpha = 1.0f - expf(-1.0f / (samplerate * window_seconds)); + det.rms_squared = 0.0f; +} + +template +inline sample rms_process(rms_detector& det, sample input) +{ + det.rms_squared = (1.0f - det.alpha) * det.rms_squared + det.alpha * (input * input); + return sqrtf(det.rms_squared); +} + +struct hp_filter { + float a0, a1, b1; + float z1; // filter state +}; + +inline void hp_filter_init(hp_filter& f, float samplerate) +{ + float cutoff = 100.0f; + float w0 = 2.0f * 3.14159265359f * cutoff / samplerate; + float alpha = (1.0f - std::tan(w0 / 2.0f)) / (1.0f + std::tan(w0 / 2.0f)); + f.a0 = 0.5f * (1.0f + alpha); + f.a1 = -0.5f * (1.0f + alpha); + f.b1 = alpha; + f.z1 = 0.0f; +} + +inline float hp_filter_process(hp_filter& f, float x) +{ + float y = f.a0 * x + f.a1 * f.z1 - f.b1 * f.z1; + f.z1 = x; + return y; +} + +struct oneknob_comp { + // params + float amount; + + // state + rms_detector detector; + hp_filter filter; + float attack_coef; + float release_coef; + float envelope_level; + float sidechain_in; +}; + +inline void oneknob_init(oneknob_comp& comp, float samplerate, float window_ms) +{ + rms_init(comp.detector, samplerate, window_ms); + hp_filter_init(comp.filter, samplerate); + comp.amount = 0.0f; + + const float attack_ms = 10.f; + const float release_ms = 100.f; + + comp.attack_coef = expf(-1.0f / (samplerate * (attack_ms * 0.001f))); + comp.release_coef = expf(-1.0f / (samplerate * (release_ms * 0.001f))); + comp.envelope_level = 0.f; + comp.sidechain_in = 0.f; +} + +template +inline void oneknob_process_block(oneknob_comp& comp, sample** audio, int frames) +{ + const float threshold = -18.f; + const float min_ratio = 1.0f; + const float max_ratio = 10.0f; + float ratio = min_ratio + comp.amount * (max_ratio - min_ratio); + + for (int i = 0; i < frames; ++i) { + float rms_value = rms_process(comp.detector, comp.sidechain_in); + float absolute_rms_db = lin_2_db(fabs(rms_value)); + + // cut envelope below threshold + float overshoot = absolute_rms_db - threshold; + if (overshoot < 0.f) overshoot = 0.f; + + if (overshoot > comp.envelope_level) { + comp.envelope_level = overshoot + comp.attack_coef * (comp.envelope_level - overshoot); + } else { + comp.envelope_level = overshoot + comp.release_coef * (comp.envelope_level - overshoot); + } + + if (comp.envelope_level < 0.f) comp.envelope_level = 0.f; + + float slope = 1.f / ratio; + + float gain_reduction_db = comp.envelope_level * (slope - 1.f); + float gain_reduction_lin = db_2_lin(gain_reduction_db); + + sample input_l = audio[0][i]; + sample input_r = audio[1][i]; + + audio[0][i] *= gain_reduction_lin; + audio[1][i] *= gain_reduction_lin; + + // feedback compression + float sum = sqrtf(0.5f * (audio[0][i] * audio[0][i] + audio[1][i] * audio[1][i])); + comp.sidechain_in = hp_filter_process(comp.filter, sum); + } +} + +} // namespace trnr