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

            Line data    Source code
       1              : #include "audio/effects/octaver.h"
       2              : #include "audio/effects/effect_factory.h"
       3              : #include <cmath>
       4              : 
       5              : namespace Amplitron {
       6              : 
       7            2 : static EffectRegistrar<Octaver> reg("Octaver");
       8              : 
       9              : // Param indices
      10              : static constexpr int P_OCT_DOWN = 0;
      11              : static constexpr int P_OCT_UP   = 1;
      12              : static constexpr int P_DRY      = 2;
      13              : 
      14              : // Hysteresis threshold for the flip-flop zero-crossing detector.
      15              : // The divider only flips when prev_sample_ < -FLIP_HYSTERESIS and the current
      16              : // sample > +FLIP_HYSTERESIS, preventing chatter caused by near-zero noise.
      17              : //
      18              : // Value choice: 0.002 ≈ -54 dBFS — comfortably above the noise floor of a
      19              : // typical USB audio interface while remaining well below the smallest musically
      20              : // significant guitar signal.  At this threshold the per-sample slope of the
      21              : // slowest guitar fundamental (E2 ≈ 82 Hz at 48 kHz) still crosses the ±H band
      22              : // reliably, so the divider tracks pitch without false negatives.  Tune upward
      23              : // only if the target hardware has an unusually noisy input stage.
      24              : static constexpr float FLIP_HYSTERESIS = 0.002f;
      25              : 
      26           52 : Octaver::Octaver() {
      27          117 :     params_ = {
      28           39 :         {"Oct -1", 0.5f, 0.0f, 1.0f, 0.5f, "",
      29           13 :          "Level of the sub-octave (one octave below). Produces a thick, organ-like low tone."},
      30           13 :         {"Oct +1", 0.0f, 0.0f, 1.0f, 0.0f, "",
      31           13 :          "Level of the upper octave (one octave above). Adds a bright, shimmery harmonic."},
      32           13 :         {"Dry",    0.7f, 0.0f, 1.0f, 0.7f, "",
      33           13 :          "Level of the original dry signal blended with the octave voices."},
      34          117 :     };
      35           52 :     set_sample_rate(DEFAULT_SAMPLE_RATE);
      36           78 : }
      37              : 
      38           75 : void Octaver::set_sample_rate(int sample_rate) {
      39           75 :     Effect::set_sample_rate(sample_rate);
      40           50 :     reset();
      41           62 : }
      42              : 
      43          408 : void Octaver::process(float* buffer, int num_samples) {
      44          408 :     if (!enabled_) return;
      45              : 
      46              :     // One-pole smoothing coefficient (~10 ms time constant)
      47          405 :     const float alpha = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.010f));
      48              : 
      49              :     // Envelope follower coefficients
      50          405 :     const float env_attack  = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.002f));  // 2 ms
      51          405 :     const float env_release = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.020f));  // 20 ms
      52              : 
      53              :     // DC blocker coefficient (high-pass at ~20 Hz)
      54          405 :     const float dc_coeff = 1.0f - (TWO_PI * 20.0f / sample_rate_);
      55              : 
      56       205269 :     for (int i = 0; i < num_samples; ++i) {
      57       204864 :         const float dry = buffer[i];
      58              : 
      59              :         // Smooth parameters
      60       204864 :         oct_down_smooth_ += alpha * (params_[P_OCT_DOWN].value - oct_down_smooth_);
      61       204864 :         oct_up_smooth_   += alpha * (params_[P_OCT_UP].value   - oct_up_smooth_);
      62       204864 :         dry_smooth_      += alpha * (params_[P_DRY].value      - dry_smooth_);
      63              : 
      64              :         // --- Envelope follower (tracks input amplitude) ---
      65       204864 :         const float abs_in = std::fabs(dry);
      66       204864 :         if (abs_in > envelope_) {
      67        79845 :             envelope_ += env_attack * (abs_in - envelope_);
      68        26615 :         } else {
      69       125019 :             envelope_ += env_release * (abs_in - envelope_);
      70              :         }
      71              : 
      72              :         // --- Oct-1: Flip-flop divider ---
      73              :         // Detect positive-going zero crossing with hysteresis: require the
      74              :         // previous sample to have been clearly negative and the current sample
      75              :         // to be clearly positive before toggling.  This suppresses chatter from
      76              :         // near-zero noise that would otherwise cause false flips.
      77       204864 :         if (prev_sample_ < -FLIP_HYSTERESIS && dry > FLIP_HYSTERESIS) {
      78         1899 :             flipflop_ = -flipflop_;
      79          633 :         }
      80       204864 :         prev_sample_ = dry;
      81              : 
      82              :         // Square wave at half frequency, shaped by envelope
      83       204864 :         float oct_down = flipflop_ * envelope_;
      84              : 
      85              :         // --- Oct+1: Full-wave rectification + DC removal ---
      86       204864 :         float rectified = std::fabs(dry);
      87              : 
      88              :         // DC blocker (1st-order high-pass): removes the DC offset from rectification
      89       204864 :         float dc_out = rectified - dc_x1_ + dc_coeff * dc_y1_;
      90       204864 :         dc_x1_ = rectified;
      91       204864 :         dc_y1_ = dc_out;
      92              : 
      93       204864 :         float oct_up = dc_out;
      94              : 
      95              :         // --- Mix ---
      96       273152 :         buffer[i] = dry * dry_smooth_
      97       204864 :                    + oct_down * oct_down_smooth_
      98       204864 :                    + oct_up * oct_up_smooth_;
      99              : 
     100              :         // Safety clamp
     101       204864 :         buffer[i] = clamp(buffer[i], -1.0f, 1.0f);
     102        68288 :     }
     103          136 : }
     104              : 
     105          192 : void Octaver::reset() {
     106          192 :     prev_sample_ = 0.0f;
     107          192 :     flipflop_ = 1.0f;
     108          192 :     dc_x1_ = 0.0f;
     109          192 :     dc_y1_ = 0.0f;
     110          192 :     envelope_ = 0.0f;
     111          192 :     oct_down_smooth_ = params_[P_OCT_DOWN].value;
     112          192 :     oct_up_smooth_ = params_[P_OCT_UP].value;
     113          192 :     dry_smooth_ = params_[P_DRY].value;
     114          192 : }
     115              : 
     116              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1