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
|