LCOV - code coverage report
Current view: top level - src/audio/effects/modulation - phaser.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 56.3 % 103 58
Test Date: 2026-06-07 15:51:50 Functions: 80.0 % 5 4

            Line data    Source code
       1              : #include "audio/effects/modulation/phaser.h"
       2              : 
       3              : #include <cmath>
       4              : 
       5              : #include "audio/effects/core/effect_factory.h"
       6              : 
       7              : namespace Amplitron {
       8              : 
       9            2 : static EffectRegistrar<Phaser> reg("Phaser");
      10              : 
      11              : // Param indices
      12              : static constexpr int P_RATE = 0;
      13              : static constexpr int P_DEPTH = 1;
      14              : static constexpr int P_STAGES = 2;
      15              : static constexpr int P_FEEDBACK = 3;
      16              : static constexpr int P_MIX = 4;
      17              : 
      18              : // Stage count table: Stages param 0..3 -> 4/6/8/12 stages
      19              : static constexpr int STAGE_COUNTS[4] = {4, 6, 8, 12};
      20              : 
      21           76 : Phaser::Phaser() {
      22          247 :     params_ = {
      23           57 :         {"Rate", 0.5f, 0.05f, 10.0f, 0.5f, "Hz",
      24           19 :          "LFO speed. Low values create a slow, hypnotic sweep; high values create a fast "
      25              :          "vibrato-like effect."},
      26           19 :         {"Depth", 0.7f, 0.0f, 1.0f, 0.7f, "",
      27           19 :          "Modulation depth. Controls how wide the all-pass frequency sweeps across the spectrum."},
      28           19 :         {"Stages", 0.0f, 0.0f, 3.0f, 0.0f, "",
      29           19 :          "Number of all-pass stages: 0=4 (Phase 90), 1=6, 2=8, 3=12. More stages add more "
      30              :          "notches."},
      31           19 :         {"Feedback", 0.5f, 0.0f, 0.95f, 0.5f, "",
      32           19 :          "Feeds the chain output back to the input, adding resonance and intensity to the phasing "
      33              :          "notches."},
      34           19 :         {"Mix", 0.5f, 0.0f, 1.0f, 0.5f, "",
      35           19 :          "Dry/wet blend. At 0.5 the notch effect is most pronounced (classic phaser mix point)."},
      36          247 :     };
      37           76 :     set_sample_rate(DEFAULT_SAMPLE_RATE);
      38          114 : }
      39              : 
      40          108 : void Phaser::set_sample_rate(int sample_rate) {
      41          108 :     Effect::set_sample_rate(sample_rate);
      42          108 :     reset();
      43           89 : }
      44              : 
      45           45 : void Phaser::process(float* buffer, int num_samples) {
      46           45 :     if (!enabled_) return;
      47              : 
      48           42 :     const float rate = params_[P_RATE].value;
      49           42 :     const float depth = params_[P_DEPTH].value;
      50           42 :     const int nstages = STAGE_COUNTS[(int)clamp(params_[P_STAGES].value + 0.5f, 0.0f, 3.0f)];
      51           42 :     const float feedback = params_[P_FEEDBACK].value;
      52           42 :     const float mix = params_[P_MIX].value;
      53              : 
      54           42 :     const float lfo_inc = rate / static_cast<float>(sample_rate_);
      55              :     // Logarithmic sweep: base_freq * exp(lfo * depth * ln(ratio))
      56              :     // fc range: ~200 Hz (lfo=0) to ~4000 Hz (lfo=1, depth=1)
      57           42 :     const float log_ratio = std::log(20.0f);  // ln(4000/200)
      58              : 
      59        15402 :     for (int i = 0; i < num_samples; ++i) {
      60        15360 :         const float dry = buffer[i];
      61              : 
      62              :         // LFO in [0, 1]
      63        15360 :         const float lfo = 0.5f * (1.0f + std::sin(TWO_PI * lfo_phase_));
      64              : 
      65              :         // Modulated all-pass corner frequency (log sweep)
      66        20480 :         const float fc = clamp(200.0f * std::exp(lfo * depth * log_ratio), 80.0f,
      67        10240 :                                static_cast<float>(sample_rate_) * 0.40f);
      68              : 
      69              :         // 1st-order all-pass coefficient: c = (tan(π*fc/fs) - 1) / (tan(π*fc/fs) + 1)
      70        15360 :         const float t = std::tan(3.14159265f * fc / static_cast<float>(sample_rate_));
      71        15360 :         const float apc = (t - 1.0f) / (t + 1.0f);
      72              : 
      73              :         // Feed input + feedback into the all-pass cascade
      74        15360 :         float x = dry + feedback * feedback_state_;
      75              : 
      76       125952 :         for (int s = 0; s < nstages; ++s) {
      77              :             // y[n] = c * (x[n] - y[n-1]) + x[n-1]
      78       110592 :             const float y = apc * (x - apf_yprev_[s]) + apf_xprev_[s];
      79       110592 :             apf_xprev_[s] = x;
      80       110592 :             apf_yprev_[s] = y;
      81       110592 :             x = y;
      82        36864 :         }
      83              : 
      84        15360 :         feedback_state_ = x;
      85              : 
      86        15360 :         buffer[i] = dry * (1.0f - mix) + x * mix;
      87              : 
      88        15360 :         lfo_phase_ += lfo_inc;
      89        15360 :         if (lfo_phase_ >= 1.0f) lfo_phase_ -= 1.0f;
      90         5120 :     }
      91           15 : }
      92              : 
      93            0 : void Phaser::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 = params_[P_DEPTH].value;
     100            0 :     const int nstages = STAGE_COUNTS[(int)clamp(params_[P_STAGES].value + 0.5f, 0.0f, 3.0f)];
     101            0 :     const float feedback = params_[P_FEEDBACK].value;
     102            0 :     const float mix = params_[P_MIX].value;
     103              : 
     104            0 :     const float lfo_inc = rate / static_cast<float>(sample_rate_);
     105            0 :     const float log_ratio = std::log(20.0f);
     106              : 
     107            0 :     for (int i = 0; i < num_samples; ++i) {
     108            0 :         const float dry_l = left[i];
     109            0 :         const float dry_r = right[i];
     110              : 
     111              :         // Left LFO
     112            0 :         const float lfo_l = 0.5f * (1.0f + std::sin(TWO_PI * lfo_phase_));
     113            0 :         const float fc_l = clamp(200.0f * std::exp(lfo_l * depth * log_ratio), 80.0f,
     114            0 :                                  static_cast<float>(sample_rate_) * 0.40f);
     115            0 :         const float t_l = std::tan(3.14159265f * fc_l / static_cast<float>(sample_rate_));
     116            0 :         const float apc_l = (t_l - 1.0f) / (t_l + 1.0f);
     117              : 
     118              :         // Right LFO — 180° offset (0.5 of normalised cycle)
     119            0 :         const float lfo_r = 0.5f * (1.0f + std::sin(TWO_PI * (lfo_phase_ + 0.5f)));
     120            0 :         const float fc_r = clamp(200.0f * std::exp(lfo_r * depth * log_ratio), 80.0f,
     121            0 :                                  static_cast<float>(sample_rate_) * 0.40f);
     122            0 :         const float t_r = std::tan(3.14159265f * fc_r / static_cast<float>(sample_rate_));
     123            0 :         const float apc_r = (t_r - 1.0f) / (t_r + 1.0f);
     124              : 
     125              :         // Left APF cascade
     126            0 :         float x_l = dry_l + feedback * feedback_state_;
     127            0 :         for (int s = 0; s < nstages; ++s) {
     128            0 :             const float y = apc_l * (x_l - apf_yprev_[s]) + apf_xprev_[s];
     129            0 :             apf_xprev_[s] = x_l;
     130            0 :             apf_yprev_[s] = y;
     131            0 :             x_l = y;
     132            0 :         }
     133            0 :         feedback_state_ = x_l;
     134              : 
     135              :         // Right APF cascade
     136            0 :         float x_r = dry_r + feedback * feedback_state_r_;
     137            0 :         for (int s = 0; s < nstages; ++s) {
     138            0 :             const float y = apc_r * (x_r - apf_yprev_r_[s]) + apf_xprev_r_[s];
     139            0 :             apf_xprev_r_[s] = x_r;
     140            0 :             apf_yprev_r_[s] = y;
     141            0 :             x_r = y;
     142            0 :         }
     143            0 :         feedback_state_r_ = x_r;
     144              : 
     145            0 :         left[i] = dry_l * (1.0f - mix) + x_l * mix;
     146            0 :         right[i] = dry_r * (1.0f - mix) + x_r * mix;
     147              : 
     148            0 :         lfo_phase_ += lfo_inc;
     149            0 :         if (lfo_phase_ >= 1.0f) lfo_phase_ -= 1.0f;
     150            0 :     }
     151            0 : }
     152              : 
     153          168 : void Phaser::reset() {
     154          168 :     lfo_phase_ = 0.0f;
     155          168 :     feedback_state_ = 0.0f;
     156          168 :     feedback_state_r_ = 0.0f;
     157          168 :     apf_xprev_.fill(0.0f);
     158          168 :     apf_yprev_.fill(0.0f);
     159          168 :     apf_xprev_r_.fill(0.0f);
     160          168 :     apf_yprev_r_.fill(0.0f);
     161          168 : }
     162              : 
     163              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1