DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
PitchFollower.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
50#include "../Core/AudioBuffer.h"
51#include "../Core/AudioSpec.h"
52#include "../Core/DspMath.h"
53#include "PitchDetector.h"
54
55#include <algorithm>
56#include <atomic>
57#include <cmath>
58#include <cstddef>
59#include <cstdint>
60#include <limits>
61#include <span>
62#include <vector>
63
64namespace dspark {
65
72template <FloatType T>
74{
75public:
76 // -- Lifecycle -------------------------------------------------------------
77
88 void prepare(const AudioSpec& spec, int windowSize = 2048)
89 {
90 if (!spec.isValid()) return;
91 sampleRate_ = spec.sampleRate;
92 detector_.prepare(spec.sampleRate, windowSize, windowSize / 4);
93 monoScratch_.assign(static_cast<size_t>(std::max(spec.maxBlockSize, 1)), T(0));
94 prepared_.store(true, std::memory_order_relaxed);
95 reset();
96 }
97
103 void reset() noexcept
104 {
105 hasTarget_ = false;
106 targetSt_ = 0.0;
107 currentSt_ = 0.0;
108 pendingSt_ = 0.0;
109 pendingCount_ = 0;
110 samplesSinceValid_ = std::numeric_limits<int64_t>::max() / 2;
111 smoothedHz_.store(T(0), std::memory_order_relaxed);
112 tracking_.store(false, std::memory_order_relaxed);
113 }
114
115 // -- Parameters (thread-safe) ------------------------------------------------
116
122 void setRange(T minHz, T maxHz) noexcept
123 {
124 if (!std::isfinite(minHz) || !std::isfinite(maxHz)) return;
125 minHz = std::max(minHz, T(10));
126 maxHz = std::max(maxHz, minHz);
127 minHz_.store(minHz, std::memory_order_relaxed);
128 maxHz_.store(maxHz, std::memory_order_relaxed);
129 }
130
132 void setConfidence(T threshold) noexcept
133 {
134 if (!std::isfinite(threshold)) return;
135 confidence_.store(std::clamp(threshold, T(0), T(1)), std::memory_order_relaxed);
136 }
137
143 void setGlide(T msPerOctave) noexcept
144 {
145 if (!std::isfinite(msPerOctave)) return;
146 glideMs_.store(std::max(msPerOctave, T(0)), std::memory_order_relaxed);
147 }
148
150 [[nodiscard]] T getMinHz() const noexcept { return minHz_.load(std::memory_order_relaxed); }
151
153 [[nodiscard]] T getMaxHz() const noexcept { return maxHz_.load(std::memory_order_relaxed); }
154
156 [[nodiscard]] T getConfidenceThreshold() const noexcept { return confidence_.load(std::memory_order_relaxed); }
157
159 [[nodiscard]] T getGlide() const noexcept { return glideMs_.load(std::memory_order_relaxed); }
160
161 // -- Readout (lock-free, any thread) ------------------------------------------
162
164 [[nodiscard]] T getSmoothedHz() const noexcept
165 {
166 return smoothedHz_.load(std::memory_order_relaxed);
167 }
168
170 [[nodiscard]] T getRawHz() const noexcept { return detector_.getFrequencyHz(); }
171
173 [[nodiscard]] T getConfidence() const noexcept { return detector_.getConfidence(); }
174
176 [[nodiscard]] bool isTracking() const noexcept
177 {
178 return tracking_.load(std::memory_order_relaxed);
179 }
180
181 // -- Processing ----------------------------------------------------------------
182
188 {
189 if (!prepared_.load(std::memory_order_relaxed)) return;
190 const int nCh = buffer.getNumChannels();
191 const int nS = buffer.getNumSamples();
192 if (nCh <= 0 || nS <= 0) return;
193
194 int i = 0;
195 while (i < nS)
196 {
197 const int chunk = std::min(nS - i, static_cast<int>(monoScratch_.size()));
198 if (nCh == 1)
199 {
200 const T* src = buffer.getChannel(0) + i;
201 std::copy(src, src + chunk, monoScratch_.begin());
202 }
203 else
204 {
205 const T invCh = T(1) / static_cast<T>(nCh);
206 std::fill(monoScratch_.begin(), monoScratch_.begin() + chunk, T(0));
207 for (int ch = 0; ch < nCh; ++ch)
208 {
209 const T* src = buffer.getChannel(ch) + i;
210 for (int k = 0; k < chunk; ++k)
211 monoScratch_[static_cast<size_t>(k)] += src[k] * invCh;
212 }
213 }
214 pushSamples({ monoScratch_.data(), static_cast<size_t>(chunk) });
215 i += chunk;
216 }
217 }
218
223 void pushSamples(std::span<const T> samples) noexcept
224 {
225 if (!prepared_.load(std::memory_order_relaxed) || samples.empty()) return;
226 detector_.pushSamples(samples);
227 update(static_cast<int>(samples.size()));
228 }
229
230private:
232 void update(int numSamples) noexcept
233 {
234 const T raw = detector_.getFrequencyHz();
235 const T conf = detector_.getConfidence();
236 const bool valid = conf >= confidence_.load(std::memory_order_relaxed)
237 && raw >= minHz_.load(std::memory_order_relaxed)
238 && raw <= maxHz_.load(std::memory_order_relaxed);
239
240 if (valid)
241 {
242 double st = 12.0 * std::log2(static_cast<double>(raw) / 440.0);
243
244 if (!hasTarget_)
245 {
246 targetSt_ = currentSt_ = st; // first lock: no sweep-in
247 hasTarget_ = true;
248 }
249 else
250 {
251 double dev = st - targetSt_;
252 if (std::abs(dev) > 7.0)
253 {
254 // Octave folding: a 2x/0.5x detector error lands within
255 // ~1.5 st of the target after removing whole octaves.
256 for (const double oct : { -24.0, -12.0, 12.0, 24.0 })
257 {
258 if (std::abs(st + oct - targetSt_) < 1.5)
259 {
260 st += oct;
261 dev = st - targetSt_;
262 break;
263 }
264 }
265 }
266
267 if (std::abs(dev) > 7.0)
268 {
269 // A real large interval: require three consistent readings.
270 if (pendingCount_ > 0 && std::abs(st - pendingSt_) < 1.0)
271 {
272 if (++pendingCount_ >= 3)
273 {
274 targetSt_ = st;
275 pendingCount_ = 0;
276 }
277 }
278 else
279 {
280 pendingSt_ = st;
281 pendingCount_ = 1;
282 }
283 }
284 else
285 {
286 targetSt_ = st;
287 pendingCount_ = 0;
288 }
289 }
290 samplesSinceValid_ = 0;
291 }
292 else
293 {
294 samplesSinceValid_ += numSamples; // freeze: target stays put
295 }
296
297 if (hasTarget_)
298 {
299 const double glideMs = static_cast<double>(glideMs_.load(std::memory_order_relaxed));
300 if (glideMs < 0.5)
301 {
302 currentSt_ = targetSt_;
303 }
304 else
305 {
306 // Constant musical speed: 12 semitones in glideMs milliseconds.
307 const double maxStep = 12.0 * static_cast<double>(numSamples)
308 / (glideMs * 0.001 * sampleRate_);
309 currentSt_ += std::clamp(targetSt_ - currentSt_, -maxStep, maxStep);
310 }
311 smoothedHz_.store(static_cast<T>(440.0 * std::exp2(currentSt_ / 12.0)),
312 std::memory_order_relaxed);
313 }
314
315 tracking_.store(hasTarget_
316 && samplesSinceValid_ < static_cast<int64_t>(0.25 * sampleRate_),
317 std::memory_order_relaxed);
318 }
319
320 // -- Members --------------------------------------------------------------------
321 double sampleRate_ = 48000.0;
322 std::atomic<bool> prepared_{ false };
323
324 PitchDetector<T> detector_;
325 std::vector<T> monoScratch_;
326
327 bool hasTarget_ = false;
328 double targetSt_ = 0.0;
329 double currentSt_ = 0.0;
330 double pendingSt_ = 0.0;
331 int pendingCount_ = 0;
332 int64_t samplesSinceValid_ = 0;
333
334 std::atomic<T> minHz_ { T(60) };
335 std::atomic<T> maxHz_ { T(1200) };
336 std::atomic<T> confidence_ { T(0.85) };
337 std::atomic<T> glideMs_ { T(60) };
338
339 std::atomic<T> smoothedHz_ { T(0) };
340 std::atomic<bool> tracking_ { false };
341};
342
343} // namespace dspark
Real-time monophonic pitch detection using the YIN algorithm.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Gated, octave-safe, semitone-smoothed pitch tracking source.
void reset() noexcept
Forgets the tracked pitch (parameters are kept). Allocation-free; call from the stream owner (it touc...
T getGlide() const noexcept
Returns the glide time in milliseconds per octave.
T getRawHz() const noexcept
bool isTracking() const noexcept
T getConfidence() const noexcept
void setConfidence(T threshold) noexcept
Confidence threshold [0, 1] below which readings are ignored (default 0.85).
T getMaxHz() const noexcept
Returns the upper bound of the accepted pitch range in Hz.
void processBlock(AudioBufferView< const T > buffer) noexcept
Feeds a block (any channel count); channels are averaged to mono.
void pushSamples(std::span< const T > samples) noexcept
Feeds mono samples directly (alternative to processBlock).
void setGlide(T msPerOctave) noexcept
Glide time in milliseconds per octave (default 60). 0 disables smoothing (the output snaps to each ac...
T getMinHz() const noexcept
Returns the lower bound of the accepted pitch range in Hz.
T getConfidenceThreshold() const noexcept
Returns the confidence gating threshold [0, 1].
void setRange(T minHz, T maxHz) noexcept
Accepted pitch range in Hz (default 60 to 1200). Non-finite values are ignored (the old max() passed ...
void prepare(const AudioSpec &spec, int windowSize=2048)
Prepares the follower and the wrapped detector.
T getSmoothedHz() const noexcept
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43