FastLED 3.9.15
Loading...
Searching...
No Matches
mood_analyzer.cpp.hpp
Go to the documentation of this file.
1// MoodAnalyzer - Mood and emotion detection from audio features
2// Part of FastLED Audio Processing System (Phase 3 - Tier 3)
3
6#include "fl/math/math.h"
7#include "fl/stl/noexcept.h"
8
9namespace fl {
10namespace audio {
11namespace detector {
12
15 , mMinDuration(1500) // 1.5 seconds minimum
17 , mSpectralCentroid(0.0f)
18 , mSpectralRolloff(0.0f)
19 , mSpectralFlux(0.0f)
20 , mZeroCrossingRate(0.0f)
21 , mRMSEnergy(0.0f)
22 , mHistoryIndex(0)
23{
26}
27
29
31 mRetainedFFT = context->getFFT(32); // Higher resolution for mood analysis
32 const fft::Bins& fft = *mRetainedFFT;
33 const fft::Bins* prevFFT = context->getHistoricalFFT(1);
34
35 // Extract audio features
39 mZeroCrossingRate = context->getZCF();
40 mRMSEnergy = context->getRMS();
41
42 // Calculate mood dimensions
45
46 // Add to history for temporal averaging
47 if (static_cast<int>(mValenceHistory.size()) < mAveragingFrames) {
48 mValenceHistory.push_back(valence);
49 mArousalHistory.push_back(arousal);
50 mHistoryIndex = static_cast<int>(mValenceHistory.size()) % mAveragingFrames;
51 } else {
55 }
56
57 // Average over history for stability
58 float avgValence = 0.0f;
59 float avgArousal = 0.0f;
60 for (size_t i = 0; i < mValenceHistory.size(); i++) {
61 avgValence += mValenceHistory[i];
62 avgArousal += mArousalHistory[i];
63 }
64 avgValence /= mValenceHistory.size();
65 avgArousal /= mArousalHistory.size();
66
67 // Update current mood
69 mCurrentMood.valence = avgValence;
70 mCurrentMood.arousal = avgArousal;
71 mCurrentMood.confidence = calculateConfidence(avgValence, avgArousal);
72 mCurrentMood.timestamp = context->getTimestamp();
73
74 // Update duration if mood is stable
75 if (mPreviousMood.getCategory() == mCurrentMood.getCategory()) {
76 mCurrentMood.duration = mPreviousMood.duration +
77 (mCurrentMood.timestamp - mPreviousMood.timestamp);
78 } else {
79 mCurrentMood.duration = 0;
80 }
81
82 // Check for mood changes (store result for fireCallbacks)
84}
85
87 if (onMood) {
89 }
90
91 if (onValenceArousal) {
93 }
94
97 }
98}
99
101 mCurrentMood = Mood();
103 mSpectralCentroid = 0.0f;
104 mSpectralRolloff = 0.0f;
105 mSpectralFlux = 0.0f;
106 mZeroCrossingRate = 0.0f;
107 mRMSEnergy = 0.0f;
108 mValenceHistory.clear();
109 mArousalHistory.clear();
110 mHistoryIndex = 0;
111}
112
114 float weightedSum = 0.0f;
115 float magnitudeSum = 0.0f;
116
117 for (size_t i = 0; i < fft.raw().size(); i++) {
118 float magnitude = fft.raw()[i];
119 weightedSum += i * magnitude;
120 magnitudeSum += magnitude;
121 }
122
123 return (magnitudeSum < 1e-6f) ? 0.0f : weightedSum / magnitudeSum;
124}
125
127 float totalEnergy = 0.0f;
128
129 for (size_t i = 0; i < fft.raw().size(); i++) {
130 float magnitude = fft.raw()[i];
131 totalEnergy += magnitude * magnitude;
132 }
133
134 float energyThreshold = totalEnergy * threshold;
135 float cumulativeEnergy = 0.0f;
136
137 for (size_t i = 0; i < fft.raw().size(); i++) {
138 float magnitude = fft.raw()[i];
139 cumulativeEnergy += magnitude * magnitude;
140 if (cumulativeEnergy >= energyThreshold) {
141 return static_cast<float>(i) / fft.raw().size();
142 }
143 }
144
145 return 1.0f;
146}
147
149 if (!prevFFT || prevFFT->raw().size() != fft.raw().size()) {
150 return 0.0f;
151 }
152
153 float flux = 0.0f;
154 for (size_t i = 0; i < fft.raw().size(); i++) {
155 float diff = fft.raw()[i] - prevFFT->raw()[i];
156 flux += diff * diff;
157 }
158
159 return fl::sqrt(flux);
160}
161
162float MoodAnalyzer::calculateValence(float centroid, float rolloff, float flux) {
163 // Valence estimation based on spectral characteristics
164 // Higher frequencies and brighter timbre = more positive
165 // Lower frequencies and darker timbre = more negative
166
167 // Normalize centroid to 0-1 range (assuming 32 bins)
168 float normalizedCentroid = centroid / 32.0f;
169
170 // Brightness score (higher = more positive)
171 float brightness = normalizedCentroid * rolloff;
172
173 // Stability score (lower flux = more positive/calm, higher = more negative/chaotic)
174 float stability = 1.0f - fl::min(1.0f, flux / 10.0f);
175
176 // Combine into valence (-1 to 1)
177 float valence = (brightness * 0.6f + stability * 0.4f) * 2.0f - 1.0f;
178
179 // Clamp to valid range
180 return fl::max(-1.0f, fl::min(1.0f, valence));
181}
182
183float MoodAnalyzer::calculateArousal(float rms, float zcr, float flux) {
184 // Arousal estimation based on energy and dynamics
185 // Higher energy and more change = higher arousal
186 // Lower energy and less change = lower arousal
187
188 // Normalize RMS (assuming typical range 0-1)
189 float normalizedRMS = fl::min(1.0f, rms);
190
191 // Normalize ZCR (assuming typical range 0-1)
192 float normalizedZCR = fl::min(1.0f, zcr);
193
194 // Normalize flux (assuming typical range 0-10)
195 float normalizedFlux = fl::min(1.0f, flux / 10.0f);
196
197 // Combine into arousal (0 to 1)
198 float arousal = normalizedRMS * 0.5f + normalizedZCR * 0.2f + normalizedFlux * 0.3f;
199
200 // Clamp to valid range
201 return fl::max(0.0f, fl::min(1.0f, arousal));
202}
203
204float MoodAnalyzer::calculateConfidence(float valence, float arousal) {
205 // Confidence based on distance from neutral (center)
206 // Further from neutral = higher confidence
207
208 float distanceFromNeutral = fl::sqrt(valence * valence + arousal * arousal);
209
210 // For valence: range is -1 to 1, so max distance from 0 is 1
211 // For arousal: range is 0 to 1, so max distance from 0.5 is 0.5
212 // Combined max distance is approximately sqrt(1^2 + 0.5^2) = 1.118
213 float normalizedDistance = distanceFromNeutral / 1.118f;
214
215 // Clamp to valid range
216 return fl::max(0.0f, fl::min(1.0f, normalizedDistance));
217}
218
220 // Check if mood category has changed
221 if (mPreviousMood.getCategory() == newMood.getCategory()) {
222 return false;
223 }
224
225 // Check confidence threshold
226 if (newMood.confidence < mConfidenceThreshold) {
227 return false;
228 }
229
230 // Check minimum duration (unless first mood or previous was invalid)
231 if (mPreviousMood.isValid() && mPreviousMood.duration < mMinDuration) {
232 // Allow early change if new mood is significantly more confident
233 float confidenceRatio = newMood.confidence / (mPreviousMood.confidence + 0.01f);
234 if (confidenceRatio < 1.3f) {
235 return false;
236 }
237 }
238
239 return true;
240}
241
242} // namespace detector
243} // namespace audio
244} // namespace fl
fl::UISlider brightness("Brightness", BRIGHTNESS, 0, 255)
float rms(fl::span< const int16_t > data)
Definition simple.h:104
float calculateSpectralCentroid(const fft::Bins &fft)
fl::vector< float > mArousalHistory
function_list< void(float valence, float arousal)> onValenceArousal
void update(shared_ptr< Context > context) override
function_list< void(const Mood &mood)> onMoodChange
bool shouldChangeMood(const Mood &newMood)
fl::vector< float > mValenceHistory
shared_ptr< const fft::Bins > mRetainedFFT
float calculateSpectralFlux(const fft::Bins &fft, const fft::Bins *prevFFT)
float calculateSpectralRolloff(const fft::Bins &fft, float threshold=0.85f)
float calculateArousal(float rms, float zcr, float flux)
float calculateValence(float centroid, float rolloff, float flux)
~MoodAnalyzer() FL_NOEXCEPT override
function_list< void(const Mood &mood)> onMood
float calculateConfidence(float valence, float arousal)
fl::span< const float > raw() const FL_NOEXCEPT
Definition fft.cpp.hpp:63
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
constexpr enable_if< is_fixed_point< T >::value, T >::type sqrt(T x) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT
Category getCategory() const