FastLED 3.9.15
Loading...
Searching...
No Matches
downbeat.cpp.hpp
Go to the documentation of this file.
3#include "fl/math/math.h"
4#include "fl/stl/array.h"
5#include "fl/stl/noexcept.h"
6
7namespace fl {
8namespace audio {
9namespace detector {
10
12 : mBeatDetector(beatDetector)
13 , mOwnsBeatDetector(false)
14 , mDownbeatDetected(false)
15 , mCurrentBeat(1)
17 , mMeasurePhase(0.0f)
18 , mConfidence(0.0f)
20 , mAccentThreshold(1.2f)
22 , mManualMeter(false)
24 , mLastBeatTime(0)
26 , mPreviousEnergy(0.0f)
27{
28}
29
35
37
39 // Update Beat if we own it
41 updateBeatDetector(context);
42 }
43
44 // Get current state from Beat
45 bool beatDetected = mBeatDetector->isBeat();
46 u32 timestamp = context->getTimestamp();
47
48 // Reset downbeat flag
49 mDownbeatDetected = false;
50
51 // If beat detected, analyze for downbeat
52 if (beatDetected) {
53 // Get fft::FFT for accent analysis
54 mRetainedFFT = context->getFFT16();
55 const fft::Bins& fft = *mRetainedFFT;
56
57 // Calculate current energy (bass-weighted for accent detection)
58 float bassEnergy = 0.0f;
59 for (size i = 0; i < fl::min(static_cast<size>(4), fft.raw().size()); i++) {
60 bassEnergy += fft.raw()[i];
61 }
62 bassEnergy /= 4.0f;
63
64 // Calculate accent strength
65 float accent = calculateBeatAccent(fft, bassEnergy);
66
67 // Store accent in history
68 if (mBeatAccents.size() >= MAX_BEAT_HISTORY) {
69 mBeatAccents.pop_front();
70 }
71 mBeatAccents.push_back(accent);
72
73 // Detect downbeat
74 mDownbeatDetected = detectDownbeat(timestamp, accent);
75
77 mCurrentBeat = 1;
79 mLastDownbeatTime = timestamp;
80
81 // Set callback flags
82 mFireDownbeat = true;
83 mFireMeasureBeat = true;
85
86 // Attempt meter detection if enabled
89 }
90 } else {
91 // Not a downbeat, increment beat counter
94
95 // Set beat callback flag
96 mFireMeasureBeat = true;
98
99 // Check if we should force a downbeat (measure boundary)
101 // Force downbeat on measure boundary
102 mDownbeatDetected = true;
103 mCurrentBeat = 1;
105 mLastDownbeatTime = timestamp;
106
107 mFireDownbeat = true;
108 mFireMeasureBeat = true;
110 }
111 }
112
113 mLastBeatTime = timestamp;
114 mPreviousEnergy = bassEnergy;
115 }
116
117 // Update measure phase
118 updateMeasurePhase(timestamp);
119}
120
122 if (mFireDownbeat) {
123 if (onDownbeat) onDownbeat();
124 mFireDownbeat = false;
125 }
126 if (mFireMeasureBeat) {
128 mFireMeasureBeat = false;
129 }
130 if (mFireMeterChange) {
132 mFireMeterChange = false;
133 }
134 if (onMeasurePhase) {
136 }
137}
138
140 mDownbeatDetected = false;
141 mCurrentBeat = 1;
143 mMeasurePhase = 0.0f;
144 mConfidence = 0.0f;
146 mLastBeatTime = 0;
148 mPreviousEnergy = 0.0f;
149 mBeatAccents.clear();
150 mMeterCandidates.clear();
151 mManualMeter = false;
152
154 mBeatDetector->reset();
155 }
156}
157
158void Downbeat::setTimeSignature(u8 beatsPerMeasure) {
159 if (beatsPerMeasure >= 2 && beatsPerMeasure <= 16) {
160 u8 oldMeter = mBeatsPerMeasure;
161 mBeatsPerMeasure = beatsPerMeasure;
162 mManualMeter = true;
163 mAutoMeterDetection = false;
164
165 // Fire meter change callback if changed
166 if (oldMeter != mBeatsPerMeasure && onMeterChange) {
168 }
169
170 // Reset beat counter to avoid invalid state
171 mCurrentBeat = 1;
173 }
174}
175
177 if (beatDetector) {
178 mBeatDetector = beatDetector;
179 mOwnsBeatDetector = false;
180 }
181}
182
184 if (mBeatDetector) {
185 mBeatDetector->update(context);
186 }
187}
188
189float Downbeat::calculateBeatAccent(const fft::Bins& fft, float bassEnergy) {
190 // Accent detection combines multiple factors:
191 // 1. Energy increase (stronger accent = more energy)
192 // 2. Bass energy (downbeats typically have more bass)
193 // 3. Spectral flux (onset strength)
194
195 // Energy increase relative to previous beat
196 float energyRatio = 1.0f;
197 if (mPreviousEnergy > 1e-6f) {
198 energyRatio = bassEnergy / mPreviousEnergy;
199 }
200
201 // Calculate overall energy
202 float totalEnergy = 0.0f;
203 for (size i = 0; i < fft.raw().size(); i++) {
204 totalEnergy += fft.raw()[i];
205 }
206 totalEnergy /= static_cast<float>(fft.raw().size());
207
208 // Bass ratio (downbeats typically have relatively more bass)
209 float bassRatio = 1.0f;
210 if (totalEnergy > 1e-6f) {
211 bassRatio = bassEnergy / totalEnergy;
212 }
213
214 // Combine factors (weighted average)
215 float accent = (energyRatio * 0.4f) + (bassRatio * 0.3f) + (totalEnergy * 0.3f);
216
217 return accent;
218}
219
220bool Downbeat::detectDownbeat(u32 timestamp, float accent) {
221 // Downbeat detection strategy:
222 // 1. Check if we're at the expected measure boundary
223 // 2. Check if accent is strong enough
224 // 3. Check if pattern matches metric grouping
225
226 // If we haven't detected any downbeats yet, consider this one
227 if (mLastDownbeatTime == 0) {
228 // Use accent strength for initial confidence instead of fixed 0.5
229 // This provides a better estimate based on actual audio characteristics
230 float meanAccent = 1.0f;
231 if (!mBeatAccents.empty()) {
232 float sum = 0.0f;
233 for (size i = 0; i < mBeatAccents.size(); i++) {
234 sum += mBeatAccents[i];
235 }
236 meanAccent = sum / static_cast<float>(mBeatAccents.size());
237 }
238
239 // Calculate accent-based confidence
240 // If we have accent history, use it; otherwise use raw accent with moderate confidence
241 float accentConfidence = meanAccent > 0.0f
242 ? fl::clamp(accent / (meanAccent * mAccentThreshold), 0.0f, 1.0f)
243 : fl::clamp(accent * 0.5f, 0.3f, 0.7f); // Raw accent scaled to 30-70% range
244
245 mConfidence = accentConfidence;
246 return true;
247 }
248
249 // Calculate expected downbeat position
250 u32 timeSinceDownbeat = timestamp - mLastDownbeatTime;
251 float beatInterval = mBeatDetector->getBPM() > 0.0f
252 ? (60000.0f / mBeatDetector->getBPM())
253 : 500.0f;
254 float expectedMeasureDuration = beatInterval * static_cast<float>(mBeatsPerMeasure);
255
256 // Check if we're near expected measure boundary
257 float timingError = fl::abs(static_cast<float>(timeSinceDownbeat) - expectedMeasureDuration);
258 float maxTimingError = beatInterval * 0.4f; // Allow 40% timing error
259 bool nearMeasureBoundary = (timingError < maxTimingError);
260
261 // Check if accent is strong enough
262 float meanAccent = 1.0f;
263 if (!mBeatAccents.empty()) {
264 float sum = 0.0f;
265 for (size i = 0; i < mBeatAccents.size(); i++) {
266 sum += mBeatAccents[i];
267 }
268 meanAccent = sum / static_cast<float>(mBeatAccents.size());
269 }
270
271 bool strongAccent = (accent > meanAccent * mAccentThreshold);
272
273 // Check if we're at the beat counter boundary
274 bool atBeatCounterBoundary = (mBeatsSinceDownbeat >= mBeatsPerMeasure - 1);
275
276 // Calculate confidence
277 float timingConfidence = 1.0f - (timingError / (beatInterval * 2.0f));
278 timingConfidence = fl::clamp(timingConfidence, 0.0f, 1.0f);
279
280 float accentConfidence = meanAccent > 0.0f
281 ? fl::clamp(accent / (meanAccent * mAccentThreshold), 0.0f, 1.0f)
282 : 0.5f;
283
284 // Adaptive weighting: favor accent when at beat boundary (structural downbeat)
285 // This improves recall for first downbeat after warm-up
286 float accentWeight = atBeatCounterBoundary ? 0.7f : 0.5f;
287 float timingWeight = atBeatCounterBoundary ? 0.3f : 0.5f;
288 mConfidence = (timingConfidence * timingWeight) + (accentConfidence * accentWeight);
289
290 // Additional confidence boost for structural downbeats (at beat boundary)
291 // This compensates for timing uncertainties in the first few measures
292 if (atBeatCounterBoundary && mConfidence < 0.6f) {
293 mConfidence = fl::max(mConfidence, 0.55f); // Ensure minimum confidence at boundaries
294 }
295
296 // Determine if this is a downbeat
297 bool isDownbeat = false;
298
299 if (atBeatCounterBoundary) {
300 // We're at expected measure boundary - high likelihood of downbeat
301 isDownbeat = true;
302 } else if (nearMeasureBoundary && strongAccent) {
303 // Early/late downbeat with strong accent
305 } else if (strongAccent && mBeatsSinceDownbeat == 0) {
306 // Strong accent on first beat (might be downbeat)
308 }
309
310 return isDownbeat;
311}
312
314 // Analyze recent beat patterns to detect time signature
315 // Look for recurring patterns in beat intervals and accents
316
317 // Add current meter candidate based on beat count
318 u8 detectedMeter = mBeatsPerMeasure;
319
320 // Analyze accent patterns to infer meter
321 if (mBeatAccents.size() >= 8) {
322 // Look for recurring strong accents
323 // Common meters: 4/4 (every 4 beats), 3/4 (every 3), 6/8 (every 6)
324
325 size numAccents = mBeatAccents.size();
326 fl::array<u8, 5> candidateMeters = {2, 3, 4, 6, 8};
327 float bestScore = 0.0f;
328 u8 bestMeter = 4;
329
330 for (size m = 0; m < candidateMeters.size(); m++) {
331 u8 meter = candidateMeters[m];
332 float score = 0.0f;
333
334 // Score based on accent pattern matching
335 for (size i = 0; i < numAccents; i++) {
336 if (i % meter == 0) {
337 // Expect strong accent at measure start
338 score += mBeatAccents[i];
339 } else {
340 // Expect weaker accent elsewhere
341 score += (2.0f - mBeatAccents[i]) * 0.5f;
342 }
343 }
344
345 if (score > bestScore) {
346 bestScore = score;
347 bestMeter = meter;
348 }
349 }
350
351 detectedMeter = bestMeter;
352 }
353
354 // Add to meter history
355 if (mMeterCandidates.size() >= METER_HISTORY_SIZE) {
356 mMeterCandidates.pop_front();
357 }
358 mMeterCandidates.push_back(detectedMeter);
359
360 // Find most common meter in recent history
361 u8 consensusMeter = findMostCommonMeter();
362
363 // Update meter if consensus differs and is stable
364 if (consensusMeter != mBeatsPerMeasure && mMeterCandidates.size() >= METER_HISTORY_SIZE / 2) {
365 (void)mBeatsPerMeasure; // Suppress unused warning (used in comparison above)
366 mBeatsPerMeasure = consensusMeter;
367
368 // Set meter change callback flag
369 mFireMeterChange = true;
371
372 // Reset beat counter
373 mCurrentBeat = 1;
375 }
376}
377
378void Downbeat::updateMeasurePhase(u32 timestamp) {
379 if (mLastDownbeatTime == 0) {
380 mMeasurePhase = 0.0f;
381 return;
382 }
383
384 u32 timeSinceDownbeat = timestamp - mLastDownbeatTime;
385 float beatInterval = mBeatDetector->getBPM() > 0.0f
386 ? (60000.0f / mBeatDetector->getBPM())
387 : 500.0f;
388 float measureDuration = beatInterval * static_cast<float>(mBeatsPerMeasure);
389
390 if (measureDuration > 0.0f) {
391 mMeasurePhase = static_cast<float>(timeSinceDownbeat) / measureDuration;
392
393 // Wrap phase to [0, 1)
394 while (mMeasurePhase >= 1.0f) {
395 mMeasurePhase -= 1.0f;
396 }
397 } else {
398 mMeasurePhase = 0.0f;
399 }
400}
401
403 if (mMeterCandidates.empty()) {
404 return 4; // Default to 4/4
405 }
406
407 // Count occurrences of each meter
408 fl::array<u8, 16> counts = {}; // Support meters 2-16
409
410 for (size i = 0; i < mMeterCandidates.size(); i++) {
411 u8 meter = mMeterCandidates[i];
412 if (meter >= 2 && meter < 16) {
413 counts[meter]++;
414 }
415 }
416
417 // Find most common
418 u8 maxCount = 0;
419 u8 mostCommonMeter = 4;
420
421 for (u8 meter = 2; meter < 16; meter++) {
422 if (counts[meter] > maxCount) {
423 maxCount = counts[meter];
424 mostCommonMeter = meter;
425 }
426 }
427
428 return mostCommonMeter;
429}
430
431} // namespace detector
432} // namespace audio
433} // namespace fl
fl::size size() const FL_NOEXCEPT
Definition array.h:90
A fixed-size array implementation similar to std::array.
Definition array.h:27
void setBeatDetector(shared_ptr< Beat > beatDetector)
Share an external Beat instance.
float calculateBeatAccent(const fft::Bins &fft, float bassEnergy)
bool isDownbeat() const
Returns true if downbeat was detected this frame.
Definition downbeat.h:71
~Downbeat() FL_NOEXCEPT override
deque< float > mBeatAccents
Definition downbeat.h:127
function_list< void(float phase)> onMeasurePhase
Fires with measure phase each frame (0-1 range)
Definition downbeat.h:66
Downbeat(shared_ptr< Beat > beatDetector)
Construct with shared Beat (recommended)
Downbeat() FL_NOEXCEPT
Construct with standalone Beat.
void setTimeSignature(u8 beatsPerMeasure)
Manually set time signature (disables auto-detection)
function_list< void()> onDownbeat
Fires on detected downbeat (first beat of measure)
Definition downbeat.h:57
shared_ptr< Beat > mBeatDetector
Definition downbeat.h:104
void update(shared_ptr< Context > context) override
void updateMeasurePhase(u32 timestamp)
void updateBeatDetector(shared_ptr< Context > context)
function_list< void(u8 beatsPerMeasure)> onMeterChange
Fires when time signature changes.
Definition downbeat.h:63
static constexpr size METER_HISTORY_SIZE
Definition downbeat.h:132
static constexpr size MAX_BEAT_HISTORY
Definition downbeat.h:128
function_list< void(u8 beatNumber)> onMeasureBeat
Fires on each beat with beat number (1-based, downbeat = 1)
Definition downbeat.h:60
shared_ptr< const fft::Bins > mRetainedFFT
Definition downbeat.h:141
bool detectDownbeat(u32 timestamp, float accent)
unsigned char u8
Definition stdint.h:131
constexpr common_type_t< T, U > max(T a, U b) FL_NOEXCEPT
Definition math.h:75
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
constexpr enable_if< is_fixed_point< T >::value, T >::type abs(T x) FL_NOEXCEPT
constexpr enable_if< is_fixed_point< T >::value, T >::type clamp(T x, T lo, T hi) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT