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

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

Generated by: LCOV version 2.0-1