FastLED 3.10.8
Loading...
Searching...
No Matches
key.cpp.hpp
Go to the documentation of this file.
1// KeyDetector implementation
2
5#include "fl/audio/fft/fft.h"
6#include "fl/math/math.h"
7#include "fl/log/log.h"
8#include "fl/stl/stdio.h"
9#include "fl/stl/noexcept.h"
10
11namespace fl {
12namespace audio {
13namespace detector {
14
15// Krumhansl-Schmuckler key profiles
16// Based on empirical studies of Western tonal music perception
17// Values represent perceptual importance of each scale degree
18
19// Major key profile (Krumhansl & Kessler, 1982)
20const float KeyDetector::MAJOR_PROFILE[12] = {
21 6.35f, // Tonic (C)
22 2.23f, // Minor 2nd (C#)
23 3.48f, // Major 2nd (D)
24 2.33f, // Minor 3rd (Eb)
25 4.38f, // Major 3rd (E)
26 4.09f, // Perfect 4th (F)
27 2.52f, // Tritone (F#)
28 5.19f, // Perfect 5th (G)
29 2.39f, // Minor 6th (Ab)
30 3.66f, // Major 6th (A)
31 2.29f, // Minor 7th (Bb)
32 2.88f // Major 7th (B)
33};
34
35// Minor key profile (Krumhansl & Kessler, 1982)
36const float KeyDetector::MINOR_PROFILE[12] = {
37 6.33f, // Tonic (A)
38 2.68f, // Minor 2nd (A#)
39 3.52f, // Major 2nd (B)
40 5.38f, // Minor 3rd (C)
41 2.60f, // Major 3rd (C#)
42 3.53f, // Perfect 4th (D)
43 2.54f, // Tritone (D#)
44 4.75f, // Perfect 5th (E)
45 3.98f, // Minor 6th (F)
46 2.69f, // Major 6th (F#)
47 3.34f, // Minor 7th (G)
48 3.17f // Major 7th (G#)
49};
50
51// Note names for root note display
52static const char* NOTE_NAMES[12] = {
53 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
54};
55
56//------------------------------------------------------------------------------
57// Key struct methods
58//------------------------------------------------------------------------------
59
60const char* Key::getRootName() const FL_NO_EXCEPT {
61 if (rootNote >= 12) return "?";
62 return NOTE_NAMES[rootNote];
63}
64
65void Key::getKeyName(char* buffer, size_t bufferSize) const FL_NO_EXCEPT {
66 if (bufferSize < 8) return; // Need space for "C# min\0"
67 const char* root = getRootName();
68 const char* quality = getQuality();
69 fl::snprintf(buffer, bufferSize, "%s %s", root, quality);
70}
71
72//------------------------------------------------------------------------------
73// KeyDetector implementation
74//------------------------------------------------------------------------------
75
78 , mPreviousKey()
79 , mKeyStartTime(0)
80 , mKeyActive(false)
82 , mMinKeyDuration(2000)
84 , mHistoryIndex(0)
85 , mHistorySize(0)
86{
87 // Initialize chroma history arrays
88 for (int i = 0; i < 12; i++) {
90 }
91
92 // Pre-compute profile statistics once (eliminates 24 recomputations per frame)
94}
95
97
99 // Pre-compute statistics for MAJOR_PROFILE
100 float majorSum = 0.0f, majorSqSum = 0.0f;
101 for (int i = 0; i < 12; i++) {
102 majorSum += MAJOR_PROFILE[i];
103 majorSqSum += MAJOR_PROFILE[i] * MAJOR_PROFILE[i];
104 }
105 mMajorProfileMean = majorSum / 12.0f;
107
108 // Pre-compute statistics for MINOR_PROFILE
109 float minorSum = 0.0f, minorSqSum = 0.0f;
110 for (int i = 0; i < 12; i++) {
111 minorSum += MINOR_PROFILE[i];
112 minorSqSum += MINOR_PROFILE[i] * MINOR_PROFILE[i];
113 }
114 mMinorProfileMean = minorSum / 12.0f;
116}
117
119 // Get fft::FFT data
120 mRetainedFFT = context->getFFT(32); // Use more bins for better pitch resolution
121 const fft::Bins& fft = *mRetainedFFT;
122 u32 timestamp = context->getTimestamp();
123
124 // Extract chroma features
125 float chroma[12] = {0};
126 extractChroma(fft, chroma);
127 normalizeChroma(chroma);
128
129 // Update temporal averaging
130 updateChromaHistory(chroma);
131
132 // Get averaged chroma for stable detection
133 float avgChroma[12] = {0};
134 getAveragedChroma(avgChroma);
135
136 // Detect key from averaged chroma
137 Key detectedKey = detectKey(avgChroma, timestamp);
138
139 // Update key duration if same key
140 if (mKeyActive && detectedKey == mCurrentKey) {
141 mCurrentKey.duration = timestamp - mKeyStartTime;
142 }
143
144 // Check for key change
145 if (detectedKey != mCurrentKey) {
146 // Only accept key change if:
147 // 1. Confidence is above threshold
148 // 2. Previous key was held for minimum duration OR new key is much stronger
149 bool acceptChange = false;
150
151 if (detectedKey.confidence >= mConfidenceThreshold) {
152 if (!mKeyActive) {
153 // No previous key, accept new key
154 acceptChange = true;
155 } else if (mCurrentKey.duration >= mMinKeyDuration) {
156 // Previous key held long enough, allow change
157 acceptChange = true;
158 } else if (detectedKey.confidence > mCurrentKey.confidence * 1.2f) {
159 // New key is significantly stronger, allow early change
160 acceptChange = true;
161 }
162 }
163
164 if (acceptChange) {
166 mCurrentKey = detectedKey;
167 mKeyStartTime = timestamp;
168 mKeyActive = true;
169
170 FL_DBG("Key change: " << mCurrentKey.getRootName() << " " << mCurrentKey.getQuality() << " (confidence: " << mCurrentKey.confidence << ")");
171
172 mFireKeyChange = true;
173 }
174 }
175
176 // Check for key end (confidence drop)
177 if (mKeyActive && detectedKey.confidence < mConfidenceThreshold * 0.5f) {
178 FL_DBG("Key ended: " << mCurrentKey.getRootName() << " " << mCurrentKey.getQuality());
179
180 mFireKeyEnd = true;
181 mKeyActive = false;
182 mCurrentKey.confidence = 0.0f;
183 }
184
185 // Set onKey flag every frame if key is active
186 if (mKeyActive) mFireKey = true;
187}
188
190 mCurrentKey = Key();
191 mPreviousKey = Key();
192 mKeyStartTime = 0;
193 mKeyActive = false;
194 mHistoryIndex = 0;
195 mHistorySize = 0;
196
197 // Clear chroma history
198 for (int i = 0; i < 12; i++) {
199 mChromaHistory[i].clear();
200 }
201}
202
203//------------------------------------------------------------------------------
204// Chroma extraction and processing
205//------------------------------------------------------------------------------
206
208 // Initialize chroma to zero
209 for (int i = 0; i < 12; i++) {
210 chroma[i] = 0.0f;
211 }
212
213 // Map fft::FFT bins to pitch classes using linearly-rebinned magnitudes.
214 // Linear bins give evenly-spaced frequency mapping: freq = fmin + bin * binWidth.
215 fl::span<const float> linearBins = fft.linear();
216 const int numBins = static_cast<int>(linearBins.size());
217 const float fmin = fft.linearFmin();
218 const float fmax = fft.linearFmax();
219 const float linearBinWidth = (numBins > 0) ? (fmax - fmin) / static_cast<float>(numBins) : 1.0f;
220
221 // Process each linear bin
222 for (int bin = 0; bin < numBins; bin++) {
223 float magnitude = linearBins[bin];
224 if (magnitude < 1e-6f) continue;
225
226 // Calculate frequency for this linearly-spaced bin
227 float freq = fmin + (static_cast<float>(bin) + 0.5f) * linearBinWidth;
228
229 // Skip frequencies below 60 Hz (below C2)
230 if (freq < 60.0f) continue;
231
232 // Convert frequency to MIDI note number
233 // MIDI note = 69 + 12 * log2(freq / 440)
234 float midiNote = 69.0f + 12.0f * (fl::logf(freq / 440.0f) / fl::logf(2.0f));
235
236 // Get pitch class (0-11)
237 int pitchClass = static_cast<int>(midiNote + 0.5f) % 12;
238 if (pitchClass < 0) pitchClass += 12;
239
240 // Accumulate magnitude into pitch class
241 chroma[pitchClass] += magnitude;
242 }
243}
244
246 // Find maximum value
247 float maxVal = 0.0f;
248 for (int i = 0; i < 12; i++) {
249 if (chroma[i] > maxVal) {
250 maxVal = chroma[i];
251 }
252 }
253
254 // Normalize to 0-1 range
255 if (maxVal > 1e-6f) {
256 for (int i = 0; i < 12; i++) {
257 chroma[i] /= maxVal;
258 }
259 }
260}
261
263 // Add current chroma to history (circular buffer)
264 for (int i = 0; i < 12; i++) {
265 if (static_cast<int>(mChromaHistory[i].size()) < mAveragingFrames) {
266 mChromaHistory[i].push_back(chroma[i]);
267 } else {
268 mChromaHistory[i][mHistoryIndex] = chroma[i];
269 }
270 }
271
274 mHistorySize++;
275 }
276}
277
279 if (mHistorySize == 0) {
280 // No history yet, return zeros
281 for (int i = 0; i < 12; i++) {
282 chroma[i] = 0.0f;
283 }
284 return;
285 }
286
287 // Average across history
288 for (int i = 0; i < 12; i++) {
289 float sum = 0.0f;
290 for (int j = 0; j < mHistorySize; j++) {
291 sum += mChromaHistory[i][j];
292 }
293 chroma[i] = sum / mHistorySize;
294 }
295}
296
297//------------------------------------------------------------------------------
298// Key detection using Krumhansl-Schmuckler algorithm
299//------------------------------------------------------------------------------
300
301Key KeyDetector::detectKey(const float* chroma, u32 timestamp) FL_NO_EXCEPT {
302 float bestCorrelation = -1.0f;
303 u8 bestRoot = 0;
304 bool bestIsMinor = false;
305
306 // Try all 24 possible keys (12 major + 12 minor)
307 for (int root = 0; root < 12; root++) {
308 // Try major key
309 float majorCorr = correlateWithProfile(chroma, MAJOR_PROFILE, root);
310 if (majorCorr > bestCorrelation) {
311 bestCorrelation = majorCorr;
312 bestRoot = root;
313 bestIsMinor = false;
314 }
315
316 // Try minor key
317 float minorCorr = correlateWithProfile(chroma, MINOR_PROFILE, root);
318 if (minorCorr > bestCorrelation) {
319 bestCorrelation = minorCorr;
320 bestRoot = root;
321 bestIsMinor = true;
322 }
323 }
324
325 // Convert correlation to 0-1 confidence
326 // Correlation ranges from -1 to 1, but we typically see 0.5-0.9 for good matches
327 float confidence = (bestCorrelation + 1.0f) / 2.0f; // Map [-1,1] to [0,1]
328 confidence = fl::max(0.0f, fl::min(1.0f, confidence));
329
330 return Key(bestRoot, bestIsMinor, confidence, timestamp);
331}
332
333float KeyDetector::correlateWithProfile(const float* chroma, const float* profile, int rootNote) FL_NO_EXCEPT {
334 // Pearson correlation coefficient between chroma and rotated profile
335 // Uses pre-computed profile statistics (calculated once in constructor)
336
337 // Use pre-computed profile statistics based on which profile
338 float profileMean, profileStdDev;
339 if (profile == MAJOR_PROFILE) {
340 profileMean = mMajorProfileMean;
341 profileStdDev = mMajorProfileStdDev;
342 } else {
343 profileMean = mMinorProfileMean;
344 profileStdDev = mMinorProfileStdDev;
345 }
346 if (profileStdDev < 1e-6f) profileStdDev = 1.0f;
347
348 // Calculate chroma mean and std dev
349 float chromaSum = 0.0f;
350 float chromaSqSum = 0.0f;
351 for (int i = 0; i < 12; i++) {
352 chromaSum += chroma[i];
353 chromaSqSum += chroma[i] * chroma[i];
354 }
355 float chromaMean = chromaSum / 12.0f;
356 float chromaStdDev = sqrtf((chromaSqSum / 12.0f) - (chromaMean * chromaMean));
357 if (chromaStdDev < 1e-6f) chromaStdDev = 1.0f;
358
359 // Calculate correlation with rotated profile
360 float correlation = 0.0f;
361 for (int i = 0; i < 12; i++) {
362 int profileIdx = (i - rootNote + 12) % 12; // Rotate profile to match root
363 float chromaNorm = (chroma[i] - chromaMean) / chromaStdDev;
364 float profileNorm = (profile[profileIdx] - profileMean) / profileStdDev;
365 correlation += chromaNorm * profileNorm;
366 }
367 correlation /= 12.0f; // Average
368
369 return correlation;
370}
371
373 if (mFireKeyEnd) {
374 if (onKeyEnd) onKeyEnd();
375 mFireKeyEnd = false;
376 }
377 if (mFireKeyChange) {
379 mFireKeyChange = false;
380 }
381 if (mFireKey) {
382 if (onKey) onKey(mCurrentKey);
383 mFireKey = false;
384 }
385}
386
387} // namespace detector
388} // namespace audio
389} // namespace fl
shared_ptr< const fft::Bins > mRetainedFFT
Definition key.h:134
void reset() FL_NO_EXCEPT override
Definition key.cpp.hpp:189
void updateChromaHistory(const float *chroma) FL_NO_EXCEPT
Definition key.cpp.hpp:262
~KeyDetector() FL_NO_EXCEPT override
static const float MAJOR_PROFILE[12]
Definition key.h:125
vector< float > mChromaHistory[12]
Definition key.h:120
void normalizeChroma(float *chroma) FL_NO_EXCEPT
Definition key.cpp.hpp:245
void fireCallbacks() FL_NO_EXCEPT override
Definition key.cpp.hpp:372
void extractChroma(const fft::Bins &fft, float *chroma) FL_NO_EXCEPT
Definition key.cpp.hpp:207
void getAveragedChroma(float *chroma) FL_NO_EXCEPT
Definition key.cpp.hpp:278
function_list< void()> onKeyEnd
Definition key.h:96
Key detectKey(const float *chroma, u32 timestamp) FL_NO_EXCEPT
Definition key.cpp.hpp:301
function_list< void(const Key &key)> onKeyChange
Definition key.h:95
static const float MINOR_PROFILE[12]
Definition key.h:126
void initializeProfileStats() FL_NO_EXCEPT
Definition key.cpp.hpp:98
float correlateWithProfile(const float *chroma, const float *profile, int rootNote) FL_NO_EXCEPT
Definition key.cpp.hpp:333
function_list< void(const Key &key)> onKey
Definition key.h:94
void update(shared_ptr< Context > context) FL_NO_EXCEPT override
Definition key.cpp.hpp:118
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
#define FL_DBG
Definition log.h:553
Centralized logging categories for FastLED hardware interfaces and subsystems.
static const char * NOTE_NAMES[12]
Definition key.cpp.hpp:52
float logf(float value) FL_NO_EXCEPT
Definition math.h:426
unsigned char u8
Definition stdint.h:131
constexpr common_type_t< T, U > max(T a, U b) FL_NO_EXCEPT
Definition math.h:75
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NO_EXCEPT
Definition math.h:71
float sqrtf(float value) FL_NO_EXCEPT
Definition math.h:461
Base definition for an LED controller.
Definition crgb.hpp:179
const char * getRootName() const FL_NO_EXCEPT
Definition key.cpp.hpp:60
void getKeyName(char *buffer, size_t bufferSize) const FL_NO_EXCEPT
Definition key.cpp.hpp:65
const char * getQuality() const FL_NO_EXCEPT
Definition key.h:62