LCOV - code coverage report
Current view: top level - src/audio/effects - pitch_shifter.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 100.0 % 66 66
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 6 6

            Line data    Source code
       1              : #include "audio/effects/pitch_shifter.h"
       2              : #include "audio/effects/effect_factory.h"
       3              : #include <cmath>
       4              : 
       5              : namespace Amplitron {
       6              : 
       7            2 : static EffectRegistrar<PitchShifter> reg("Pitch Shifter");
       8              : 
       9              : // Param indices
      10              : static constexpr int P_SHIFT = 0;
      11              : static constexpr int P_FINE  = 1;
      12              : static constexpr int P_MIX   = 2;
      13              : 
      14              : // Grain window size in seconds (~23 ms at 48kHz = 1024 samples)
      15              : static constexpr float GRAIN_WINDOW_SEC = 0.023f;
      16              : 
      17       261120 : static float wrap_phase(float phase, int buf_size) {
      18       261120 :     phase = std::fmod(phase, static_cast<float>(buf_size));
      19       261120 :     if (phase < 0.0f) phase += static_cast<float>(buf_size);
      20       261120 :     return phase;
      21              : }
      22              : 
      23           40 : PitchShifter::PitchShifter() {
      24           90 :     params_ = {
      25           30 :         {"Shift", 0.0f, -12.0f, 12.0f, 0.0f, "st",
      26           10 :          "Pitch shift in semitones. Negative shifts down, positive shifts up. 12 = one octave."},
      27           10 :         {"Fine",  0.0f, -50.0f, 50.0f, 0.0f, "ct",
      28           10 :          "Fine-tune adjustment in cents (hundredths of a semitone) for precise detuning."},
      29           10 :         {"Mix",   0.0f,   0.0f,  1.0f, 0.0f, "",
      30           10 :          "Dry/wet blend. 0 = fully dry, 1 = fully pitch-shifted."},
      31           90 :     };
      32           30 :     set_sample_rate(DEFAULT_SAMPLE_RATE);
      33           60 : }
      34              : 
      35           57 : void PitchShifter::set_sample_rate(int sample_rate) {
      36           57 :     Effect::set_sample_rate(sample_rate);
      37           57 :     buf_size_ = static_cast<int>(sample_rate * GRAIN_WINDOW_SEC * 2.0f);
      38           57 :     if (buf_size_ < 256) buf_size_ = 256;
      39           57 :     grain_buf_.assign(buf_size_, 0.0f);
      40           57 :     reset();
      41           57 : }
      42              : 
      43       130560 : float PitchShifter::read_linear(float phase) const {
      44       130560 :     phase = wrap_phase(phase, buf_size_);
      45              : 
      46       130560 :     int pos0 = static_cast<int>(phase);
      47       130560 :     int pos1 = (pos0 + 1) % buf_size_;
      48       130560 :     float frac = phase - static_cast<float>(pos0);
      49       130560 :     return grain_buf_[pos0] * (1.0f - frac) + grain_buf_[pos1] * frac;
      50              : }
      51              : 
      52          120 : void PitchShifter::process(float* buffer, int num_samples) {
      53          120 :     if (!enabled_) return;
      54              : 
      55          120 :     const float alpha = 1.0f - std::exp(-1.0f / (sample_rate_ * 0.010f));
      56              : 
      57        65400 :     for (int i = 0; i < num_samples; ++i) {
      58        65280 :         const float dry = buffer[i];
      59              : 
      60              :         // Write input into circular grain buffer
      61        65280 :         grain_buf_[write_pos_] = dry;
      62              : 
      63              :         // Smooth parameters
      64        65280 :         shift_smooth_ += alpha * (params_[P_SHIFT].value - shift_smooth_);
      65        65280 :         fine_smooth_  += alpha * (params_[P_FINE].value  - fine_smooth_);
      66        65280 :         mix_smooth_   += alpha * (params_[P_MIX].value   - mix_smooth_);
      67              : 
      68              :         // Total shift in semitones (coarse + fine)
      69        65280 :         float total_semitones = shift_smooth_ + fine_smooth_ / 100.0f;
      70              : 
      71              :         // Pitch ratio: 2^(semitones/12)
      72        65280 :         float ratio = std::pow(2.0f, total_semitones / 12.0f);
      73              : 
      74              :         // Read pointer increment: how much faster/slower we read vs write
      75              :         // ratio > 1 means pitch up -> read faster -> increment > 1
      76              :         // We want the *offset* from write to change, so increment = 1 - ratio
      77              :         // gives us the drift rate of the read pointer relative to write.
      78        65280 :         float drift = 1.0f - ratio;
      79              : 
      80              :         // Advance read phases (they drift relative to write position)
      81        65280 :         read_phase_a_ += drift;
      82        65280 :         read_phase_b_ += drift;
      83              : 
      84              :         // Wrap phases into [0, buf_size_) in constant time.
      85        65280 :         read_phase_a_ = wrap_phase(read_phase_a_, buf_size_);
      86        65280 :         read_phase_b_ = wrap_phase(read_phase_b_, buf_size_);
      87              : 
      88              :         // Compute absolute read positions in the buffer
      89        65280 :         float pos_a = static_cast<float>(write_pos_) - read_phase_a_;
      90        65280 :         float pos_b = static_cast<float>(write_pos_) - read_phase_b_;
      91              : 
      92              :         // Read from both taps with linear interpolation
      93        65280 :         float tap_a = read_linear(pos_a);
      94        65280 :         float tap_b = read_linear(pos_b);
      95              : 
      96              :         // Raised-cosine crossfade based on distance between taps
      97              :         // Tap A and B are offset by half_buf; crossfade so the one closer
      98              :         // to write_pos_ is louder (its grain is fresher).
      99              :         // Use read_phase_a_ as the crossfade driver: as it sweeps 0..buf_size_,
     100              :         // we fade A in for the first half and B in for the second half.
     101        65280 :         float fade_pos = read_phase_a_ / static_cast<float>(buf_size_);
     102              :         // Hann window for smooth crossfade
     103        65280 :         float gain_a = 0.5f * (1.0f - std::cos(TWO_PI * fade_pos));
     104        65280 :         float gain_b = 1.0f - gain_a;
     105              : 
     106        65280 :         float wet = tap_a * gain_a + tap_b * gain_b;
     107              : 
     108              :         // Dry/wet mix
     109        65280 :         buffer[i] = dry * (1.0f - mix_smooth_) + wet * mix_smooth_;
     110              : 
     111              :         // Advance write position
     112        65280 :         write_pos_ = (write_pos_ + 1) % buf_size_;
     113        21760 :     }
     114           40 : }
     115              : 
     116           84 : void PitchShifter::reset() {
     117           84 :     std::fill(grain_buf_.begin(), grain_buf_.end(), 0.0f);
     118           84 :     write_pos_ = 0;
     119              :     // Start tap B offset by half the buffer from tap A
     120           84 :     read_phase_a_ = 0.0f;
     121           84 :     read_phase_b_ = static_cast<float>(buf_size_) * 0.5f;
     122           84 :     shift_smooth_ = params_[P_SHIFT].value;
     123           84 :     fine_smooth_ = params_[P_FINE].value;
     124           84 :     mix_smooth_ = params_[P_MIX].value;
     125           84 : }
     126              : 
     127              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1