Line data Source code
1 : #include "audio/effects/utility/tuner.h"
2 :
3 : #include <cmath>
4 : #include <cstring>
5 :
6 : #include "audio/effects/core/effect_factory.h"
7 :
8 : namespace Amplitron {
9 :
10 2 : static EffectRegistrar<TunerPedal> reg("Tuner");
11 :
12 : static const char* NOTE_NAMES[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
13 :
14 24 : const char* TunerPedal::note_name(int note_index) {
15 24 : if (note_index < 0 || note_index > 11) return "?";
16 21 : return NOTE_NAMES[note_index];
17 8 : }
18 :
19 116 : TunerPedal::TunerPedal() {
20 203 : params_ = {
21 87 : {"Mute", 1.0f, 0.0f, 1.0f, 1.0f, "",
22 29 : "When fully engaged (1.0), the tuner silences the audio output while you tune."},
23 29 : {"A4 Ref", 440.0f, 430.0f, 450.0f, 440.0f, "Hz",
24 29 : "Calibration frequency for the note A4. Default is standard 440Hz."},
25 203 : };
26 58 : yin_buffer_.resize(YIN_BUFFER_SIZE, 0.0f);
27 87 : yin_buf_.resize(YIN_BUFFER_SIZE, 0.0f);
28 87 : yin_d_.resize(YIN_BUFFER_SIZE / 2, 0.0f);
29 116 : recalc_update_interval();
30 203 : }
31 :
32 48 : void TunerPedal::set_sample_rate(int sample_rate) {
33 48 : sample_rate_ = sample_rate;
34 64 : recalc_update_interval();
35 48 : }
36 :
37 135 : void TunerPedal::recalc_update_interval() {
38 : // ~15 updates per second for responsive display
39 135 : update_interval_ = sample_rate_ / 15;
40 135 : if (update_interval_ < YIN_BUFFER_SIZE) update_interval_ = YIN_BUFFER_SIZE;
41 90 : }
42 :
43 48 : void TunerPedal::reset() {
44 48 : std::fill(yin_buffer_.begin(), yin_buffer_.end(), 0.0f);
45 48 : yin_write_pos_ = 0;
46 48 : yin_buffer_full_ = false;
47 48 : samples_since_update_ = 0;
48 48 : detected_freq.store(0.0f, std::memory_order_relaxed);
49 48 : detected_cents.store(0.0f, std::memory_order_relaxed);
50 48 : detected_note.store(-1, std::memory_order_relaxed);
51 48 : detected_octave.store(-1, std::memory_order_relaxed);
52 48 : signal_detected.store(false, std::memory_order_relaxed);
53 48 : }
54 :
55 4575 : void TunerPedal::process(float* buffer, int num_samples) {
56 4575 : if (!enabled_) return;
57 :
58 4575 : float a4_ref = params_[1].value;
59 4575 : bool mute = params_[0].value >= 0.5f;
60 :
61 : // Accumulate samples into YIN buffer
62 1175007 : for (int i = 0; i < num_samples; ++i) {
63 1170432 : yin_buffer_[yin_write_pos_] = buffer[i];
64 1170432 : yin_write_pos_++;
65 1170432 : if (yin_write_pos_ >= YIN_BUFFER_SIZE) {
66 267 : yin_write_pos_ = 0;
67 267 : yin_buffer_full_ = true;
68 89 : }
69 390144 : }
70 :
71 4575 : samples_since_update_ += num_samples;
72 :
73 : // Run pitch detection at the update interval when buffer is full
74 4575 : if (yin_buffer_full_ && samples_since_update_ >= update_interval_) {
75 267 : samples_since_update_ = 0;
76 :
77 267 : float freq = yin_detect_pitch(a4_ref);
78 267 : if (freq > 0.0f) {
79 231 : detected_freq.store(freq, std::memory_order_relaxed);
80 231 : signal_detected.store(true, std::memory_order_relaxed);
81 231 : freq_to_note(freq, a4_ref);
82 77 : } else {
83 36 : signal_detected.store(false, std::memory_order_relaxed);
84 : }
85 89 : }
86 :
87 : // Mute output when tuner is active (standard hardware tuner behavior)
88 4575 : if (mute) {
89 1188 : std::memset(buffer, 0, static_cast<size_t>(num_samples) * sizeof(float));
90 396 : }
91 1525 : }
92 :
93 : // ============================================================
94 : // YIN pitch detection algorithm
95 : // Reference: de Cheveigné & Kawahara, 2002
96 : // ============================================================
97 :
98 267 : float TunerPedal::yin_detect_pitch(float /*a4_ref*/) {
99 : // W = integration window length. Use half the buffer.
100 267 : const int W = YIN_BUFFER_SIZE / 2;
101 :
102 : // Linearize the circular buffer into preallocated member (no heap alloc)
103 1093899 : for (int i = 0; i < YIN_BUFFER_SIZE; ++i) {
104 1093632 : yin_buf_[i] = yin_buffer_[(yin_write_pos_ + i) % YIN_BUFFER_SIZE];
105 364544 : }
106 :
107 : // Check if there's enough signal energy (RMS gate)
108 178 : float energy = 0.0f;
109 1093899 : for (int i = 0; i < YIN_BUFFER_SIZE; ++i) energy += yin_buf_[i] * yin_buf_[i];
110 267 : float rms_val = std::sqrt(energy / YIN_BUFFER_SIZE);
111 267 : if (rms_val < 0.01f) return -1.0f; // Too quiet — no pitch
112 :
113 : // Step 1 & 2: Difference function d(tau) and cumulative mean normalized
114 : // difference function d'(tau)
115 : // Reuse preallocated member (no heap alloc)
116 231 : std::fill(yin_d_.begin(), yin_d_.begin() + W, 0.0f);
117 :
118 : // d'(0) is defined as 1
119 231 : yin_d_[0] = 1.0f;
120 :
121 231 : float running_sum = 0.0f;
122 :
123 473088 : for (int tau = 1; tau < W; ++tau) {
124 315238 : float diff = 0.0f;
125 968883993 : for (int j = 0; j < W; ++j) {
126 968411136 : float delta = yin_buf_[j] - yin_buf_[j + tau];
127 968411136 : diff += delta * delta;
128 322803712 : }
129 :
130 : // Cumulative mean normalized difference
131 472857 : running_sum += diff;
132 472857 : yin_d_[tau] = (running_sum > 0.0f) ? (diff * tau / running_sum) : 1.0f;
133 157619 : }
134 :
135 : // Step 3: Absolute threshold — find the first dip below threshold
136 : // then pick the deepest local minimum within that dip.
137 231 : constexpr float YIN_THRESHOLD = 0.20f;
138 :
139 : // Minimum lag: highest detectable frequency ~2000Hz
140 231 : int min_tau = sample_rate_ / 2000;
141 231 : if (min_tau < 2) min_tau = 2;
142 :
143 : // Maximum lag: lowest detectable frequency ~60Hz (below low E)
144 231 : int max_tau = sample_rate_ / 60;
145 231 : if (max_tau >= W) max_tau = W - 1;
146 :
147 : // Search strategy: find the first tau below threshold, then walk to
148 : // the local minimum. This avoids sub-harmonic false positives by
149 : // preferring the earliest qualifying dip (highest frequency candidate).
150 231 : int best_tau = -1;
151 54828 : for (int tau = min_tau; tau < max_tau; ++tau) {
152 54828 : if (yin_d_[tau] < YIN_THRESHOLD) {
153 : // Walk to the local minimum of this valley
154 154 : int valley_min = tau;
155 7377 : while (tau + 1 < max_tau && yin_d_[tau + 1] < yin_d_[tau]) {
156 4764 : ++tau;
157 : }
158 : // tau now points at the local minimum (or end of descent)
159 154 : valley_min = tau;
160 154 : best_tau = valley_min;
161 154 : break;
162 : }
163 18199 : }
164 :
165 : // Fallback: if no dip was below threshold, take the global minimum
166 231 : if (best_tau < 1) {
167 0 : float global_min = 2.0f;
168 0 : for (int tau = min_tau; tau < max_tau; ++tau) {
169 0 : if (yin_d_[tau] < global_min) {
170 0 : global_min = yin_d_[tau];
171 0 : best_tau = tau;
172 0 : }
173 0 : }
174 : // Only accept if reasonably periodic
175 0 : if (global_min > 0.5f) return -1.0f;
176 0 : }
177 :
178 154 : if (best_tau < 1) return -1.0f;
179 :
180 : // Step 4: Parabolic interpolation for sub-sample accuracy
181 231 : float refined_tau = static_cast<float>(best_tau);
182 231 : if (best_tau > min_tau && best_tau < W - 1) {
183 231 : float s0 = yin_d_[best_tau - 1];
184 231 : float s1 = yin_d_[best_tau];
185 231 : float s2 = yin_d_[best_tau + 1];
186 : // Only interpolate if it's a true local minimum
187 231 : if (s0 > s1 && s2 > s1) {
188 231 : float denom = 2.0f * (s0 - 2.0f * s1 + s2);
189 231 : if (std::fabs(denom) > 1e-12f) {
190 231 : refined_tau += (s0 - s2) / denom;
191 77 : }
192 77 : }
193 77 : }
194 :
195 231 : if (refined_tau <= 0.0f) return -1.0f;
196 :
197 231 : float freq = static_cast<float>(sample_rate_) / refined_tau;
198 :
199 : // Sanity check: guitar range ~60Hz to ~1400Hz (high frets on high E)
200 231 : if (freq < 60.0f || freq > 1400.0f) return -1.0f;
201 :
202 154 : return freq;
203 89 : }
204 :
205 231 : void TunerPedal::freq_to_note(float freq, float a4_ref) {
206 : // Semitones from A4
207 231 : float semitones_from_a4 = 12.0f * std::log2(freq / a4_ref);
208 :
209 : // Nearest semitone
210 231 : int nearest = static_cast<int>(std::round(semitones_from_a4));
211 :
212 : // Cents deviation from nearest note
213 231 : float cents = (semitones_from_a4 - static_cast<float>(nearest)) * 100.0f;
214 :
215 : // A4 = MIDI note 69 (note index 9 = A, octave 4)
216 231 : int midi_note = 69 + nearest;
217 231 : int note_index = ((midi_note % 12) + 12) % 12; // 0=C .. 11=B
218 231 : int octave = (midi_note / 12) - 1;
219 :
220 231 : detected_note.store(note_index, std::memory_order_relaxed);
221 231 : detected_octave.store(octave, std::memory_order_relaxed);
222 231 : detected_cents.store(cents, std::memory_order_relaxed);
223 231 : }
224 :
225 : } // namespace Amplitron
|