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
|