Line data Source code
1 : #include "audio/effects/modulation/flanger.h"
2 :
3 : #include <cmath>
4 :
5 : #include "audio/effects/core/effect_factory.h"
6 :
7 : namespace Amplitron {
8 :
9 2 : static EffectRegistrar<Flanger> reg("Flanger");
10 :
11 : // Param indices
12 : static constexpr int P_RATE = 0;
13 : static constexpr int P_DEPTH = 1;
14 : static constexpr int P_DELAY = 2;
15 : static constexpr int P_FEEDBACK = 3;
16 : static constexpr int P_MIX = 4;
17 :
18 : // Max total delay: 15ms base + 7ms depth = 22ms. Size to 30ms for safety.
19 : static constexpr float MAX_DELAY_MS = 30.0f;
20 :
21 96 : Flanger::Flanger() {
22 312 : params_ = {
23 72 : {"Rate", 0.3f, 0.05f, 5.0f, 0.3f, "Hz",
24 24 : "LFO speed. Slow rates create a dreamy sweep; fast rates push toward vibrato territory."},
25 24 : {"Depth", 2.0f, 0.1f, 7.0f, 2.0f, "ms",
26 24 : "Modulation depth in milliseconds. Controls the width of the delay time sweep around the "
27 : "base delay."},
28 24 : {"Delay", 1.0f, 0.1f, 10.0f, 1.0f, "ms",
29 24 : "Base delay time. Short values (< 2ms) create tight comb filtering; longer values soften "
30 : "the effect."},
31 24 : {"Feedback", 0.7f, -0.95f, 0.95f, 0.7f, "",
32 24 : "Feedback amount. Positive values add metallic resonance; negative values create a "
33 : "hollow, phasey character."},
34 24 : {"Mix", 0.5f, 0.0f, 1.0f, 0.5f, "",
35 24 : "Dry/wet blend. At 0.5 the comb filter notches are deepest for the classic flanger "
36 : "sound."},
37 312 : };
38 72 : set_sample_rate(DEFAULT_SAMPLE_RATE);
39 144 : }
40 :
41 141 : void Flanger::set_sample_rate(int sample_rate) {
42 141 : Effect::set_sample_rate(sample_rate);
43 141 : max_delay_samples_ = static_cast<int>(sample_rate * MAX_DELAY_MS * 0.001f) + 2;
44 141 : delay_buffer_.assign(max_delay_samples_, 0.0f);
45 141 : delay_buffer_r_.assign(max_delay_samples_, 0.0f);
46 141 : reset();
47 141 : }
48 :
49 102 : void Flanger::process(float* buffer, int num_samples) {
50 102 : if (!enabled_) return;
51 :
52 96 : const float rate = params_[P_RATE].value;
53 96 : const float depth_ms = params_[P_DEPTH].value;
54 96 : const float delay_ms = params_[P_DELAY].value;
55 96 : const float feedback = params_[P_FEEDBACK].value;
56 96 : const float mix = params_[P_MIX].value;
57 :
58 96 : const float lfo_inc = rate / static_cast<float>(sample_rate_);
59 :
60 53856 : for (int i = 0; i < num_samples; ++i) {
61 53760 : const float dry = buffer[i];
62 :
63 : // LFO in [0, 1]
64 53760 : const float lfo = 0.5f * (1.0f + std::sin(TWO_PI * lfo_phase_));
65 :
66 : // Modulated delay in samples
67 35840 : const float delay_samples =
68 89600 : clamp((delay_ms + lfo * depth_ms) * 0.001f * static_cast<float>(sample_rate_), 1.0f,
69 53760 : static_cast<float>(max_delay_samples_ - 2));
70 :
71 : // Fractional read position
72 53760 : float read_pos_f = static_cast<float>(write_pos_) - delay_samples;
73 53760 : if (read_pos_f < 0.0f) read_pos_f += static_cast<float>(max_delay_samples_);
74 :
75 53760 : const int ipos = static_cast<int>(read_pos_f);
76 53760 : const float frac = read_pos_f - static_cast<float>(ipos);
77 53760 : const int pos0 = ipos % max_delay_samples_;
78 53760 : const int pos1 = (ipos + 1) % max_delay_samples_;
79 :
80 53760 : const float delayed = delay_buffer_[pos0] * (1.0f - frac) + delay_buffer_[pos1] * frac;
81 :
82 : // Write dry + feedback into the delay line; clamp to prevent runaway
83 53760 : delay_buffer_[write_pos_] = clamp(dry + feedback * delayed, -2.0f, 2.0f);
84 :
85 53760 : buffer[i] = dry * (1.0f - mix) + delayed * mix;
86 :
87 53760 : write_pos_ = (write_pos_ + 1) % max_delay_samples_;
88 53760 : lfo_phase_ += lfo_inc;
89 53760 : if (lfo_phase_ >= 1.0f) lfo_phase_ -= 1.0f;
90 17920 : }
91 34 : }
92 :
93 0 : void Flanger::process_stereo(float* left, float* right, int num_samples) {
94 0 : if (!enabled_) {
95 0 : return;
96 : }
97 :
98 0 : const float rate = params_[P_RATE].value;
99 0 : const float depth_ms = params_[P_DEPTH].value;
100 0 : const float delay_ms = params_[P_DELAY].value;
101 0 : const float feedback = params_[P_FEEDBACK].value;
102 0 : const float mix = params_[P_MIX].value;
103 0 : const float lfo_inc = rate / static_cast<float>(sample_rate_);
104 :
105 0 : for (int i = 0; i < num_samples; ++i) {
106 0 : const float dry_l = left[i];
107 0 : const float dry_r = right[i];
108 :
109 : // Left LFO
110 0 : const float lfo_l = 0.5f * (1.0f + std::sin(TWO_PI * lfo_phase_));
111 0 : const float delay_samp_l =
112 0 : clamp((delay_ms + lfo_l * depth_ms) * 0.001f * static_cast<float>(sample_rate_), 1.0f,
113 0 : static_cast<float>(max_delay_samples_ - 2));
114 :
115 0 : float rp_l = static_cast<float>(write_pos_) - delay_samp_l;
116 0 : if (rp_l < 0.0f) rp_l += static_cast<float>(max_delay_samples_);
117 0 : const int ip_l = static_cast<int>(rp_l);
118 0 : const float f_l = rp_l - static_cast<float>(ip_l);
119 0 : const float delayed_l = delay_buffer_[ip_l % max_delay_samples_] * (1.0f - f_l) +
120 0 : delay_buffer_[(ip_l + 1) % max_delay_samples_] * f_l;
121 :
122 0 : delay_buffer_[write_pos_] = clamp(dry_l + feedback * delayed_l, -2.0f, 2.0f);
123 0 : left[i] = dry_l * (1.0f - mix) + delayed_l * mix;
124 :
125 : // Right LFO — 180° offset (0.5 of normalised cycle)
126 0 : const float lfo_r = 0.5f * (1.0f + std::sin(TWO_PI * (lfo_phase_ + 0.5f)));
127 0 : const float delay_samp_r =
128 0 : clamp((delay_ms + lfo_r * depth_ms) * 0.001f * static_cast<float>(sample_rate_), 1.0f,
129 0 : static_cast<float>(max_delay_samples_ - 2));
130 :
131 0 : float rp_r = static_cast<float>(write_pos_r_) - delay_samp_r;
132 0 : if (rp_r < 0.0f) rp_r += static_cast<float>(max_delay_samples_);
133 0 : const int ip_r = static_cast<int>(rp_r);
134 0 : const float f_r = rp_r - static_cast<float>(ip_r);
135 0 : const float delayed_r = delay_buffer_r_[ip_r % max_delay_samples_] * (1.0f - f_r) +
136 0 : delay_buffer_r_[(ip_r + 1) % max_delay_samples_] * f_r;
137 :
138 0 : delay_buffer_r_[write_pos_r_] = clamp(dry_r + feedback * delayed_r, -2.0f, 2.0f);
139 0 : right[i] = dry_r * (1.0f - mix) + delayed_r * mix;
140 :
141 0 : write_pos_ = (write_pos_ + 1) % max_delay_samples_;
142 0 : write_pos_r_ = (write_pos_r_ + 1) % max_delay_samples_;
143 0 : lfo_phase_ += lfo_inc;
144 0 : if (lfo_phase_ >= 1.0f) lfo_phase_ -= 1.0f;
145 0 : }
146 0 : }
147 :
148 255 : void Flanger::reset() {
149 255 : std::fill(delay_buffer_.begin(), delay_buffer_.end(), 0.0f);
150 255 : std::fill(delay_buffer_r_.begin(), delay_buffer_r_.end(), 0.0f);
151 255 : write_pos_ = 0;
152 255 : write_pos_r_ = 0;
153 255 : lfo_phase_ = 0.0f;
154 255 : }
155 :
156 : } // namespace Amplitron
|