FastLED 3.10.6
Loading...
Searching...
No Matches
room_sense.cpp
Go to the documentation of this file.
1// room_sense.cpp - derive RoomSense from the audio Processor.
2#include "room_sense.h"
3
4#include "fl/math/math.h"
5#include "fl/stl/chrono.h"
6#include "ring_canvas.h"
7
8namespace mood_ring {
9
10namespace {
11
12// Energy followers. Fast minus slow is the trend: positive while the room is
13// getting louder, negative while it is settling.
14constexpr float kEnergyFastMs = 120.0f;
15constexpr float kEnergySlowMs = 1800.0f;
16
17// Transient followers: instant up, short tail.
18constexpr float kTransientAttackMs = 5.0f;
19constexpr float kTransientReleaseMs = 160.0f;
20
21// Band followers: quick enough to feel live, slow enough not to strobe.
22constexpr float kBandAttackMs = 30.0f;
23constexpr float kBandReleaseMs = 250.0f;
24
25// Longest frame the followers are allowed to see. A stall (or a pause in
26// listening) must not turn into alpha == 1 and snap every follower to its
27// target; it should just play out as one slow frame.
28constexpr float kMaxFrameMs = 100.0f;
29
30// Mood moves slowly on purpose. A palette that flips every second reads as
31// noise, not feeling.
32constexpr float kMoodMs = 4000.0f;
33
34// Soft-knee clip into [0, 1]. Meets the two branches at exactly 0.75 when
35// x == 1 so a band pinned at full scale never reads dimmer than one at 0.99.
36float softKnee(float x) {
37 if (x <= 0.0f)
38 return 0.0f;
39 if (x >= 1.0f)
40 return 1.0f - 0.25f / (1.0f + (x - 1.0f) * 4.0f);
41 return x * (1.0f - 0.25f * x);
42}
43
44bool edge(fl::u32 stamp, fl::u32 &seen) {
45 if (stamp != 0 && stamp != seen) {
46 seen = stamp;
47 return true;
48 }
49 return false;
50}
51
52} // namespace
53
54float Follower::update(float target, float dtMs, float attackMs,
55 float releaseMs) {
56 if (dtMs <= 0.0f)
57 return mValue;
58 const float tau = (target > mValue) ? attackMs : releaseMs;
59 if (tau <= 0.0f) {
60 mValue = target;
61 return mValue;
62 }
63 // One-pole with alpha = dt / (tau + dt): no expf, never overshoots.
64 float alpha = dtMs / (tau + dtMs);
65 if (alpha > 1.0f)
66 alpha = 1.0f;
67 mValue += (target - mValue) * alpha;
68 return mValue;
69}
70
72 : mProcessor(processor) {
73 mPresence.reset(0.0f);
74 mWarmth.reset(0.5f);
75}
76
78 if (!mProcessor)
79 return;
80 // The Processor keeps one callback per event, so the last begin() wins.
81 // Callbacks only stamp a time; update() turns stamps into edges.
82 RoomListener *self = this;
83 mProcessor->onBeat([self]() { self->mLastBeatMs = fl::millis(); });
84 mProcessor->onDownbeat([self]() { self->mLastDownbeatMs = fl::millis(); });
85 mProcessor->onKick([self]() { self->mLastKickMs = fl::millis(); });
86 mProcessor->onSnare([self]() { self->mLastSnareMs = fl::millis(); });
87 mProcessor->onHiHat([self]() { self->mLastHiHatMs = fl::millis(); });
88}
89
90float RoomListener::stepClock(fl::u32 nowMs) {
91 float dtMs = (mLastMs == 0) ? 16.0f : static_cast<float>(nowMs - mLastMs);
92 mLastMs = nowMs;
93 return clampf(dtMs, 0.0f, kMaxFrameMs);
94}
95
96void RoomListener::update(fl::u32 nowMs) {
97 const float dtMs = stepClock(nowMs);
98
99 if (!mProcessor) {
100 mSense = RoomSense{};
101 return;
102 }
103
104 deriveEnergy(dtMs);
105 deriveBlend(dtMs);
106 deriveRhythm(nowMs);
107 deriveMood(dtMs);
108 deriveEvents();
109}
110
113
114 mSense.low = mLow.update(softKnee(p.getEqBass()), dtMs, kBandAttackMs,
115 kBandReleaseMs);
116 mSense.mid = mMid.update(softKnee(p.getEqMid()), dtMs, kBandAttackMs,
117 kBandReleaseMs);
118 mSense.high = mHigh.update(softKnee(p.getEqTreble()), dtMs, kBandAttackMs,
119 kBandReleaseMs);
120
121 const float vol = softKnee(p.getEqVolume());
122 const float fast =
123 mEnergyFast.update(vol, dtMs, kEnergyFastMs, kEnergyFastMs);
124 const float slow =
125 mEnergySlow.update(vol, dtMs, kEnergySlowMs, kEnergySlowMs);
126 mSense.energy = fast;
127 // A 0.25 swing between fast and slow is a full-scale trend.
128 mSense.trend = clampf((fast - slow) * 4.0f, -1.0f, 1.0f);
129
130 // Punch is relative-minus-attenuated: during a sustained loud passage
131 // the slow level catches the fast one and punch falls to zero, so only
132 // a transient reads as a hit.
133 const float bassHit =
134 fl::max(0.0f, p.getVibeBass() - p.getVibeBassAtt()) * tuning.punchGain;
135 const float trebHit = fl::max(0.0f, p.getVibeTreb() - p.getVibeTrebAtt());
136 mSense.punch = clampf(
137 mPunch.update(bassHit, dtMs, kTransientAttackMs, kTransientReleaseMs),
138 0.0f, 1.0f);
139 mSense.shimmer = clampf(
140 mShimmer.update(trebHit, dtMs, kTransientAttackMs, kTransientReleaseMs),
141 0.0f, 1.0f);
142}
143
146 // Groove confidence: tempo AND beat have to agree.
147 const float conf = p.getTempoConfidence() * p.getBeatConfidence();
148 blendFrom(p.isSilent() ? 0.0f : 1.0f, conf, dtMs);
149}
150
151void RoomListener::blendFrom(float presenceTarget, float grooveConf,
152 float dtMs) {
153 // Presence: fast attack so one clap wakes the ring, slow release so a
154 // breath between phrases does not drop it into calm.
155 mSense.presence =
156 mPresence.update(presenceTarget, dtMs, tuning.presenceAttackMs,
157 tuning.presenceReleaseMs);
158
159 // Slow both ways: a beat has to prove itself, and one dropped bar
160 // should not lose it.
161 mSense.grooveConfidence = mGroove.update(
162 grooveConf, dtMs, tuning.grooveAttackMs, tuning.grooveReleaseMs);
163
164 // Blend. smoothstep between exit and enter is the whole "hysteresis".
165 const float g = smoothstepf(tuning.grooveExit, tuning.grooveEnter,
166 mSense.grooveConfidence);
167 mSense.calm = 1.0f - mSense.presence;
168 mSense.groove = mSense.presence * g;
169 mSense.flow = mSense.presence * (1.0f - g);
170}
171
172void RoomListener::deriveRhythm(fl::u32 nowMs) {
174
175 mSense.bpm = p.getTempoBPM();
176 mSense.measurePhase = clampf(p.getMeasurePhase(), 0.0f, 1.0f);
177
178 // Beat phase from our own beat stamps and the reported tempo. Wraps so
179 // a missed beat callback still leaves the phase running at tempo.
180 if (mSense.bpm > 20.0f && mLastBeatMs != 0) {
181 const float beatMs = 60000.0f / mSense.bpm;
182 const float since = static_cast<float>(nowMs - mLastBeatMs);
183 mSense.beatPhase = wrapTurns(since / beatMs);
184 } else {
185 mSense.beatPhase = 0.0f;
186 }
187}
188
189void RoomListener::deriveMood(float dtMs) {
191 // Valence is [-1, 1]; warmth is [0, 1] with 0.5 neutral.
192 const float warmthTarget =
193 clampf(0.5f + 0.5f * p.getMoodValence(), 0.0f, 1.0f);
194 const float arousalTarget = clampf(p.getMoodArousal(), 0.0f, 1.0f);
195 mSense.warmth = mWarmth.update(warmthTarget, dtMs, kMoodMs, kMoodMs);
196 mSense.arousal = mArousal.update(arousalTarget, dtMs, kMoodMs, kMoodMs);
197}
198
200 RoomEvents &e = mSense.events;
201 // Edges are consumed regardless of regime so a beat that landed while
202 // the ring was calm does not fire the moment groove takes over.
203 e.beat = edge(mLastBeatMs, mSeenBeatMs);
205 e.kick = edge(mLastKickMs, mSeenKickMs);
208}
209
210void RoomListener::rest(fl::u32 nowMs) {
211 const float dtMs = stepClock(nowMs);
212
213 // Everything audio-driven eases to zero at its own release rate, so
214 // switching listening off is a fade, not a cut. Mood is left where it
215 // was: the palette has no reason to move.
216 mSense.low = mLow.update(0.0f, dtMs, kBandAttackMs, kBandReleaseMs);
217 mSense.mid = mMid.update(0.0f, dtMs, kBandAttackMs, kBandReleaseMs);
218 mSense.high = mHigh.update(0.0f, dtMs, kBandAttackMs, kBandReleaseMs);
219 const float fast =
220 mEnergyFast.update(0.0f, dtMs, kEnergyFastMs, kEnergyFastMs);
221 const float slow =
222 mEnergySlow.update(0.0f, dtMs, kEnergySlowMs, kEnergySlowMs);
223 mSense.energy = fast;
224 mSense.trend = clampf((fast - slow) * 4.0f, -1.0f, 1.0f);
225 mSense.punch =
226 mPunch.update(0.0f, dtMs, kTransientAttackMs, kTransientReleaseMs);
227 mSense.shimmer =
228 mShimmer.update(0.0f, dtMs, kTransientAttackMs, kTransientReleaseMs);
229 mSense.beatPhase = 0.0f;
230
231 blendFrom(0.0f, 0.0f, dtMs);
232
233 // Consume edges that landed while resting so they cannot fire on resume,
234 // and publish none.
235 deriveEvents();
236 mSense.events = RoomEvents{};
237}
238
239const char *RoomListener::regimeName() const {
240 if (mSense.calm >= mSense.flow && mSense.calm >= mSense.groove)
241 return "calm";
242 if (mSense.groove >= mSense.flow)
243 return "groove";
244 return "flow";
245}
246
247} // namespace mood_ring
int x
Definition simple.h:92
FastLED chrono implementation - duration types for time measurements.
float getEqBass() FL_NO_EXCEPT
float getMoodArousal() FL_NO_EXCEPT
float getVibeBass() FL_NO_EXCEPT
float getTempoBPM() FL_NO_EXCEPT
float getVibeTreb() FL_NO_EXCEPT
float getMeasurePhase() FL_NO_EXCEPT
float getEqVolume() FL_NO_EXCEPT
float getEqMid() FL_NO_EXCEPT
float getTempoConfidence() FL_NO_EXCEPT
float getEqTreble() FL_NO_EXCEPT
float getVibeBassAtt() FL_NO_EXCEPT
float getVibeTrebAtt() FL_NO_EXCEPT
bool isSilent() FL_NO_EXCEPT
float getBeatConfidence() FL_NO_EXCEPT
float getMoodValence() FL_NO_EXCEPT
float update(float target, float dtMs, float attackMs, float releaseMs)
void update(fl::u32 nowMs)
Poll the Processor and refresh sense(). Call once per frame.
void deriveMood(float dtMs)
void deriveRhythm(fl::u32 nowMs)
fl::shared_ptr< fl::audio::Processor > mProcessor
Definition room_sense.h:114
RoomListener(fl::shared_ptr< fl::audio::Processor > processor)
void rest(fl::u32 nowMs)
Not listening this frame.
const char * regimeName() const
Which regime currently dominates, for logs only.
void deriveBlend(float dtMs)
ListenerTuning tuning
Definition room_sense.h:102
void blendFrom(float presenceTarget, float grooveConf, float dtMs)
void begin()
Install the event callbacks. Call once after the Processor exists.
float stepClock(fl::u32 nowMs)
Advance the frame clock; returns a bounded dt in ms.
void deriveEnergy(float dtMs)
constexpr common_type_t< T, U > max(T a, U b) FL_NO_EXCEPT
Definition math.h:75
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
bool edge(fl::u32 stamp, fl::u32 &seen)
float smoothstepf(float e0, float e1, float x)
0 below e0, 1 above e1, smooth in between.
float clampf(float v, float lo, float hi)
Definition ring_canvas.h:19
float wrapTurns(float t)
Wrap into [0, 1).
Everything a visual is allowed to react to.
Definition room_sense.h:31
One-frame rising edges. Set on the frame the event lands, cleared next.
Definition room_sense.h:21