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

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

Generated by: LCOV version 2.0-1