LCOV - code coverage report
Current view: top level - src/audio/effects - flanger.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 58.0 % 100 58
Test Date: 2026-06-03 09:13:19 Functions: 80.0 % 5 4

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

Generated by: LCOV version 2.0-1