FastLED 3.9.15
Loading...
Searching...
No Matches
pitch.cpp.hpp
Go to the documentation of this file.
3#include "fl/math/math.h"
4#include "fl/stl/noexcept.h"
5
6namespace fl {
7namespace audio {
8namespace detector {
9
11 : mCurrentPitch(0.0f)
12 , mSmoothedPitch(0.0f)
13 , mConfidence(0.0f)
14 , mIsVoiced(false)
15 , mPreviousVoiced(false)
16 , mPreviousPitch(0.0f)
17 , mMinFrequency(80.0f) // Typical low male voice / bass guitar
18 , mMaxFrequency(1000.0f) // Upper range for most melodic instruments
19 , mConfidenceThreshold(0.5f) // Require 50% confidence minimum
20 , mPitchChangeSensitivity(5.0f) // 5 Hz threshold for pitch change events
21 , mMinPeriod(0)
22 , mMaxPeriod(0)
23 , mSampleRate(44100.0f) // Standard audio sample rate
24{
26 // Reserve space for autocorrelation buffer (worst case: max period)
27 mAutocorrelation.reserve(static_cast<size>(mMaxPeriod + 1));
28}
29
30Pitch::~Pitch() FL_NOEXCEPT = default;
31
32void Pitch::update(shared_ptr<Context> context) {
33 // Get PCM data from context
34 span<const i16> pcm = context->getPCM();
35 size numSamples = pcm.size();
36
37 // Compute dt from actual audio buffer duration
38 mLastDt = computeAudioDt(pcm.size(), static_cast<int>(mSampleRate));
39
40 // Need at least 2x max period for autocorrelation
41 if (numSamples < static_cast<size>(mMaxPeriod * 2)) {
42 // Not enough samples for reliable pitch detection
43 mConfidence = 0.0f;
44 mIsVoiced = false;
45
47 return;
48 }
49
50 // Calculate autocorrelation and find pitch
51 float detectedPitch = calculateAutocorrelation(pcm.data(), numSamples);
52
53 // Check if pitch is valid and confidence is sufficient
54 if (detectedPitch > 0.0f && mConfidence >= mConfidenceThreshold) {
55 mIsVoiced = true;
56 mCurrentPitch = detectedPitch;
57 updatePitchSmoothing(detectedPitch);
58 mFirePitch = true;
59
60 if (shouldReportPitchChange(detectedPitch)) {
61 mFirePitchChange = true;
62 mPreviousPitch = detectedPitch;
63 }
64 } else {
65 mIsVoiced = false;
66 mCurrentPitch = 0.0f;
67 mFirePitch = false;
68 }
69
71}
72
89
91 mCurrentPitch = 0.0f;
92 mSmoothedPitch = 0.0f;
93 mConfidence = 0.0f;
94 mIsVoiced = false;
95 mPreviousVoiced = false;
96 mPreviousPitch = 0.0f;
97 mAutocorrelation.clear();
98 mPitchSmoother.reset();
99}
100
102 // Convert frequency range to period range (in samples)
103 // Period (samples) = SampleRate / Frequency
106}
107
108float Pitch::calculateAutocorrelation(const i16* pcm, size numSamples) {
109 // Clear and resize autocorrelation buffer
110 mAutocorrelation.clear();
111 mAutocorrelation.resize(static_cast<size>(mMaxPeriod + 1), 0.0f);
112
113 // Normalize input to float range [-1, 1]
114 const float normFactor = 1.0f / 32768.0f;
115
116 // Calculate autocorrelation for all lags in the period range
117 // ACF[k] = sum(signal[n] * signal[n + k]) for all valid n
118 for (int lag = mMinPeriod; lag <= mMaxPeriod; lag++) {
119 float sum = 0.0f;
120 int validSamples = 0;
121
122 for (size i = 0; i + lag < numSamples; i++) {
123 float s1 = static_cast<float>(pcm[i]) * normFactor;
124 float s2 = static_cast<float>(pcm[i + lag]) * normFactor;
125 sum += s1 * s2;
126 validSamples++;
127 }
128
129 // Normalize by number of samples
130 if (validSamples > 0) {
131 mAutocorrelation[static_cast<size>(lag)] = sum / static_cast<float>(validSamples);
132 }
133 }
134
135 // Find the lag with maximum autocorrelation (best period match)
136 int bestLag = findBestPeakLag(mAutocorrelation);
137
138 // Calculate confidence based on autocorrelation peak
139 if (bestLag > 0) {
141
142 // Convert period (lag) to frequency
143 return periodToFrequency(bestLag);
144 }
145
146 // No reliable pitch found
147 mConfidence = 0.0f;
148 return 0.0f;
149}
150
151int Pitch::findBestPeakLag(const vector<float>& autocorr) const {
152 // Find the lag with maximum autocorrelation value
153 // (excluding lag 0, which is always maximum by definition)
154
155 float maxValue = -1.0f;
156 int bestLag = 0;
157
158 for (int lag = mMinPeriod; lag <= mMaxPeriod && lag < static_cast<int>(autocorr.size()); lag++) {
159 float value = autocorr[static_cast<size>(lag)];
160
161 // Look for positive peaks
162 if (value > maxValue && value > 0.0f) {
163 maxValue = value;
164 bestLag = lag;
165 }
166 }
167
168 return bestLag;
169}
170
171float Pitch::calculateConfidence(const vector<float>& autocorr, int peakLag) const {
172 // Confidence based on:
173 // 1. Strength of the autocorrelation peak
174 // 2. Ratio of peak to nearby values (peak clarity)
175
176 if (peakLag <= 0 || peakLag >= static_cast<int>(autocorr.size())) {
177 return 0.0f;
178 }
179
180 float peakValue = autocorr[static_cast<size>(peakLag)];
181
182 // Confidence is primarily based on peak strength
183 // Autocorrelation ranges from -1 to 1, but we only care about positive peaks
184 float confidence = fl::max(0.0f, fl::min(1.0f, peakValue));
185
186 // Calculate clarity by comparing peak to surrounding values
187 // Look at ±10% of the period
188 int windowSize = fl::max(2, peakLag / 10);
189 float neighborSum = 0.0f;
190 int neighborCount = 0;
191
192 for (int offset = -windowSize; offset <= windowSize; offset++) {
193 if (offset == 0) continue; // Skip the peak itself
194
195 int lag = peakLag + offset;
196 if (lag >= mMinPeriod && lag <= mMaxPeriod && lag < static_cast<int>(autocorr.size())) {
197 neighborSum += fl::max(0.0f, autocorr[static_cast<size>(lag)]);
198 neighborCount++;
199 }
200 }
201
202 if (neighborCount > 0) {
203 float neighborAvg = neighborSum / static_cast<float>(neighborCount);
204
205 // Peak should be significantly higher than neighbors
206 // If peak is 2x higher, that's good; if it's similar, reduce confidence
207 if (neighborAvg > 1e-6f) {
208 float clarity = fl::min(1.0f, (peakValue - neighborAvg) / neighborAvg);
209 confidence *= (0.7f + 0.3f * clarity); // Weight clarity at 30%
210 }
211 }
212
213 return confidence;
214}
215
216float Pitch::periodToFrequency(int period) const {
217 if (period <= 0) {
218 return 0.0f;
219 }
220 return mSampleRate / static_cast<float>(period);
221}
222
223int Pitch::frequencyToPeriod(float frequency) const {
224 if (frequency <= 0.0f) {
225 return 0;
226 }
227 return static_cast<int>(mSampleRate / frequency);
228}
229
230void Pitch::updatePitchSmoothing(float newPitch) {
231 // OneEuroFilter: adaptive — low jitter when pitch stable, low lag on changes
232 mSmoothedPitch = mPitchSmoother.update(newPitch, mLastDt);
233}
234
235bool Pitch::shouldReportPitchChange(float newPitch) const {
236 // Check if pitch has changed significantly from previous detection
237 if (mPreviousPitch == 0.0f) {
238 // First pitch detection
239 return true;
240 }
241
242 // Calculate absolute difference in Hz
243 float pitchDiff = fl::abs(newPitch - mPreviousPitch);
244
245 // Report change if difference exceeds sensitivity threshold
246 return pitchDiff >= mPitchChangeSensitivity;
247}
248
249} // namespace detector
250} // namespace audio
251} // namespace fl
function_list< void(float hz)> onPitch
Definition pitch.h:45
function_list< void(float hz)> onPitchChange
Definition pitch.h:47
~Pitch() FL_NOEXCEPT override
vector< float > mAutocorrelation
Definition pitch.h:91
float calculateConfidence(const vector< float > &autocorr, int peakLag) const
float periodToFrequency(int period) const
bool shouldReportPitchChange(float newPitch) const
float calculateAutocorrelation(const i16 *pcm, size numSamples)
void fireCallbacks() override
Definition pitch.cpp.hpp:73
void update(shared_ptr< Context > context) override
Definition pitch.cpp.hpp:32
void updatePitchSmoothing(float newPitch)
function_list< void(float hz, float confidence)> onPitchWithConfidence
Definition pitch.h:46
int findBestPeakLag(const vector< float > &autocorr) const
int frequencyToPeriod(float frequency) const
function_list< void(u8 voiced)> onVoiced
Definition pitch.h:48
OneEuroFilter< float > mPitchSmoother
Definition pitch.h:82
const T * data() const FL_NOEXCEPT
Definition span.h:461
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
fl::size size() const FL_NOEXCEPT
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NOEXCEPT
Compute the time delta (in seconds) for an audio buffer.
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NOEXCEPT
Definition math.h:71
unsigned char u8
Definition stdint.h:131
constexpr int type_rank< T >::value
constexpr common_type_t< T, U > max(T a, U b) FL_NOEXCEPT
Definition math.h:75
constexpr enable_if< is_fixed_point< T >::value, T >::type abs(T x) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT