FastLED 3.9.15
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()
87 << mCurrentChord.getTypeName()
88 << " (conf: " << mCurrentChord.confidence << ")");
89 } else {
90 // Same chord, update confidence
91 mCurrentChord.confidence = detected.confidence;
92 mCurrentChord.timestamp = timestamp;
93 }
94
95 // Set flag for onChord callback every frame when chord is active
96 mFireChord = true;
97 } else {
98 // No valid chord or low confidence
99 if (mCurrentChord.isValid()) {
100 u32 duration = timestamp - mChordStartTime;
101 if (duration >= mMinChordDuration) {
102 mChordEndTime = timestamp;
103 mFireChordEnd = true;
104 }
105 mCurrentChord = Chord(); // Reset to invalid
106 }
107 }
108
109 // Save chroma for next frame
110 for (int i = 0; i < 12; i++) {
111 mPrevChroma[i] = mChroma[i];
112 }
113}
114
116 if (mFireChordEnd) {
117 if (onChordEnd) onChordEnd();
118 mFireChordEnd = false;
119 }
120 if (mFireChordChange) {
122 mFireChordChange = false;
123 }
124 if (mFireChord) {
126 mFireChord = false;
127 }
128}
129
133 mChordStartTime = 0;
134 mChordEndTime = 0;
135 for (int i = 0; i < 12; i++) {
136 mChroma[i] = 0.0f;
137 mPrevChroma[i] = 0.0f;
138 }
139}
140
142 // Clear chroma
143 for (int i = 0; i < 12; i++) {
144 mChroma[i] = 0.0f;
145 }
146
147 // Map fft::FFT bins to pitch classes (chroma) using linearly-rebinned magnitudes.
148 // Linear bins give evenly-spaced frequency mapping: freq = fmin + bin * binWidth.
149 fl::span<const float> linearBins = fft.linear();
150 const fl::size numBins = linearBins.size();
151 const float fmin = fft.linearFmin();
152 const float fmax = fft.linearFmax();
153 const float linearBinWidth = (numBins > 0) ? (fmax - fmin) / static_cast<float>(numBins) : 1.0f;
154
155 for (fl::size binIdx = 0; binIdx < numBins; binIdx++) {
156 float magnitude = linearBins[binIdx];
157 if (magnitude < 1e-6f) continue;
158
159 // Calculate frequency for this linearly-spaced bin
160 float freq = fmin + (static_cast<float>(binIdx) + 0.5f) * linearBinWidth;
161
162 // Skip frequencies below 60Hz (too low for chord detection)
163 if (freq < 60.0f) continue;
164
165 // Convert frequency to MIDI note number (A4 = 440Hz = MIDI 69)
166 float midiNote = 69.0f + 12.0f * (fl::logf(freq / 440.0f) / fl::logf(2.0f));
167
168 // Get pitch class (0-11)
169 int pitchClass = static_cast<int>(midiNote + 0.5f) % 12;
170 if (pitchClass < 0) pitchClass += 12;
171
172 // Accumulate magnitude into chroma
173 mChroma[pitchClass] += magnitude;
174 }
175
176 // Normalize chroma
178}
179
180Chord ChordDetector::detectChord(const float* chroma, u32 timestamp) {
181 float bestScore = 0.0f;
182 int bestRoot = -1;
183 ChordType bestType = ChordType::UNKNOWN;
184
185 // Try all root notes (0-11)
186 for (int root = 0; root < 12; root++) {
187 // Try all chord templates
188 for (int t = 0; t < kNumChordTemplates; t++) {
189 float score = matchChordPattern(chroma, root, kChordTemplates[t].type);
190 if (score > bestScore) {
191 bestScore = score;
192 bestRoot = root;
193 bestType = kChordTemplates[t].type;
194 }
195 }
196 }
197
198 // Confidence threshold
199 if (bestScore < 0.3f) {
200 return Chord(); // No valid chord
201 }
202
203 return Chord(bestRoot, bestType, bestScore, timestamp);
204}
205
206float ChordDetector::matchChordPattern(const float* chroma, int root, ChordType type) {
207 // Find the matching template using pre-computed lookup (O(1) vs O(n) linear search)
208 auto it = mTemplateMap.find(static_cast<int>(type));
209 if (it == mTemplateMap.end()) return 0.0f;
210
211 const ChordTemplate* tmpl = it->second;
212 if (!tmpl) return 0.0f;
213
214 // Calculate match score
215 float matchScore = 0.0f;
216 float totalChroma = 0.0f;
217
218 // Sum chroma energy for chord notes
219 for (int i = 0; i < tmpl->numNotes; i++) {
220 int interval = tmpl->intervals[i];
221 if (interval < 0) break;
222 int pitchClass = (root + interval) % 12;
223 matchScore += chroma[pitchClass];
224 }
225
226 // Sum total chroma energy
227 for (int i = 0; i < 12; i++) {
228 totalChroma += chroma[i];
229 }
230
231 // Penalize energy in non-chord notes
232 float nonChordEnergy = totalChroma - matchScore;
233
234 // Score is ratio of chord notes to total energy, minus non-chord penalty
235 float score = 0.0f;
236 if (totalChroma > 1e-6f) {
237 score = matchScore / totalChroma;
238 score -= 0.3f * (nonChordEnergy / totalChroma);
239 score = fl::max(0.0f, score);
240 }
241
242 return score;
243}
244
245bool ChordDetector::isSimilarChord(const Chord& a, const Chord& b) {
246 if (!a.isValid() || !b.isValid()) return false;
247
248 // Same root and type = similar
249 if (a.rootNote == b.rootNote && a.type == b.type) {
250 return true;
251 }
252
253 // Check for enharmonic equivalents or closely related chords
254 // For now, just exact match
255 return false;
256}
257
259 // Find max value
260 float maxVal = 0.0f;
261 for (int i = 0; i < 12; i++) {
262 if (chroma[i] > maxVal) {
263 maxVal = chroma[i];
264 }
265 }
266
267 // Normalize to 0-1 range
268 if (maxVal > 1e-6f) {
269 for (int i = 0; i < 12; i++) {
270 chroma[i] /= maxVal;
271 }
272 }
273}
274
275float ChordDetector::chromaDistance(const float* a, const float* b) {
276 float dist = 0.0f;
277 for (int i = 0; i < 12; i++) {
278 float diff = a[i] - b[i];
279 dist += diff * diff;
280 }
281 return fl::sqrt(dist);
282}
283
284// Out-of-line definitions for Chord methods (needed for linking with unity builds)
285const char* Chord::getRootName() const {
286 static const char* noteNames[] = {
287 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
288 };
289 return (rootNote >= 0 && rootNote < 12) ? noteNames[rootNote] : "?";
290}
291
292const char* Chord::getTypeName() const {
293 switch (type) {
294 case ChordType::MAJOR: return "maj";
295 case ChordType::MINOR: return "min";
296 case ChordType::DIMINISHED: return "dim";
297 case ChordType::AUGMENTED: return "aug";
298 case ChordType::MAJOR7: return "maj7";
299 case ChordType::MINOR7: return "min7";
300 case ChordType::DOMINANT7: return "7";
301 case ChordType::SUSPENDED2: return "sus2";
302 case ChordType::SUSPENDED4: return "sus4";
303 default: return "?";
304 }
305}
306
307} // namespace detector
308} // namespace audio
309} // namespace fl
function_list< void()> onChordEnd
Definition chord.h:63
bool isSimilarChord(const Chord &a, const Chord &b)
flat_map< int, const ChordTemplate * > mTemplateMap
Definition chord.h:94
void normalizeChroma(float *chroma)
function_list< void(const Chord &chord)> onChord
Definition chord.h:61
float chromaDistance(const float *a, const float *b)
function_list< void(const Chord &chord)> onChordChange
Definition chord.h:62
void calculateChroma(const fft::Bins &fft)
void update(shared_ptr< Context > context) override
Definition chord.cpp.hpp:61
float matchChordPattern(const float *chroma, int root, ChordType type)
shared_ptr< const fft::Bins > mRetainedFFT
Definition chord.h:96
~ChordDetector() FL_NOEXCEPT override
Chord detectChord(const float *chroma, u32 timestamp)
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
#define FL_DBG
Definition log.h:388
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
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 sqrt(T x) FL_NOEXCEPT
float logf(float value) FL_NOEXCEPT
Definition math.h:418
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT
const char * getTypeName() const
bool isValid() const
Definition chord.h:41
const char * getRootName() const