FastLED 3.10.8
Loading...
Searching...
No Matches
chord.cpp.hpp
Go to the documentation of this file.
3#include "fl/audio/fft/fft.h"
4#include "fl/math/math.h"
5#include "fl/log/log.h"
6#include "fl/stl/noexcept.h"
7
8namespace fl {
9namespace audio {
10namespace detector {
11
12// Chord templates (intervals relative to root)
13// Major: root, major third (4 semitones), perfect fifth (7 semitones)
14// Minor: root, minor third (3 semitones), perfect fifth (7 semitones)
15// etc.
16
19 int intervals[5]; // Relative semitone intervals, -1 = unused
21};
22
24 {ChordType::MAJOR, {0, 4, 7, -1, -1}, 3},
25 {ChordType::MINOR, {0, 3, 7, -1, -1}, 3},
26 {ChordType::DIMINISHED, {0, 3, 6, -1, -1}, 3},
27 {ChordType::AUGMENTED, {0, 4, 8, -1, -1}, 3},
28 {ChordType::MAJOR7, {0, 4, 7, 11, -1}, 4},
29 {ChordType::MINOR7, {0, 3, 7, 10, -1}, 4},
30 {ChordType::DOMINANT7, {0, 4, 7, 10, -1}, 4},
31 {ChordType::SUSPENDED2, {0, 2, 7, -1, -1}, 3},
32 {ChordType::SUSPENDED4, {0, 5, 7, -1, -1}, 3},
33};
34static const int kNumChordTemplates = sizeof(kChordTemplates) / sizeof(ChordTemplate);
35
38 , mChordEndTime(0)
40 , mMinChordDuration(200) // 200ms minimum chord duration
41{
42 for (int i = 0; i < 12; i++) {
43 mChroma[i] = 0.0f;
44 mPrevChroma[i] = 0.0f;
45 }
46
47 // Pre-compute template lookups (O(1) access per frame)
49}
50
52
54 // Pre-compute lookup map from ChordType to ChordTemplate*
55 // Avoids linear search through kChordTemplates (9 searches per frame)
56 for (int i = 0; i < kNumChordTemplates; i++) {
57 mTemplateMap[static_cast<int>(kChordTemplates[i].type)] = &kChordTemplates[i];
58 }
59}
60
62 mRetainedFFT = context->getFFT(32); // Higher resolution for pitch detection
63 const fft::Bins& fft = *mRetainedFFT;
64 u32 timestamp = context->getTimestamp();
65
66 // Calculate chroma features
68
69 // Detect current chord
70 Chord detected = detectChord(mChroma, timestamp);
71
72 // Check if chord changed
73 if (detected.isValid() && detected.confidence >= mConfidenceThreshold) {
74 if (!mCurrentChord.isValid() || !isSimilarChord(detected, mCurrentChord)) {
75 // New chord detected
76 if (mCurrentChord.isValid()) {
77 mFireChordEnd = true;
78 }
79
81 mCurrentChord = detected;
82 mChordStartTime = timestamp;
83
84 mFireChordChange = true;
85
86 FL_DBG("Chord detected: " << mCurrentChord.getRootName() << mCurrentChord.getTypeName() << " (conf: " << mCurrentChord.confidence << ")");
87 } else {
88 // Same chord, update confidence
89 mCurrentChord.confidence = detected.confidence;
90 mCurrentChord.timestamp = timestamp;
91 }
92
93 // Set flag for onChord callback every frame when chord is active
94 mFireChord = true;
95 } else {
96 // No valid chord or low confidence
97 if (mCurrentChord.isValid()) {
98 u32 duration = timestamp - mChordStartTime;
99 if (duration >= mMinChordDuration) {
100 mChordEndTime = timestamp;
101 mFireChordEnd = true;
102 }
103 mCurrentChord = Chord(); // Reset to invalid
104 }
105 }
106
107 // Save chroma for next frame
108 for (int i = 0; i < 12; i++) {
109 mPrevChroma[i] = mChroma[i];
110 }
111}
112
114 if (mFireChordEnd) {
115 if (onChordEnd) onChordEnd();
116 mFireChordEnd = false;
117 }
118 if (mFireChordChange) {
120 mFireChordChange = false;
121 }
122 if (mFireChord) {
124 mFireChord = false;
125 }
126}
127
131 mChordStartTime = 0;
132 mChordEndTime = 0;
133 for (int i = 0; i < 12; i++) {
134 mChroma[i] = 0.0f;
135 mPrevChroma[i] = 0.0f;
136 }
137}
138
140 // Clear chroma
141 for (int i = 0; i < 12; i++) {
142 mChroma[i] = 0.0f;
143 }
144
145 // Map fft::FFT bins to pitch classes (chroma) using linearly-rebinned magnitudes.
146 // Linear bins give evenly-spaced frequency mapping: freq = fmin + bin * binWidth.
147 fl::span<const float> linearBins = fft.linear();
148 const fl::size numBins = linearBins.size();
149 const float fmin = fft.linearFmin();
150 const float fmax = fft.linearFmax();
151 const float linearBinWidth = (numBins > 0) ? (fmax - fmin) / static_cast<float>(numBins) : 1.0f;
152
153 for (fl::size binIdx = 0; binIdx < numBins; binIdx++) {
154 float magnitude = linearBins[binIdx];
155 if (magnitude < 1e-6f) continue;
156
157 // Calculate frequency for this linearly-spaced bin
158 float freq = fmin + (static_cast<float>(binIdx) + 0.5f) * linearBinWidth;
159
160 // Skip frequencies below 60Hz (too low for chord detection)
161 if (freq < 60.0f) continue;
162
163 // Convert frequency to MIDI note number (A4 = 440Hz = MIDI 69)
164 float midiNote = 69.0f + 12.0f * (fl::logf(freq / 440.0f) / fl::logf(2.0f));
165
166 // Get pitch class (0-11)
167 int pitchClass = static_cast<int>(midiNote + 0.5f) % 12;
168 if (pitchClass < 0) pitchClass += 12;
169
170 // Accumulate magnitude into chroma
171 mChroma[pitchClass] += magnitude;
172 }
173
174 // Normalize chroma
176}
177
178Chord ChordDetector::detectChord(const float* chroma, u32 timestamp) FL_NO_EXCEPT {
179 float bestScore = 0.0f;
180 int bestRoot = -1;
181 ChordType bestType = ChordType::UNKNOWN;
182
183 // Try all root notes (0-11)
184 for (int root = 0; root < 12; root++) {
185 // Try all chord templates
186 for (int t = 0; t < kNumChordTemplates; t++) {
187 float score = matchChordPattern(chroma, root, kChordTemplates[t].type);
188 if (score > bestScore) {
189 bestScore = score;
190 bestRoot = root;
191 bestType = kChordTemplates[t].type;
192 }
193 }
194 }
195
196 // Confidence threshold
197 if (bestScore < 0.3f) {
198 return Chord(); // No valid chord
199 }
200
201 return Chord(bestRoot, bestType, bestScore, timestamp);
202}
203
204float ChordDetector::matchChordPattern(const float* chroma, int root, ChordType type) FL_NO_EXCEPT {
205 // Find the matching template using pre-computed lookup (O(1) vs O(n) linear search)
206 auto it = mTemplateMap.find(static_cast<int>(type));
207 if (it == mTemplateMap.end()) return 0.0f;
208
209 const ChordTemplate* tmpl = it->second;
210 if (!tmpl) return 0.0f;
211
212 // Calculate match score
213 float matchScore = 0.0f;
214 float totalChroma = 0.0f;
215
216 // Sum chroma energy for chord notes
217 for (int i = 0; i < tmpl->numNotes; i++) {
218 int interval = tmpl->intervals[i];
219 if (interval < 0) break;
220 int pitchClass = (root + interval) % 12;
221 matchScore += chroma[pitchClass];
222 }
223
224 // Sum total chroma energy
225 for (int i = 0; i < 12; i++) {
226 totalChroma += chroma[i];
227 }
228
229 // Penalize energy in non-chord notes
230 float nonChordEnergy = totalChroma - matchScore;
231
232 // Score is ratio of chord notes to total energy, minus non-chord penalty
233 float score = 0.0f;
234 if (totalChroma > 1e-6f) {
235 score = matchScore / totalChroma;
236 score -= 0.3f * (nonChordEnergy / totalChroma);
237 score = fl::max(0.0f, score);
238 }
239
240 return score;
241}
242
244 if (!a.isValid() || !b.isValid()) return false;
245
246 // Same root and type = similar
247 if (a.rootNote == b.rootNote && a.type == b.type) {
248 return true;
249 }
250
251 // Check for enharmonic equivalents or closely related chords
252 // For now, just exact match
253 return false;
254}
255
257 // Find max value
258 float maxVal = 0.0f;
259 for (int i = 0; i < 12; i++) {
260 if (chroma[i] > maxVal) {
261 maxVal = chroma[i];
262 }
263 }
264
265 // Normalize to 0-1 range
266 if (maxVal > 1e-6f) {
267 for (int i = 0; i < 12; i++) {
268 chroma[i] /= maxVal;
269 }
270 }
271}
272
273float ChordDetector::chromaDistance(const float* a, const float* b) FL_NO_EXCEPT {
274 float dist = 0.0f;
275 for (int i = 0; i < 12; i++) {
276 float diff = a[i] - b[i];
277 dist += diff * diff;
278 }
279 return fl::sqrt(dist);
280}
281
282// Out-of-line definitions for Chord methods (needed for linking with unity builds)
283const char* Chord::getRootName() const FL_NO_EXCEPT {
284 static const char* noteNames[] = {
285 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
286 };
287 return (rootNote >= 0 && rootNote < 12) ? noteNames[rootNote] : "?";
288}
289
290const char* Chord::getTypeName() const FL_NO_EXCEPT {
291 switch (type) {
292 case ChordType::MAJOR: return "maj";
293 case ChordType::MINOR: return "min";
294 case ChordType::DIMINISHED: return "dim";
295 case ChordType::AUGMENTED: return "aug";
296 case ChordType::MAJOR7: return "maj7";
297 case ChordType::MINOR7: return "min7";
298 case ChordType::DOMINANT7: return "7";
299 case ChordType::SUSPENDED2: return "sus2";
300 case ChordType::SUSPENDED4: return "sus4";
301 default: return "?";
302 }
303}
304
305} // namespace detector
306} // namespace audio
307} // namespace fl
function_list< void()> onChordEnd
Definition chord.h:63
bool isSimilarChord(const Chord &a, const Chord &b) FL_NO_EXCEPT
float matchChordPattern(const float *chroma, int root, ChordType type) FL_NO_EXCEPT
flat_map< int, const ChordTemplate * > mTemplateMap
Definition chord.h:94
function_list< void(const Chord &chord)> onChord
Definition chord.h:61
Chord detectChord(const float *chroma, u32 timestamp) FL_NO_EXCEPT
function_list< void(const Chord &chord)> onChordChange
Definition chord.h:62
float chromaDistance(const float *a, const float *b) FL_NO_EXCEPT
void update(shared_ptr< Context > context) FL_NO_EXCEPT override
Definition chord.cpp.hpp:61
void calculateChroma(const fft::Bins &fft) FL_NO_EXCEPT
void normalizeChroma(float *chroma) FL_NO_EXCEPT
~ChordDetector() FL_NO_EXCEPT override
shared_ptr< const fft::Bins > mRetainedFFT
Definition chord.h:96
void reset() FL_NO_EXCEPT override
void initializeTemplateMap() FL_NO_EXCEPT
Definition chord.cpp.hpp:53
void fireCallbacks() FL_NO_EXCEPT override
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 int kNumChordTemplates
Definition chord.cpp.hpp:34
static const ChordTemplate kChordTemplates[]
Definition chord.cpp.hpp:23
float logf(float value) FL_NO_EXCEPT
Definition math.h:426
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
constexpr enable_if< is_fixed_point< T >::value, T >::type sqrt(T x) FL_NO_EXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
bool isValid() const FL_NO_EXCEPT
Definition chord.h:41
const char * getTypeName() const FL_NO_EXCEPT
const char * getRootName() const FL_NO_EXCEPT