Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
yin_tracker.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <cmath>
8#include <cstddef>
9#include <vector>
10
11namespace pulp::signal {
12
13// ── Stage 1 — YIN pitch tracking ──────────────────────────────────────────
14
29template <typename SampleType = float>
31public:
35 static constexpr double kF0MinDefault = 80.0;
36 static constexpr double kF0MinLimit = 50.0;
37 static constexpr double kF0MaxLimit = 2000.0;
38
41 static constexpr double kF0MaxDefault = 1000.0;
42
46 static constexpr double kYinThreshold = 0.10;
47
50 static constexpr int kHop = 256;
51
54 static constexpr int kMedianTaps = 3;
55 static constexpr int kMaxMedianTaps = 5;
56
57 void prepare(double sample_rate) {
58 sample_rate_ = (std::isfinite(sample_rate) && sample_rate > 0.0)
59 ? sample_rate
60 : 48000.0;
61 // W, tau_min and tau_max are DERIVED here, never literals.
62 tau_min_ = static_cast<int>(std::ceil(sample_rate_ / f0_max_));
63 tau_max_ = static_cast<int>(std::ceil(sample_rate_ / f0_min_));
64 tau_min_ = std::max(tau_min_, 2);
65 tau_max_ = std::max(tau_max_, tau_min_ + 1);
66 window_ = 2 * tau_max_;
67
68 // The integration length is what makes the buffer self-consistent: the
69 // difference function reads `x[j + tau]` for j < integration_ and
70 // tau <= tau_max_, so the largest index touched is
71 // integration_ - 1 + tau_max_ = window_ - 1 — the last sample in the
72 // window. Integrating over the FULL window instead (as the difference
73 // function is often written) would read tau_max_ samples past the end
74 // of the very window whose length the latency is derived from.
75 integration_ = window_ - tau_max_;
76
77 ring_.assign(static_cast<std::size_t>(window_), 0.0);
78 scratch_.assign(static_cast<std::size_t>(window_), 0.0);
79 diff_.assign(static_cast<std::size_t>(tau_max_) + 1, 0.0);
80 cmnd_.assign(static_cast<std::size_t>(tau_max_) + 1, 0.0);
81 reset();
82 }
83
84 void reset() {
85 std::fill(ring_.begin(), ring_.end(), 0.0);
86 valid_samples_ = 0;
87 write_ = 0;
88 hop_counter_ = 0;
89 median_count_ = 0;
90 median_index_ = 0;
91 f0_hz_ = 0.0;
92 tau_ = 0.0;
93 voiced_ = false;
94 min_cmnd_ = 1.0;
95 }
96
98 void discard_history() noexcept {
99 write_ = 0;
100 valid_samples_ = 0;
101 hop_counter_ = 0;
102 median_count_ = 0;
103 median_index_ = 0;
104 f0_hz_ = 0.0;
105 tau_ = 0.0;
106 voiced_ = false;
107 min_cmnd_ = 1.0;
108 }
109
114 void set_f0_range(double min_hz, double max_hz) {
115 if (!std::isfinite(min_hz) || !std::isfinite(max_hz)) return;
116 const double lo = std::clamp(min_hz, kF0MinLimit, kF0MaxLimit);
117 const double hi = std::clamp(max_hz, kF0MinLimit, kF0MaxLimit);
118 if (hi <= lo) return;
119 f0_min_ = lo;
120 f0_max_ = hi;
121 }
122
123 double f0_min_hz() const { return f0_min_; }
124 double f0_max_hz() const { return f0_max_; }
125
128 bool process(SampleType x) {
129 if (window_ <= 0) return false;
130 if (!std::isfinite(static_cast<double>(x))) {
132 return false;
133 }
134 ring_[static_cast<std::size_t>(write_)] = static_cast<double>(x);
135 write_ = (write_ + 1) % window_;
136 valid_samples_ = std::min(valid_samples_ + 1, window_);
137 if (++hop_counter_ < kHop) return false;
138 hop_counter_ = 0;
139 analyse();
140 return true;
141 }
142
145 double f0_hz() const { return f0_hz_; }
146
148 double tau_samples() const { return tau_; }
149
151 bool voiced() const { return voiced_; }
152
154 double min_cmnd() const { return min_cmnd_; }
155
159 int latency_samples() const { return window_; }
160
161 int window_samples() const { return window_; }
162 int integration_samples() const { return integration_; }
163 int tau_min() const { return tau_min_; }
164 int tau_max() const { return tau_max_; }
165 static constexpr int hop_samples() { return kHop; }
166
169 double cost_mac_per_sample() const {
170 return static_cast<double>(tau_max_) * static_cast<double>(integration_) /
171 static_cast<double>(kHop);
172 }
173
174private:
177 void analyse() {
178 // Unroll the ring into a linear window so the difference function is a
179 // flat scan rather than `tau_max * integration` modulo operations.
180 const int missing = window_ - valid_samples_;
181 for (int i = 0; i < window_; ++i) {
182 scratch_[static_cast<std::size_t>(i)] =
183 i < missing ? 0.0
184 : ring_[static_cast<std::size_t>((write_ + i) % window_)];
185 }
186
187 // [1] Eq. 6 — the difference function. Computed from tau = 1, not from
188 // tau_min: the CMND denominator below is a running mean over ALL lags
189 // from 1, so starting at tau_min would normalise against a truncated
190 // sum and shift every d' value.
191 diff_[0] = 0.0;
192 for (int tau = 1; tau <= tau_max_; ++tau) {
193 double sum = 0.0;
194 for (int j = 0; j < integration_; ++j) {
195 const double d = scratch_[static_cast<std::size_t>(j)] -
196 scratch_[static_cast<std::size_t>(j + tau)];
197 sum += d * d;
198 }
199 diff_[static_cast<std::size_t>(tau)] = sum;
200 }
201
202 // [1] Eq. 8 — cumulative mean normalized difference. This is YIN's key
203 // step: it removes plain autocorrelation's "zero lag always wins" bias
204 // and leaves a ~1.0 baseline with dips at the period and its multiples.
205 cmnd_[0] = 1.0;
206 double running = 0.0;
207 for (int tau = 1; tau <= tau_max_; ++tau) {
208 running += diff_[static_cast<std::size_t>(tau)];
209 cmnd_[static_cast<std::size_t>(tau)] =
210 running > 0.0 ? diff_[static_cast<std::size_t>(tau)] *
211 static_cast<double>(tau) / running
212 : 1.0;
213 }
214
215 // [1] Eq. 9 — absolute threshold. Take the FIRST dip below the
216 // threshold and descend to its bottom, rather than the global minimum,
217 // which is what stops a period multiple from winning.
218 int best = -1;
219 for (int tau = tau_min_; tau <= tau_max_; ++tau) {
220 if (cmnd_[static_cast<std::size_t>(tau)] < kYinThreshold) {
221 while (tau + 1 <= tau_max_ &&
222 cmnd_[static_cast<std::size_t>(tau + 1)] <
223 cmnd_[static_cast<std::size_t>(tau)])
224 ++tau;
225 best = tau;
226 break;
227 }
228 }
229
230 double lowest = cmnd_[static_cast<std::size_t>(tau_min_)];
231 if (best < 0) {
232 int arg = tau_min_;
233 for (int tau = tau_min_ + 1; tau <= tau_max_; ++tau) {
234 if (cmnd_[static_cast<std::size_t>(tau)] < lowest) {
235 lowest = cmnd_[static_cast<std::size_t>(tau)];
236 arg = tau;
237 }
238 }
239 best = arg;
240 } else {
241 lowest = cmnd_[static_cast<std::size_t>(best)];
242 }
243 min_cmnd_ = lowest;
244
245 // §3.5 voicing gate.
246 voiced_ = lowest <= kYinThreshold;
247 if (!voiced_) return; // hold the last f0
248
249 // [1] step 5 — parabolic interpolation on the CMND around the dip.
250 const double refined = parabolic(best);
251
252 // The octave-error median (ORIGINAL heuristic, not [1] step 6).
253 median_buffer_[static_cast<std::size_t>(median_index_)] = refined;
254 median_index_ = (median_index_ + 1) % kMedianTaps;
255 if (median_count_ < kMedianTaps) ++median_count_;
256
257 tau_ = median_of_buffer();
258 f0_hz_ = tau_ > 0.0 ? sample_rate_ / tau_ : 0.0;
259 }
260
264 double parabolic(int tau) const {
265 if (tau <= tau_min_ || tau >= tau_max_) return static_cast<double>(tau);
266 const double a = cmnd_[static_cast<std::size_t>(tau - 1)];
267 const double b = cmnd_[static_cast<std::size_t>(tau)];
268 const double c = cmnd_[static_cast<std::size_t>(tau + 1)];
269 const double denom = a - 2.0 * b + c;
270 if (!(std::abs(denom) > 1e-18)) return static_cast<double>(tau);
271 const double offset = 0.5 * (a - c) / denom;
272 // A correct parabolic vertex on a genuine minimum lands inside ±0.5;
273 // anything further means the three points were not a dip.
274 if (!(std::abs(offset) <= 1.0)) return static_cast<double>(tau);
275 return static_cast<double>(tau) + offset;
276 }
277
278 double median_of_buffer() const {
279 double sorted[kMaxMedianTaps];
280 const int n = median_count_;
281 for (int i = 0; i < n; ++i)
282 sorted[i] = median_buffer_[static_cast<std::size_t>(i)];
283 std::sort(sorted, sorted + n);
284 return sorted[n / 2];
285 }
286
287 double sample_rate_ = 48000.0;
288 double f0_min_ = kF0MinDefault;
289 double f0_max_ = kF0MaxDefault;
290
291 int tau_min_ = 0;
292 int tau_max_ = 0;
293 int window_ = 0;
294 int integration_ = 0;
295
296 std::vector<double> ring_{};
297 std::vector<double> scratch_{};
298 std::vector<double> diff_{};
299 std::vector<double> cmnd_{};
300
301 int write_ = 0;
302 int valid_samples_ = 0;
303 int hop_counter_ = 0;
304
305 double median_buffer_[kMaxMedianTaps] = {};
306 int median_index_ = 0;
307 int median_count_ = 0;
308
309 double f0_hz_ = 0.0;
310 double tau_ = 0.0;
311 double min_cmnd_ = 1.0;
312 bool voiced_ = false;
313};
314
317
318} // namespace pulp::signal
Monophonic fundamental-frequency estimator, after de Cheveigné & Kawahara 2002 [1],...
Definition yin_tracker.hpp:30
void prepare(double sample_rate)
Definition yin_tracker.hpp:57
int integration_samples() const
Definition yin_tracker.hpp:162
static constexpr int kMedianTaps
Definition yin_tracker.hpp:54
static constexpr double kYinThreshold
Definition yin_tracker.hpp:46
int tau_max() const
Definition yin_tracker.hpp:164
void reset()
Definition yin_tracker.hpp:84
double cost_mac_per_sample() const
Amortized analysis cost, so a caller can see before profiling that this is a one-per-track block rath...
Definition yin_tracker.hpp:169
double f0_max_hz() const
Definition yin_tracker.hpp:124
void discard_history() noexcept
Constant-time logical reset for audio-thread fault recovery.
Definition yin_tracker.hpp:98
static constexpr int hop_samples()
Definition yin_tracker.hpp:165
double f0_min_hz() const
Definition yin_tracker.hpp:123
static constexpr double kF0MinLimit
Definition yin_tracker.hpp:36
double tau_samples() const
The refined period in samples, before the Hz conversion.
Definition yin_tracker.hpp:148
static constexpr double kF0MinDefault
Definition yin_tracker.hpp:35
int window_samples() const
Definition yin_tracker.hpp:161
bool process(SampleType x)
Feeds one sample.
Definition yin_tracker.hpp:128
double f0_hz() const
The current estimate.
Definition yin_tracker.hpp:145
static constexpr int kHop
Definition yin_tracker.hpp:50
void set_f0_range(double min_hz, double max_hz)
Sets the tracked range.
Definition yin_tracker.hpp:114
static constexpr int kMaxMedianTaps
Definition yin_tracker.hpp:55
double min_cmnd() const
The frame's minimum CMND — the tracker's own confidence reading.
Definition yin_tracker.hpp:154
bool voiced() const
False when the frame carried no stable period (min d' > kYinThreshold).
Definition yin_tracker.hpp:151
int tau_min() const
Definition yin_tracker.hpp:163
int latency_samples() const
Series law 5: the estimate for a frame needs the whole window, so this is W.
Definition yin_tracker.hpp:159
static constexpr double kF0MaxLimit
Definition yin_tracker.hpp:37
static constexpr double kF0MaxDefault
Definition yin_tracker.hpp:41
@ b
Definition pitch.hpp:31
T arg(const std::complex< T > &z) noexcept
Counted std::arg (an atan2). Bit-identical result.
Definition rt_work_counter.hpp:161
Definition additive_bank.hpp:218
YinTrackerT< float > YinTracker
Definition yin_tracker.hpp:315
@ c
Definition spectrum_trace.hpp:35
YinTrackerT< double > YinTracker64
Definition yin_tracker.hpp:316
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ running
Maintain running sums as frames arrive; the latch only divides.
Definition freeze_hold.hpp:136
@ n
Definition input_events.hpp:40
STL namespace.