LCOV - code coverage report
Current view: top level - src/audio/effects/pitch - octaver.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 100.0 % 56 56
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 4 4

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