FastLED 3.9.15
Loading...
Searching...
No Matches
beat.cpp.hpp
Go to the documentation of this file.
3#include "fl/math/math.h"
4#include "fl/stl/algorithm.h"
5#include "fl/stl/noexcept.h"
6
7namespace fl {
8namespace audio {
9namespace detector {
10
12 : mBeatDetected(false)
13 , mBPM(120.0f)
14 , mPhase(0.0f)
15 , mConfidence(0.0f)
16 , mThreshold(1.3f)
17 , mSensitivity(1.0f)
18 , mSpectralFlux(0.0f)
19 , mLastBeatTime(0)
20 , mBeatInterval(500)
21 , mAdaptiveThreshold(0.0f)
22{
23 mPreviousMagnitudes.resize(16, 0.0f);
24}
25
26Beat::~Beat() FL_NOEXCEPT = default;
27
28void Beat::update(shared_ptr<Context> context) {
29 // Use 30 Hz min frequency so bass bins actually cover sub-bass (20-60 Hz).
30 // Default fmin (90 Hz) covers most bass but misses the deepest sub-bass.
31 // CQ_NAIVE provides bass/treble discrimination via CQ kernels
32 // (which include built-in Hamming windowing), while being faster
33 // than CQ_OCTAVE. The 16-bin / 30-14080Hz range has N_window=1,
34 // which is borderline for CQ_NAIVE but acceptable for beat detection.
35 mRetainedFFT = context->getFFT(16, 30.0f,
38 const fft::Bins& fft = *mRetainedFFT;
39 u32 timestamp = context->getTimestamp();
40
41 // Calculate spectral flux
43
44 // Detect beat BEFORE updating adaptive threshold.
45 // If we update the threshold first, the current frame's (potentially
46 // high) spectral flux inflates the running average, raising the
47 // threshold at the exact moment we need it lowest (onset detection).
48 mBeatDetected = detectBeat(timestamp);
49
50 // Update adaptive threshold AFTER beat detection
52
53 if (mBeatDetected) {
54 updateTempo(timestamp);
55 mLastBeatTime = timestamp;
56 }
57
58 // Update phase regardless of beat detection
59 updatePhase(timestamp);
60
61 // Update previous magnitudes for next frame
62 for (size i = 0; i < fft.raw().size() && i < mPreviousMagnitudes.size(); i++) {
63 mPreviousMagnitudes[i] = fft.raw()[i];
64 }
65}
66
68 mBeatDetected = false;
69 mBPM = 120.0f;
70 mPhase = 0.0f;
71 mConfidence = 0.0f;
72 mSpectralFlux = 0.0f;
73 mLastBeatTime = 0;
74 mBeatInterval = 500;
75 mAdaptiveThreshold = 0.0f;
77 mFluxAvg.reset();
78}
79
81 float flux = 0.0f;
82 size numBins = fl::min(fft.raw().size(), mPreviousMagnitudes.size());
83
84 // Use the bass half of fft::FFT bins for beat detection.
85 // Musical beats (kick drums) have energy at 60-200 Hz.
86 // With 16 CQ log-spaced bins from 30-4698 Hz:
87 // bins 0-3 cover ~30-82 Hz (sub-bass)
88 // bins 4-7 cover ~82-226 Hz (bass/low-mid, kick fundamentals)
89 // Using numBins/2 = 8 bins covers the full kick drum range (~30-226 Hz).
90 // Treble transients (hi-hats, cymbals) in bins 8-15 are excluded.
91 size bassBins = numBins / 2;
92 if (bassBins < 1) bassBins = 1;
93
94 for (size i = 0; i < bassBins; i++) {
95 float diff = fft.raw()[i] - mPreviousMagnitudes[i];
96 if (diff > 0.0f) {
97 flux += diff;
98 }
99 }
100
101 return flux / static_cast<float>(bassBins);
102}
103
105 // O(1) running average via MovingAverage filter
106 float mean = mFluxAvg.update(mSpectralFlux);
108}
109
110bool Beat::detectBeat(u32 timestamp) {
111 // Use adaptive threshold with an absolute floor. The floor handles
112 // the silence-to-signal transition when adaptive threshold is near zero,
113 // and prevents CQ spectral leakage from triggering false beats.
114 static constexpr float MIN_FLUX_THRESHOLD = 50.0f;
115 float effectiveThreshold = fl::max(mAdaptiveThreshold, MIN_FLUX_THRESHOLD);
116
117 // Check if flux exceeds effective threshold
118 if (mSpectralFlux <= effectiveThreshold) {
119 return false;
120 }
121
122 // Check cooldown period
123 u32 timeSinceLastBeat = timestamp - mLastBeatTime;
124 if (timeSinceLastBeat < MIN_BEAT_INTERVAL_MS) {
125 return false;
126 }
127
128 // Calculate confidence based on how much we exceeded threshold
129 if (mAdaptiveThreshold > 0.0f) {
131 } else {
132 mConfidence = 1.0f;
133 }
134
135 return true;
136}
137
138void Beat::updateTempo(u32 timestamp) {
139 u32 interval = timestamp - mLastBeatTime;
140
141 // Only update tempo if interval is reasonable
142 if (interval >= MIN_BEAT_INTERVAL_MS && interval <= MAX_BEAT_INTERVAL_MS) {
143 // Smooth tempo changes
144 const float alpha = 0.2f;
145 mBeatInterval = static_cast<u32>(
146 alpha * static_cast<float>(interval) +
147 (1.0f - alpha) * static_cast<float>(mBeatInterval)
148 );
149
150 // Convert interval to BPM
151 float newBPM = 60000.0f / static_cast<float>(mBeatInterval);
152
153 // Check if tempo changed significantly
154 float bpmDiff = fl::abs(newBPM - mBPM);
155 mTempoChanged = (bpmDiff > 5.0f);
156
157 mBPM = newBPM;
158 }
159}
160
161void Beat::updatePhase(u32 timestamp) {
162 if (mBeatInterval == 0) {
163 mPhase = 0.0f;
164 return;
165 }
166
167 u32 timeSinceLastBeat = timestamp - mLastBeatTime;
168 mPhase = static_cast<float>(timeSinceLastBeat) / static_cast<float>(mBeatInterval);
169
170 // Wrap phase to [0, 1) using fmod for proper wrapping
171 // when beats are missed (phase exceeds 1.0)
172 if (mPhase >= 1.0f) {
173 mPhase = fl::fmodf(mPhase, 1.0f);
174 }
175}
176
178 if (mBeatDetected) {
179 onBeat();
181 }
182 if (mTempoChanged) {
184 mTempoChanged = false;
185 }
187}
188
189} // namespace detector
190} // namespace audio
191} // namespace fl
bool detectBeat(float energy)
Definition advanced.h:123
MovingAverage< float, 43 > mFluxAvg
Definition beat.h:62
function_list< void(float bpm, float confidence)> onTempoChange
Definition beat.h:29
function_list< void()> onBeat
Definition beat.h:26
bool detectBeat(u32 timestamp)
Definition beat.cpp.hpp:110
float calculateSpectralFlux(const fft::Bins &fft)
Definition beat.cpp.hpp:80
function_list< void(float phase)> onBeatPhase
Definition beat.h:27
~Beat() FL_NOEXCEPT override
vector< float > mPreviousMagnitudes
Definition beat.h:51
shared_ptr< const fft::Bins > mRetainedFFT
Definition beat.h:65
void update(shared_ptr< Context > context) override
Definition beat.cpp.hpp:28
function_list< void(float strength)> onOnset
Definition beat.h:28
void updateTempo(u32 timestamp)
Definition beat.cpp.hpp:138
static constexpr u32 MAX_BEAT_INTERVAL_MS
Definition beat.h:58
void fireCallbacks() override
Definition beat.cpp.hpp:177
void updatePhase(u32 timestamp)
Definition beat.cpp.hpp:161
void reset() override
Definition beat.cpp.hpp:67
static constexpr u32 MIN_BEAT_INTERVAL_MS
Definition beat.h:57
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NOEXCEPT
Definition math.h:71
constexpr common_type_t< T, U > max(T a, U b) FL_NOEXCEPT
Definition math.h:75
float fmodf(float x, float y) FL_NOEXCEPT
Definition math.h:336
void fill(Iterator first, Iterator last, const T &value) FL_NOEXCEPT
Definition algorithm.h:204
constexpr enable_if< is_fixed_point< T >::value, T >::type abs(T x) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT
static float DefaultMaxFrequency() FL_NOEXCEPT
Definition fft.h:128