OXYGENHUB
◆ main openmsx / src / utils / yin.hh
9.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
// YIN pitch detection algorithm
2
// Based on: de Cheveigné & Kawahara (2002)
3
// "YIN, a fundamental frequency estimator for speech and music"
4
5
#include "autocorrelation.hh"
6
#include "narrow.hh"
7
#include "xrange.hh"
8
9
#include <algorithm>
10
#include <array>
11
#include <bit>
12
#include <cassert>
13
#include <cmath>
14
#include <span>
15
16
namespace yin {
17
18
template<typename T> [[nodiscard]] constexpr T square(T t)
19
{
20
	return t * t;
21
}
22
23
// Helper: Compute difference function d(τ) using autocorrelation
24
//
25
// d(τ) = Σ(x[n] - x[n+τ])²
26
//      = Σx[n]² + Σx[n+τ]² - 2·Σx[n]·x[n+τ]
27
//      = energyLeft(τ) + energyRight(τ) - 2·r(τ)
28
//
29
// Returns: Total energy of the signal (sum of all squared samples)
30
//
31
// Preconditions:
32
//   - autocorr.size() == diff.size()
33
//   - diff.size() <= signal.size()
34
//   - WORK_LEN >= signal.size() (for internal cumSumSq buffer)
35
template<size_t WORK_LEN>
36
[[nodiscard]] inline float computeDifferenceFunction(
37
	std::span<const float> signal,
38
	std::span<const float> autocorr,
39
	std::span<float> diff)
40
{
41
	assert(autocorr.size() == diff.size());
42
	assert(diff.size() <= signal.size());
43
	const auto N = signal.size();
44
	const auto maxLag = diff.size();
45
	assert(WORK_LEN >= N);
46
47
	// Internal work buffer for cumulative sum of squares
48
	std::array<float, WORK_LEN> cumSumSq; // ok, uninitialized
49
50
	// Precompute cumulative sum of squares
51
	// cumSumSq[i] = Σ signal[j]² for j=0..i (inclusive)
52
	cumSumSq[0] = square(signal[0]);
53
	for (auto i : xrange(size_t(1), N)) {
54
		cumSumSq[i] = cumSumSq[i - 1] + square(signal[i]);
55
	}
56
	const auto totalEnergy = cumSumSq[N - 1];
57
58
	// Compute difference function
59
	diff[0] = 0.0f; // Perfect match at lag 0
60
61
	for (auto lag : xrange(size_t(1), maxLag)) {
62
		// energyLeft = Σ signal[n]² for n in [0, N-lag)
63
		auto energyLeft = cumSumSq[N - lag - 1];
64
65
		// energyRight = Σ signal[n]² for n in [lag, N)
66
		auto energyRight = totalEnergy - cumSumSq[lag - 1];
67
68
		// d(τ) = energyLeft + energyRight - 2·r(τ)
69
		diff[lag] = energyLeft + energyRight - 2.0f * autocorr[lag];
70
	}
71
72
	return totalEnergy;
73
}
74
75
// Helper: Cumulative mean normalization
76
//
77
// d'(τ) = d(τ) / [(1/τ) · Σd(j) for j=1..τ]
78
//       = d(τ) · τ / Σd(j) for j=1..τ
79
//
80
// This is the key innovation of YIN that reduces octave errors.
81
inline void cumulativeMeanNormalize(std::span<const float> diff,
82
                                    std::span<float> diffNorm)
83
{
84
	assert(diff.size() == diffNorm.size());
85
86
	diffNorm[0] = 1.0f; // Special case
87
88
	auto runningSum = 0.0f;
89
	for (auto lag : xrange(size_t(1), diff.size())) {
90
		runningSum += diff[lag];
91
		if (runningSum > 0.0f) {
92
			diffNorm[lag] = diff[lag] * float(lag) / runningSum;
93
		} else {
94
			diffNorm[lag] = 1.0f;
95
		}
96
	}
97
}
98
99
// Helper: Find first minimum below threshold
100
//
101
// Returns the lag corresponding to the fundamental period.
102
// Uses YIN's absolute threshold approach.
103
// Always returns a result (the best candidate found, even if weak).
104
//
105
// Preconditions:
106
//   - minLag < maxLag
107
//   - diffNorm.size() >= maxLag (typically size == maxLag + 1)
108
[[nodiscard]] inline size_t findBestLag(
109
	std::span<const float> diffNorm, size_t minLag,
110
	size_t maxLag, float threshold)
111
{
112
	assert(minLag < maxLag);
113
	assert(diffNorm.size() >= maxLag);
114
115
	// Single-pass: look for first value below threshold (with early exit)
116
	// while tracking global minimum as fallback
117
	auto bestLag = minLag;
118
	auto minVal = diffNorm[minLag];
119
120
	for (auto lag : xrange(minLag, maxLag)) {
121
		auto val = diffNorm[lag];
122
123
		// Check for first value below threshold -> early exit
124
		if (val < threshold) {
125
			// Found candidate, search forward for local minimum
126
			while (lag + 1 < maxLag &&
127
			       diffNorm[lag + 1] < diffNorm[lag]) {
128
				++lag;
129
			}
130
			return lag; // Early exit!
131
		}
132
133
		// Track global minimum (used if nothing below threshold)
134
		if (val < minVal) {
135
			minVal = val;
136
			bestLag = lag;
137
		}
138
	}
139
140
	// No value below threshold, use tracked global minimum
141
	// (Return even if confidence is low - let caller decide if acceptable)
142
	return bestLag;
143
}
144
145
// Helper: Parabolic interpolation for sub-sample accuracy
146
//
147
// Uses 3 points around the minimum to fit a parabola and find the true minimum.
148
[[nodiscard]] inline float parabolicInterpolation(
149
	std::span<const float> diffNorm, size_t lag)
150
{
151
	// Need neighbors on both sides (check array bounds only)
152
	if (lag == 0 || lag >= diffNorm.size() - 1) {
153
		return float(lag);
154
	}
155
156
	auto y1 = diffNorm[lag - 1];
157
	auto y2 = diffNorm[lag + 0];
158
	auto y3 = diffNorm[lag + 1];
159
160
	// Check we have a proper minimum (not maximum or flat)
161
	auto denom = y1 - 2.0f * y2 + y3;
162
	if (std::abs(denom) <= 1e-10f || y1 <= y2 || y3 <= y2) {
163
		// Can't fit parabola or not a minimum
164
		return float(lag);
165
	}
166
167
	// Parabolic interpolation formula
168
	// The offset is mathematically guaranteed to be in (-1, 1) given the
169
	// guards above: For a concave-up parabola where the middle of three
170
	// equally-spaced points is strictly lower than both neighbors, the
171
	// vertex must lie between them.
172
	auto offset = 0.5f * (y1 - y3) / denom;
173
	return float(lag) + offset;
174
}
175
176
struct PitchResult {
177
	float frequency = 0.0f; // Detected frequency in Hz (0.0 = no pitch detected)
178
	float error = 1.0f; // YIN aperiodicity measure (0.0 = perfect periodic,
179
	                    // higher = worse) Typical: <0.1 excellent, 0.1-0.2
180
	                    // good, 0.2-0.5 weak, >0.5 poor Default 1.0 = no
181
	                    // valid pitch detected
182
};
183
184
// Template helper that does the actual YIN computation for a specific FFT size
185
template<unsigned FFT_LEN_L2>
186
[[nodiscard]] inline PitchResult detectPitchImpl(
187
	std::span<const float> signal, float sampleRate,
188
	size_t minLag, size_t maxLag, float threshold)
189
{
190
	static constexpr unsigned FFT_LEN = 1 << FFT_LEN_L2;
191
	static constexpr unsigned WORK_LEN = FFT_LEN / 2;
192
	// FFT requires full FFT_LEN, but YIN working buffers only need WORK_LEN
193
	// because:
194
	//   - FFT_LEN >= 2*N (chosen to be next power of 2 above 2*N)
195
	//   - So N <= FFT_LEN/2, and maxLag <= N/2 <= FFT_LEN/4
196
	//   - Therefore maxLag + 1 < WORK_LEN, and signal.size() <= WORK_LEN
197
198
	// Compute autocorrelation using the new autocorrelation<SIGNAL_LEN_L2> API
199
	// SIGNAL_LEN_L2 = FFT_LEN_L2 - 1 because FFT_LEN = 2 * SIGNAL_LEN
200
	std::array<float, WORK_LEN> autocorr_; // ok, uninitialized
201
	auto autocorrFull = std::span(autocorr_).first(signal.size());
202
	autocorrelation<FFT_LEN_L2 - 1>(signal, autocorrFull);
203
	// Extract only the lags we need (0..maxLag)
204
	auto autocorr = autocorrFull.first(maxLag + 1);
205
206
	// Compute difference function
207
	std::array<float, WORK_LEN> diff_; // ok, uninitialized
208
	auto diff = std::span(diff_).first(maxLag + 1);
209
	auto totalEnergy = computeDifferenceFunction<WORK_LEN>(signal, autocorr, diff);
210
211
	// Early exit if signal is essentially silence
212
	if (totalEnergy < 1e-10f) return {};
213
214
	// Cumulative mean normalization
215
	std::array<float, WORK_LEN> diffNorm_; // ok, uninitialized
216
	auto diffNorm = std::span(diffNorm_).first(maxLag + 1);
217
	cumulativeMeanNormalize(diff, diffNorm);
218
219
	// Find best lag (always returns a result, even if confidence is low)
220
	auto bestLag = findBestLag(diffNorm, minLag, maxLag, threshold);
221
222
	// Parabolic interpolation
223
	auto refinedLag = parabolicInterpolation(diffNorm, bestLag);
224
225
	// Convert to frequency and get error measure
226
	auto frequency = sampleRate / refinedLag;
227
	auto error = std::max(diffNorm[bestLag], 0.0f); // YIN normalized difference (lower is better)
228
	return {frequency, error};
229
}
230
231
// Main YIN pitch detection function (public API)
232
//
233
// Detects the fundamental frequency of an audio signal using the YIN algorithm.
234
//
235
// Parameters:
236
//   signal     - Audio samples
237
//   sampleRate - Sample rate in Hz (e.g., 44100.0f)
238
//   minFreq    - Minimum frequency to search for (default: 50 Hz)
239
//   maxFreq    - Maximum frequency to search for (default: 2000 Hz)
240
//   threshold  - YIN threshold parameter (default: 0.15, range: 0.1-0.2)
241
//
242
// Returns:
243
//   PitchResult with:
244
//     frequency  - Detected pitch in Hz
245
//     error      - YIN aperiodicity measure (lower is better)
246
//                  Typical values: <0.1 = excellent, 0.1-0.2 = good, 0.2-0.5 =
247
//                  weak, >0.5 = poor Caller should filter out results with high
248
//                  error (e.g., >0.5) Can exceed 1.0 for very non-periodic
249
//                  signals (drums, noise)
250
//
251
// Preconditions (checked by assertions in debug builds):
252
//   - signal.size() >= 2
253
//   - minFreq < maxFreq
254
//   - signal.size() >= 2 * sampleRate / minFreq (approximately)
255
//     i.e., signal must contain at least ~2 periods of the lowest frequency
256
//   - Recommended buffer size: ~2048 samples @ 44.1kHz for good frequency
257
//   resolution
258
//     (allows detecting down to ~43 Hz with 2048 samples @ 44.1kHz)
259
[[nodiscard]] inline PitchResult detectPitch(
260
	std::span<const float> signal,
261
	float sampleRate,
262
	float minFreq = 50.0f,
263
	float maxFreq = 2000.0f,
264
	float threshold = 0.15f)
265
{
266
	const auto N = signal.size();
267
268
	// Calculate lag range
269
	const auto minLag = std::max(size_t(1), size_t(sampleRate / maxFreq));
270
	const auto maxLag = std::min(size_t(sampleRate / minFreq), N / 2);
271
272
	// Preconditions: signal must be large enough and frequency range must
273
	// be valid
274
	assert(N >= 2);
275
	assert(maxLag > minLag);
276
277
	// Determine FFT size: next power of 2 that's at least 2*N
278
	const auto fftLenL2 = narrow<unsigned>(std::bit_width(2 * N - 1));
279
280
	// Dispatch to appropriate FFT size
281
	switch (fftLenL2) {
282
	case  8: return detectPitchImpl< 8>(signal, sampleRate, minLag, maxLag, threshold);
283
	case  9: return detectPitchImpl< 9>(signal, sampleRate, minLag, maxLag, threshold);
284
	case 10: return detectPitchImpl<10>(signal, sampleRate, minLag, maxLag, threshold);
285
	case 11: return detectPitchImpl<11>(signal, sampleRate, minLag, maxLag, threshold);
286
	case 12: return detectPitchImpl<12>(signal, sampleRate, minLag, maxLag, threshold);
287
	case 13: return detectPitchImpl<13>(signal, sampleRate, minLag, maxLag, threshold);
288
	case 14: return detectPitchImpl<14>(signal, sampleRate, minLag, maxLag, threshold);
289
	default:
290
		// Unsupported FFT size - signal too large or too small
291
		assert(false && "Unsupported FFT size in detectPitchYIN");
292
		return {};
293
	}
294
}
295
296
} // namespace yin