FastLED 3.9.15
Loading...
Searching...
No Matches
backbeat.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 : mBeatDetector(beatDetector)
12 , mDownbeatDetector(nullptr)
13 , mOwnsBeatDetector(false)
15 , mBackbeatDetected(false)
17 , mConfidence(0.0f)
18 , mCurrentStrength(0.0f)
19 , mBackbeatRatio(1.0f)
21 , mBassThreshold(1.2f)
22 , mMidThreshold(1.3f)
23 , mHighThreshold(1.1f)
24 , mBackbeatMask(0x0A) // Bits 1 and 3 = beats 2 and 4 in 4/4
25 , mAdaptive(true)
26 , mCurrentBeat(1)
28 , mPreviousWasBeat(false)
29 , mBackbeatMean(1.0f)
30 , mNonBackbeatMean(0.8f)
31 , mAdaptiveThreshold(1.0f)
32 , mProfileAlpha(0.1f)
33{
34 mPreviousAccent = {0.0f, 0.0f, 0.0f, 0.0f};
36 for (size i = 0; i < SPECTRAL_PROFILE_SIZE; i++) {
37 mBackbeatSpectralProfile.push_back(0.0f);
38 }
39}
40
42 shared_ptr<Downbeat> downbeatDetector)
43 : Backbeat(beatDetector)
44{
45 mDownbeatDetector = downbeatDetector;
47}
48
54
56
58 // Update Beat if we own it
60 updateBeatDetector(context);
61 }
62
63 // Update Downbeat if we own it
65 mDownbeatDetector->update(context);
66 }
67
68 // Reset backbeat flag
69 mBackbeatDetected = false;
70
71 // Check if a beat occurred
72 bool beatDetected = mBeatDetector && mBeatDetector->isBeat();
73
74 // Update beat position tracking
76
77 // Process beat if detected
78 if (beatDetected) {
79 // Get fft::FFT for multi-band analysis
80 mRetainedFFT = context->getFFT16();
81 const fft::Bins& fft = *mRetainedFFT;
82
83 // Calculate multi-band accent
85
86 // Detect backbeat accent strength
87 float accentStrength = detectBackbeatAccent(accent);
88 mCurrentStrength = accentStrength;
89
90 // Detect backbeat
91 mBackbeatDetected = detectBackbeat(accentStrength, fft);
92
95
96 // Update backbeat profile
97 if (mAdaptive) {
99 }
100
101 // Store accent in backbeat history
102 if (mBackbeatAccents.size() >= MAX_ACCENT_HISTORY) {
103 mBackbeatAccents.pop_front();
104 }
105 mBackbeatAccents.push_back(accentStrength);
106 } else {
107 // Store accent in non-backbeat history
109 mNonBackbeatAccents.pop_front();
110 }
111 mNonBackbeatAccents.push_back(accentStrength);
112 }
113
114 // Update adaptive thresholds
115 if (mAdaptive) {
117 }
118
119 // Store current accent for next frame
120 mPreviousAccent = accent;
121 }
122}
123
131
133 mBackbeatDetected = false;
135 mConfidence = 0.0f;
136 mCurrentStrength = 0.0f;
137 mBackbeatRatio = 1.0f;
138 mCurrentBeat = 1;
140 mPreviousWasBeat = false;
141 mPreviousAccent = {0.0f, 0.0f, 0.0f, 0.0f};
142 mBackbeatAccents.clear();
143 mNonBackbeatAccents.clear();
144 mBackbeatMean = 1.0f;
145 mNonBackbeatMean = 0.8f;
146 mAdaptiveThreshold = 1.0f;
147
148 for (size i = 0; i < mBackbeatSpectralProfile.size(); i++) {
149 mBackbeatSpectralProfile[i] = 0.0f;
150 }
151
152 mRetainedFFT.reset();
153
155 mBeatDetector->reset();
156 }
158 mDownbeatDetector->reset();
159 }
160}
161
163 if (beatDetector) {
164 mBeatDetector = beatDetector;
165 mOwnsBeatDetector = false;
166 }
167}
168
170 if (downbeatDetector) {
171 mDownbeatDetector = downbeatDetector;
172 mOwnsDownbeatDetector = false;
173 }
174}
175
177 if (mBeatDetector) {
178 mBeatDetector->update(context);
179 }
180}
181
183 if (!mBeatDetector) {
184 return;
185 }
186
187 bool currentlyBeat = mBeatDetector->isBeat();
188
189 // Detect beat transition (rising edge)
190 if (currentlyBeat && !mPreviousWasBeat) {
191 // Beat just occurred, advance position
192 if (mDownbeatDetector) {
193 // Use Downbeat for accurate position
194 mCurrentBeat = mDownbeatDetector->getCurrentBeat();
195 mBeatsPerMeasure = mDownbeatDetector->getBeatsPerMeasure();
196 } else {
197 // Standalone mode: assume 4/4 and cycle
198 mCurrentBeat++;
200 mCurrentBeat = 1;
201 }
202 }
203 }
204
205 mPreviousWasBeat = currentlyBeat;
206}
207
209 MultibandAccent accent;
210
211 // Define band ranges for 16-bin CQ log-spaced fft::FFT
212 // Bass: bins 0-3 (~175-400 Hz) - kick drum fundamentals
213 // Mid: bins 4-10 (~460-2100 Hz) - snare fundamental and harmonics
214 // High: bins 11-15 (~2450-4698 Hz) - hi-hats, cymbals
215
216 size numBins = fft.raw().size();
217 if (numBins == 0) {
218 accent = {0.0f, 0.0f, 0.0f, 0.0f};
219 return accent;
220 }
221
222 // Calculate current energy for each band
223 float bassEnergy = 0.0f;
224 float midEnergy = 0.0f;
225 float highEnergy = 0.0f;
226
227 // Adjust band ranges based on actual bin count
228 size bassEnd = fl::min(static_cast<size>(4), numBins);
229 size midStart = bassEnd;
230 size midEnd = fl::min(static_cast<size>(11), numBins);
231 size highStart = midEnd;
232 size highEnd = numBins;
233
234 // Sum energy in each band
235 for (size i = 0; i < bassEnd; i++) {
236 bassEnergy += fft.raw()[i];
237 }
238 for (size i = midStart; i < midEnd; i++) {
239 midEnergy += fft.raw()[i];
240 }
241 for (size i = highStart; i < highEnd; i++) {
242 highEnergy += fft.raw()[i];
243 }
244
245 // Normalize by bin count
246 bassEnergy /= static_cast<float>(bassEnd);
247 if (midEnd > midStart) {
248 midEnergy /= static_cast<float>(midEnd - midStart);
249 }
250 if (highEnd > highStart) {
251 highEnergy /= static_cast<float>(highEnd - highStart);
252 }
253
254 // Calculate accent strength for each band (current vs previous)
255 // Using logarithmic ratio with normalization
256 const float epsilon = 1e-6f;
257 const float maxRatio = 10.0f; // Maximum expected energy increase
258
259 float bassRatio = (mPreviousAccent.bass > epsilon)
260 ? (bassEnergy / mPreviousAccent.bass)
261 : 1.0f;
262 float midRatio = (mPreviousAccent.mid > epsilon)
263 ? (midEnergy / mPreviousAccent.mid)
264 : 1.0f;
265 float highRatio = (mPreviousAccent.high > epsilon)
266 ? (highEnergy / mPreviousAccent.high)
267 : 1.0f;
268
269 // Apply logarithmic scaling and normalize to 0-1 range
270 float logMaxRatio = fl::log10f(1.0f + maxRatio);
271 accent.bass = fl::clamp(fl::log10f(1.0f + bassRatio) / logMaxRatio, 0.0f, 1.0f);
272 accent.mid = fl::clamp(fl::log10f(1.0f + midRatio) / logMaxRatio, 0.0f, 1.0f);
273 accent.high = fl::clamp(fl::log10f(1.0f + highRatio) / logMaxRatio, 0.0f, 1.0f);
274
275 // Store raw energies for next comparison (not normalized accents)
276 accent.bass = bassEnergy;
277 accent.mid = midEnergy;
278 accent.high = highEnergy;
279
280 // Actually calculate accents properly
281 float bassAccent = fl::clamp(fl::log10f(1.0f + bassRatio) / logMaxRatio, 0.0f, 1.0f);
282 float midAccent = fl::clamp(fl::log10f(1.0f + midRatio) / logMaxRatio, 0.0f, 1.0f);
283 float highAccent = fl::clamp(fl::log10f(1.0f + highRatio) / logMaxRatio, 0.0f, 1.0f);
284
285 // Weighted combination emphasizing mid-range (snare)
286 // Bass: 0.3, Mid: 0.5, High: 0.2
287 accent.total = (bassAccent * 0.3f) + (midAccent * 0.5f) + (highAccent * 0.2f);
288
289 // Return struct with raw energies stored for next frame
290 return {bassEnergy, midEnergy, highEnergy, accent.total};
291}
292
294 // The accent strength is already calculated in calculateMultibandAccent
295 // This method could apply additional processing if needed
296 return accent.total;
297}
298
300 // Check if current beat number matches backbeat mask
301 // Beat numbers are 1-based, mask is 0-based
302 if (mCurrentBeat == 0 || mCurrentBeat > 8) {
303 return false; // Invalid beat number
304 }
305
306 u8 bitIndex = mCurrentBeat - 1; // Convert to 0-based
307 return (mBackbeatMask & (1 << bitIndex)) != 0;
308}
309
310bool Backbeat::detectBackbeat(float accentStrength, const fft::Bins& fft) {
311 // Check if we're at a backbeat position
312 bool atBackbeatPosition = isBackbeatPosition();
313
314 if (!atBackbeatPosition) {
315 mConfidence = 0.0f;
316 return false;
317 }
318
319 // Calculate accent confidence (relative to adaptive threshold)
320 float accentConfidence = 0.0f;
322 // Compare to learned distribution
323 float separation = mBackbeatMean - mNonBackbeatMean;
324 if (separation > 1e-6f) {
325 accentConfidence = (accentStrength - mNonBackbeatMean) / separation;
326 accentConfidence = fl::clamp(accentConfidence, 0.0f, 1.0f);
327 }
328 } else {
329 // Use fixed threshold
330 accentConfidence = (accentStrength >= mAdaptiveThreshold) ? 1.0f : 0.0f;
331 }
332
333 // Position confidence (we're at backbeat position)
334 float positionConfidence = 1.0f;
335
336 // Pattern confidence (spectral similarity to learned profile)
337 float patternConfidence = calculatePatternConfidence(fft);
338
339 // Combined confidence
340 // Weights: accent 40%, position 30%, pattern 30%
341 mConfidence = (accentConfidence * 0.4f) +
342 (positionConfidence * 0.3f) +
343 (patternConfidence * 0.3f);
344
345 // Detect backbeat if confidence exceeds threshold
347}
348
350 // Update mean accent strengths for backbeat and non-backbeat positions
351 if (!mBackbeatAccents.empty()) {
352 float sum = 0.0f;
353 for (size i = 0; i < mBackbeatAccents.size(); i++) {
354 sum += mBackbeatAccents[i];
355 }
356 mBackbeatMean = sum / static_cast<float>(mBackbeatAccents.size());
357 }
358
359 if (!mNonBackbeatAccents.empty()) {
360 float sum = 0.0f;
361 for (size i = 0; i < mNonBackbeatAccents.size(); i++) {
362 sum += mNonBackbeatAccents[i];
363 }
364 mNonBackbeatMean = sum / static_cast<float>(mNonBackbeatAccents.size());
365 }
366
367 // Adaptive threshold is midpoint between means
370 }
371
372 // Calculate backbeat ratio (for state access)
373 if (mNonBackbeatMean > 1e-6f) {
375 } else {
376 mBackbeatRatio = 1.0f;
377 }
378}
379
381 // Update spectral profile using exponential moving average
382 // This learns the typical frequency content of backbeats
383
384 size profileSize = fl::min(mBackbeatSpectralProfile.size(), fft.raw().size());
385
386 for (size i = 0; i < profileSize; i++) {
387 // EMA: profile = alpha * new + (1 - alpha) * old
389 (mProfileAlpha * fft.raw()[i]) +
391 }
392}
393
395 // Calculate spectral correlation between current spectrum and learned profile
396 // Returns 0-1 indicating how well current spectrum matches typical backbeat
397
398 // If profile is not yet learned, return neutral confidence
399 bool profileLearned = false;
400 for (size i = 0; i < mBackbeatSpectralProfile.size(); i++) {
401 if (mBackbeatSpectralProfile[i] > 1e-6f) {
402 profileLearned = true;
403 break;
404 }
405 }
406
407 if (!profileLearned) {
408 return 0.5f; // Neutral confidence
409 }
410
411 // Calculate normalized correlation
412 size compareSize = fl::min(mBackbeatSpectralProfile.size(), fft.raw().size());
413
414 float dotProduct = 0.0f;
415 float profileMag = 0.0f;
416 float currentMag = 0.0f;
417
418 for (size i = 0; i < compareSize; i++) {
419 dotProduct += mBackbeatSpectralProfile[i] * fft.raw()[i];
421 currentMag += fft.raw()[i] * fft.raw()[i];
422 }
423
424 // Cosine similarity
425 const float epsilon = 1e-6f;
426 float denominator = fl::sqrt(profileMag * currentMag);
427
428 if (denominator < epsilon) {
429 return 0.5f; // Neutral if magnitudes too small
430 }
431
432 float correlation = dotProduct / denominator;
433
434 // Clamp to 0-1 range
435 return fl::clamp(correlation, 0.0f, 1.0f);
436}
437
438} // namespace detector
439} // namespace audio
440} // namespace fl
void setDownbeatDetector(shared_ptr< Downbeat > downbeatDetector)
Share an external Downbeat instance.
shared_ptr< Beat > mBeatDetector
Definition backbeat.h:122
void update(shared_ptr< Context > context) override
MultibandAccent calculateMultibandAccent(const fft::Bins &fft)
float detectBackbeatAccent(const MultibandAccent &accent)
deque< float > mBackbeatAccents
Definition backbeat.h:149
MultibandAccent mPreviousAccent
Definition backbeat.h:148
void setBeatDetector(shared_ptr< Beat > beatDetector)
Share an external Beat instance.
static constexpr size MAX_ACCENT_HISTORY
Definition backbeat.h:151
function_list< void(u8 beatNumber, float confidence, float strength)> onBackbeat
Fires on detected backbeat (beats 2, 4) with beat number, confidence, and strength.
Definition backbeat.h:75
bool detectBackbeat(float accentStrength, const fft::Bins &fft)
void updateBeatDetector(shared_ptr< Context > context)
float calculatePatternConfidence(const fft::Bins &fft)
vector< float > mBackbeatSpectralProfile
Definition backbeat.h:159
~Backbeat() FL_NOEXCEPT override
shared_ptr< Downbeat > mDownbeatDetector
Definition backbeat.h:123
shared_ptr< const fft::Bins > mRetainedFFT
Definition backbeat.h:163
void updateBackbeatProfile(const fft::Bins &fft)
Backbeat() FL_NOEXCEPT
Construct with standalone Beat.
static constexpr size SPECTRAL_PROFILE_SIZE
Definition backbeat.h:160
Backbeat(shared_ptr< Beat > beatDetector)
Construct with shared Beat.
deque< float > mNonBackbeatAccents
Definition backbeat.h:150
Multi-band accent information for backbeat detection.
Definition backbeat.h:17
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 enable_if< is_fixed_point< T >::value, T >::type sqrt(T x) FL_NOEXCEPT
float log10f(float value) FL_NOEXCEPT
Definition math.h:424
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
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