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 % 75 75
Test Date: 2026-06-03 09:13:19 Functions: 100.0 % 7 7

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

Generated by: LCOV version 2.0-1