FastLED 3.10.6
Loading...
Searching...
No Matches
layers.cpp
Go to the documentation of this file.
1// layers.cpp - Breath, Flow, Groove.
2#include "layers.h"
3
4#include "fl/math/math.h"
5
6namespace mood_ring {
7
8namespace {
9
10constexpr float kTwoPi = 6.2831853f;
11
12// ---- Breath ----
13constexpr float kBreathHz = 0.10f; // one breath every ten seconds
14constexpr float kBreathDriftTurnsPerSec = 0.012f;
15constexpr float kBreathSigma = 0.18f; // wide, soft blobs
16constexpr float kBreathFloor = 0.12f; // never fully dark
17
18// ---- Flow ----
19constexpr float kFlowBaseTurnsPerSec = 0.05f;
20constexpr float kFlowMidTurnsPerSec = 0.35f;
21constexpr float kFlowSigmaMin = 0.04f;
22constexpr float kFlowSigmaMax = 0.16f;
23constexpr float kFlowHueWalkPerSec = 0.06f;
24constexpr float kFlowBodyAttackMs = 20.0f;
25constexpr float kFlowBodyReleaseMs = 90.0f;
26constexpr float kFlowSigmaSlewMs = 120.0f;
27constexpr float kGrainsPerSecAtFull = 60.0f; // per 100 LEDs at full shimmer
28constexpr float kGrainAttackSec = 0.04f;
29constexpr float kGrainLifeSec = 0.30f;
30
31// ---- Groove ----
32constexpr float kMarkerSigma = 0.03f;
33constexpr float kSwellStart = 0.75f; // beat phase where the swell begins
34constexpr float kSwellAttackMs = 25.0f;
35constexpr float kSwellReleaseMs = 140.0f;
36constexpr float kFlashAttackMs = 15.0f;
37constexpr float kFlashReleaseMs = 220.0f;
38constexpr float kSnareAttackMs = 10.0f;
39constexpr float kSnareReleaseMs = 110.0f;
40constexpr float kCorrectionBeats = 0.4f; // finish a phase correction in this
41 // fraction of a beat
42
43float advance(float pos, float turnsPerSec, float dtSec) {
44 return wrapTurns(pos + turnsPerSec * dtSec);
45}
46
48float grainEnvelope(float ageSec) {
49 if (ageSec < kGrainAttackSec) {
50 return smoothstepf(0.0f, kGrainAttackSec, ageSec);
51 }
52 return 1.0f - smoothstepf(kGrainAttackSec, kGrainLifeSec, ageSec);
53}
54
55} // namespace
56
57// ---------------------------------------------------------------------------
58
59void BreathLayer::render(const RoomSense &s, const MoodPalette &pal,
60 float dtSec, RingCanvas &c) {
61 (void)s;
62 mBreath += kTwoPi * kBreathHz * dtSec;
63 while (mBreath > kTwoPi)
64 mBreath -= kTwoPi;
65 mPosA = advance(mPosA, kBreathDriftTurnsPerSec, dtSec);
66 mPosB = advance(mPosB, -kBreathDriftTurnsPerSec * 0.7f, dtSec);
67
68 // Whole-ring breath between floor and a restrained ceiling.
69 const float breath =
70 kBreathFloor + 0.25f * (0.5f + 0.5f * fl::sin(mBreath));
71 c.fill(paletteColor(pal, 0.0f), breath);
72
73 // Two blobs on opposite ends of the family, each breathing out of phase.
74 const float aGain = 0.35f + 0.25f * (0.5f + 0.5f * fl::sin(mBreath + 1.2f));
75 const float bGain = 0.35f + 0.25f * (0.5f + 0.5f * fl::sin(mBreath + 3.6f));
76 c.lobe(mPosA, kBreathSigma, paletteColor(pal, 0.35f), aGain);
77 c.lobe(mPosB, kBreathSigma, paletteColor(pal, 1.0f), bGain);
78}
79
80// ---------------------------------------------------------------------------
81
82void FlowLayer::spawnGrains(const RoomSense &s, float dtSec, int ringSize) {
83 // Spawn rate is per second and scales with ring length, so the density
84 // of sparkle is the same on a 60-LED ring and a 244-LED one.
85 const float rate = kGrainsPerSecAtFull *
86 (static_cast<float>(ringSize) / 100.0f) * s.shimmer *
87 (0.5f + 0.5f * s.high);
88 mSpawnBudget += rate * dtSec;
89 while (mSpawnBudget >= 1.0f) {
90 mSpawnBudget -= 1.0f;
91 for (int g = 0; g < kMaxGrains; ++g) {
92 if (mGrains[g].active)
93 continue;
94 mGrains[g].active = true;
95 mGrains[g].ageSec = 0.0f;
96 mGrains[g].index =
97 static_cast<int>(mRng.unit() * static_cast<float>(ringSize));
98 break;
99 }
100 }
101 if (mSpawnBudget > 4.0f)
102 mSpawnBudget = 4.0f; // no burst after a stall
103}
104
105void FlowLayer::drawGrains(const MoodPalette &pal, float dtSec, RingCanvas &c) {
106 const RGBf tint = paletteColor(pal, 1.0f);
107 for (int g = 0; g < kMaxGrains; ++g) {
108 Grain &grain = mGrains[g];
109 if (!grain.active)
110 continue;
111 grain.ageSec += dtSec;
112 if (grain.ageSec >= kGrainLifeSec) {
113 grain.active = false;
114 continue;
115 }
116 const float e = grainEnvelope(grain.ageSec);
117 // A bright core with dimmer shoulders so a grain is a glint, not a
118 // single hot pixel.
119 c.pixel(grain.index, rgbfWhite(), 0.45f * e);
120 c.pixel(grain.index, tint, 0.35f * e);
121 c.pixel(grain.index - 1, tint, 0.20f * e);
122 c.pixel(grain.index + 1, tint, 0.20f * e);
123 }
124}
125
126void FlowLayer::render(const RoomSense &s, const MoodPalette &pal, float dtSec,
127 RingCanvas &c) {
128 const float dtMs = dtSec * 1000.0f;
129
130 // Mids set travel speed; a rising room speeds up, a settling one slows.
131 const float speed = kFlowBaseTurnsPerSec +
132 kFlowMidTurnsPerSec * s.mid * (1.0f + 0.5f * s.trend);
133 mPosA = advance(mPosA, speed, dtSec);
134 mPosB = advance(mPosB, -speed * 0.8f, dtSec);
135 mHueWalk = wrapTurns(mHueWalk + kFlowHueWalkPerSec * s.mid * dtSec);
136
137 // Bass sets lobe size; trend widens on the way up and tightens on the
138 // way down so a build feels like it is opening. Slewed so a bass drop
139 // grows the lobe instead of snapping it.
140 float sigmaTarget = kFlowSigmaMin + (kFlowSigmaMax - kFlowSigmaMin) * s.low;
141 sigmaTarget *= 1.0f + 0.35f * s.trend;
142 sigmaTarget =
143 clampf(sigmaTarget, kFlowSigmaMin * 0.7f, kFlowSigmaMax * 1.4f);
144 const float sigma =
145 mSigma.update(sigmaTarget, dtMs, kFlowSigmaSlewMs, kFlowSigmaSlewMs);
146
147 // Punch adds a brief brightness kick so a hit is visible without a beat.
148 const float bodyTarget = 0.45f + 0.45f * s.energy + 0.4f * s.punch;
149 const float body =
150 mBody.update(bodyTarget, dtMs, kFlowBodyAttackMs, kFlowBodyReleaseMs);
151
152 c.lobe(mPosA, sigma, paletteColor(pal, mHueWalk), body);
153 c.lobe(mPosB, sigma * 0.8f, paletteColor(pal, wrapTurns(mHueWalk + 0.5f)),
154 body * 0.8f);
155
156 // A dim bed so the ring is never black between lobes.
157 c.fill(paletteColor(pal, 0.5f), 0.06f + 0.10f * s.energy);
158
159 spawnGrains(s, dtSec, c.size());
160 drawGrains(pal, dtSec, c);
161}
162
163// ---------------------------------------------------------------------------
164
165void GrooveLayer::render(const RoomSense &s, const MoodPalette &pal,
166 float dtSec, RingCanvas &c) {
167 const float dtMs = dtSec * 1000.0f;
168 const float slot = 1.0f / static_cast<float>(kMarkers);
169 const float beatsPerSec = (s.bpm > 20.0f) ? s.bpm / 60.0f : 0.0f;
170
171 // Rotate continuously at tempo: one slot per beat. On each beat, measure
172 // how far the nearest marker is from its slot boundary and slew that
173 // error out over the next fraction of a beat, so the geometry locks to
174 // the music without ever jumping.
175 mRotation = advance(mRotation, slot * beatsPerSec, dtSec);
176 if (s.events.beat) {
177 const float inSlot =
178 mRotation -
179 slot * static_cast<float>(static_cast<int>(mRotation / slot));
180 // Nearest boundary: either back to the start of this slot or forward
181 // to the next one.
182 const float err = (inSlot < slot * 0.5f) ? -inSlot : (slot - inSlot);
183 mCorrection = err;
184 }
185 if (mCorrection != 0.0f && beatsPerSec > 0.0f) {
186 const float maxStep = slot * beatsPerSec * dtSec / kCorrectionBeats;
187 float step = mCorrection;
188 if (step > maxStep)
189 step = maxStep;
190 if (step < -maxStep)
191 step = -maxStep;
192 mRotation = wrapTurns(mRotation + step);
193 mCorrection -= step;
195 mCorrection = 0.0f;
196 }
197
198 // Swell into the beat: brightness climbs over the last quarter of the
199 // beat and releases right after it. Followed so a beat that lands early
200 // (phase reset from 0.9 to 0) ramps over a frame instead of stepping.
201 float swellTarget;
202 if (s.beatPhase >= kSwellStart) {
203 swellTarget = smoothstepf(kSwellStart, 1.0f, s.beatPhase);
204 } else {
205 swellTarget = 1.0f - smoothstepf(0.0f, 0.25f, s.beatPhase);
206 }
207 const float swell =
208 mSwell.update(swellTarget, dtMs, kSwellAttackMs, kSwellReleaseMs);
209
210 // Event envelopes: instant-feeling attack, smooth release.
211 const float flash = mFlash.update(s.events.downbeat ? 1.0f : 0.0f, dtMs,
212 kFlashAttackMs, kFlashReleaseMs);
213 const float snare = mSnare.update(s.events.snare ? 1.0f : 0.0f, dtMs,
214 kSnareAttackMs, kSnareReleaseMs);
215
216 const float markerGain = 0.45f + 0.55f * swell + 0.5f * s.punch;
217
218 // Bed: measure phase breathes the whole ring once per bar.
219 const float bar = 0.5f + 0.5f * fl::sin(kTwoPi * s.measurePhase);
220 c.fill(paletteColor(pal, 0.2f), 0.08f + 0.12f * bar);
221
222 // Markers alternate ends of the palette so the geometry reads.
223 for (int k = 0; k < kMarkers; ++k) {
224 const float pos = wrapTurns(mRotation + slot * static_cast<float>(k));
225 const float t = (k & 1) ? 1.0f : 0.4f;
226 c.lobe(pos, kMarkerSigma + 0.02f * s.low, paletteColor(pal, t),
227 markerGain);
228 }
229
230 // Snare: a thin counter-marker halfway between each pair.
231 if (snare > 0.001f) {
232 for (int k = 0; k < kMarkers; ++k) {
233 const float pos =
234 wrapTurns(mRotation + slot * (static_cast<float>(k) + 0.5f));
235 c.lobe(pos, kMarkerSigma * 0.6f, rgbfWhite(), 0.6f * snare);
236 }
237 }
238
239 // Downbeat: whole-ring flash in the warm end of the family.
240 if (flash > 0.001f) {
241 c.fill(paletteColor(pal, 1.0f), 0.5f * flash);
242 }
243}
244
245} // namespace mood_ring
uint8_t pos
Definition Blur.ino:11
uint16_t speed
Definition Noise.ino:72
float mPosA
turns
Definition layers.h:31
void render(const RoomSense &s, const MoodPalette &pal, float dtSec, RingCanvas &c)
Definition layers.cpp:59
float mBreath
radians
Definition layers.h:30
Follower mSigma
Definition layers.h:60
void drawGrains(const MoodPalette &pal, float dtSec, RingCanvas &c)
Definition layers.cpp:105
Grain mGrains[kMaxGrains]
Definition layers.h:61
void render(const RoomSense &s, const MoodPalette &pal, float dtSec, RingCanvas &c)
Definition layers.cpp:126
static const int kMaxGrains
Definition layers.h:50
void spawnGrains(const RoomSense &s, float dtSec, int ringSize)
Definition layers.cpp:82
float mCorrection
remaining phase correction, turns
Definition layers.h:78
void render(const RoomSense &s, const MoodPalette &pal, float dtSec, RingCanvas &c)
Definition layers.cpp:165
float mRotation
turns, advanced continuously
Definition layers.h:77
static const int kMarkers
Definition layers.h:75
void pixel(int i, const RGBf &color, float gain)
Add to one pixel, index wrapped.
void lobe(float pos, float sigma, const RGBf &color, float gain)
Add a smooth lobe centred at pos (turns) with half-width sigma (turns).
void fill(const RGBf &color, float gain)
Add the same colour to every pixel.
Additive drawing helpers over a span of float pixels.
Definition ring_canvas.h:66
static uint32_t t
Definition Luminova.h:55
enable_if< is_fixed_point< T >::value, T >::type sin(T angle) FL_NO_EXCEPT
float advance(float pos, float turnsPerSec, float dtSec)
Definition layers.cpp:43
float grainEnvelope(float ageSec)
Rise over attack, hold, fall to zero at life. Smooth at both ends.
Definition layers.cpp:48
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
RGBf rgbfWhite()
Definition ring_canvas.h:36
RGBf paletteColor(const MoodPalette &p, float t)
Colour at position t in [0, 1] along the family, at value v.
float wrapTurns(float t)
Wrap into [0, 1).
float shimmer
treble transient
Definition room_sense.h:42
float punch
bass transient (a hit, not loudness)
Definition room_sense.h:41
float energy
smoothed loudness
Definition room_sense.h:39
float measurePhase
0 on the downbeat
Definition room_sense.h:50
float trend
[-1, 1] falling .. rising
Definition room_sense.h:40
float beatPhase
0 on the beat, ramps to 1 before the next
Definition room_sense.h:49
Everything a visual is allowed to react to.
Definition room_sense.h:31
Linear-light float pixel, nominal range [0, 1], may exceed 1 before clip.
Definition ring_canvas.h:24