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
|