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