FastLED 3.10.6
Loading...
Searching...
No Matches
hydropack.ino
// FL_AGENT_ALLOW_NEW_EXAMPLE
// @filter: (mem is large) and (platform is esp32*)
// This is deliberately an LED prototype, not an EL renderer. It draws two
// triangular clusters and a center bar using ordinary addressable LEDs. Two
// independent threshold analyzers consume the same self-normalized bass input:
// - center: sensitive - fires on a transient just above the song average
// - triangles: loud - fire only on a substantially stronger transient
//
// Vibe's normalized bass-rise confidence arms music mode and drives the
// actual real-time response. It is codec-level independent, unlike a fixed
// raw spectral-flux threshold. Music mode warms up over two seconds of
// recurring confident beats, with no fixed BPM or tempo-consistency lock.
// Once music is established, FastLED's Vibe detector tracks its running bass
// average; relative bass energy is about 1.0 at the current average. A loud
// hit lights the center, then launches into the triangles.
#include <Arduino.h>
#include <FastLED.h>
#include "fl/math/math.h"
#include "fl/ui/ui.h"
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
// INMP441 wiring (matches examples/AudioInput):
// SCK (BCLK) -> GPIO 4, WS (LRCLK) -> GPIO 7, SD (DATA) -> GPIO 8,
// L/R -> 3.3V (right channel), VDD -> 3.3V, GND -> GND
#define I2S_WS 7
#define I2S_SD 8
#define I2S_CLK 4
#define PIN_PREVIEW 3
constexpr uint8_t kTriangleLedCount = 6;
constexpr uint8_t kCenterStart = 6;
constexpr uint8_t kHydroPackLedCount = 18;
constexpr float kDefaultStageThresholdDbSpl = 72.0f;
constexpr uint32_t kMusicWarmupMs = 2000;
constexpr uint32_t kMusicHoldMs = 2000;
constexpr float kMusicModeFadeSeconds = 0.5f;
constexpr float kMaximumMusicZcf = 0.40f;
constexpr float kDefaultMinimumBeatConfidence = 0.05f;
// Locked HydroPack response: the center detects ordinary bass hits; the
// triangles require a substantially louder hit. Two cascaded envelopes keep
// the attack sharp while giving the center a slightly longer tail.
constexpr float kCenterBassThreshold = 1.05f;
constexpr float kTriangleBassThreshold = 1.65f;
constexpr float kCenterEnvelopeAttackSeconds = 0.015f;
constexpr float kCenterEnvelopeFadeSeconds = 0.060f;
constexpr float kTriangleEnvelopeAttackSeconds = 0.015f;
constexpr float kTriangleEnvelopeFadeSeconds = 0.040f;
// Six LEDs form each triangle and six form the center bar. The layout reads
// < | > in the WASM screenmap renderer without EL panel/wire primitives.
// Left triangle (<)
{12.0f, 50.0f}, {22.0f, 42.0f}, {22.0f, 58.0f},
{32.0f, 34.0f}, {32.0f, 50.0f}, {32.0f, 66.0f},
// Center bar (|)
{50.0f, 30.0f}, {50.0f, 38.0f}, {50.0f, 46.0f},
{50.0f, 54.0f}, {50.0f, 62.0f}, {50.0f, 70.0f},
// Right triangle (>)
{88.0f, 50.0f}, {78.0f, 42.0f}, {78.0f, 58.0f},
{68.0f, 34.0f}, {68.0f, 50.0f}, {68.0f, 66.0f},
};
"HydroPack screenmap must contain every preview LED");
}
fl::UITitle title("HydroPack Stage Music Launch");
"The INMP441 sound-pressure gate warms up for two seconds of confident "
"beats before arming music mode. Conversation and one-off noise stay "
"dark; musical bass "
"fires the center, with stronger hits launching to both triangles.");
fl::UISlider stageThresholdDbSpl("Minimum Music Level (dB SPL)",
1.0f);
fl::UISlider minimumBeatConfidence("Minimum Vibe Beat Confidence",
0.05f);
#if defined(FL_IS_WASM)
fl::UICheckbox streamDetectorFrames("Stream Detector Frames", true);
#endif
// The two analyzers have independent envelopes but share one bass input.
// The loud analyzer is released 5 ms after the center, preserving an outward
// launch without adding a perceptible artificial delay.
float gCenterLevel = 0.0f;
float gTriangleLevel = 0.0f;
float gCenterEnvelopeStage = 0.0f;
float gCenterTrigger = 0.0f;
float gPendingTriangleLevel = 0.0f;
float gTriangleTrigger = 0.0f;
float gSoundPressureDbSpl = 33.0f;
float gBeatConfidence = 0.0f;
float gMusicModeLevel = 0.0f;
if (!gAudio) {
return;
}
const fl::audio::Sample &sample = gAudio->getSample();
if (!sample.isValid() || sample.pcm().empty()) {
return;
}
gSoundLevelMeter.processBlock(sample.pcm());
gSoundPressureDbSpl = static_cast<float>(gSoundLevelMeter.getSPL());
}
const fl::audio::Sample &sample = gAudio->getSample();
if (!sample.isValid() || sample.zcf() >= kMaximumMusicZcf) {
return false;
}
const uint32_t now = millis();
if (static_cast<int32_t>(now - gMusicActiveUntilMs) < 0) {
// Each qualifying bass beat extends the short music-present hold.
return true;
}
}
return false;
}
return true;
}
return static_cast<int32_t>(millis() - gMusicActiveUntilMs) < 0;
}
const float bassAverage = gAudio->getVibeBassAtt();
if (!levels.bassSpike || bassAverage <= 0.0f) {
return 0.0f;
}
return fl::clamp((levels.bass - bassAverage) / bassAverage, 0.0f, 1.0f);
}
void fireAnalyzer(float bass, float threshold, float minIntensity,
float &trigger) {
if (bass <= threshold) {
return;
}
const float intensity = fl::clamp((bass - threshold) / threshold,
minIntensity, 1.0f);
trigger = fl::max(trigger, intensity);
}
float applyAttackDecay(float value, float target, float attackSeconds,
float decaySeconds, float deltaSeconds) {
const float seconds = target > value ? attackSeconds : decaySeconds;
if (seconds <= 0.0f) {
return target;
}
const float amount = fl::clamp(deltaSeconds / seconds, 0.0f, 1.0f);
return value + (target - value) * amount;
}
void advanceDoubleEnvelope(float &trigger, float &stage, float &level,
float attackSeconds, float decaySeconds,
float deltaSeconds) {
// Two cascaded attack/decay filters soften the event into a natural
// envelope. Both stages share an intentionally short attack; their decay
// is independently adjustable for the center and triangle elements.
stage = applyAttackDecay(stage, trigger, attackSeconds, decaySeconds,
deltaSeconds);
level = applyAttackDecay(level, stage, attackSeconds, decaySeconds,
deltaSeconds);
trigger = 0.0f;
}
float advanceLinearFade(float value, float target, float durationSeconds,
float deltaSeconds) {
if (durationSeconds <= 0.0f) {
return target;
}
const float step = deltaSeconds / durationSeconds;
if (target > value) {
return fl::min(target, value + step);
}
return fl::max(target, value - step);
}
uint8_t toByte(float value) {
return static_cast<uint8_t>(fl::clamp(value, 0.0f, 255.0f));
}
void renderPreview() {
// Quiet is fully dark. The sensitive center appears first; a loud hit
// subsequently drives the two triangle clusters outward from it.
const uint8_t centerBrightness =
const uint8_t triangleBrightness =
const CRGB centerColor(0, centerBrightness / 3, centerBrightness);
const CRGB triangleColor(0, triangleBrightness / 3, triangleBrightness);
for (uint8_t i = 0; i < kTriangleLedCount; ++i) {
previewLeds[i] = triangleColor;
previewLeds[kRightTriangleStart + i] = triangleColor;
}
for (uint8_t i = 0; i < kTriangleLedCount; ++i) {
previewLeds[kCenterStart + i] = centerColor;
}
}
void setup() {
Serial.begin(115200);
.setScreenMap(previewMap);
FastLED.setBrightness(255);
stageThresholdDbSpl.setGroup("Stage Music Gate");
minimumBeatConfidence.setGroup("Stage Music Gate");
#if defined(FL_IS_WASM)
streamDetectorFrames.setGroup("Debug");
#endif
// The unified input is browser audio on WASM and INMP441 I2S on ESP32.
if (!gAudio) {
FL_WARN("HydroPack: no audio processor available");
return;
}
gAudio->setNoiseFloorTrackingEnabled(true);
gAudio->onVibeLevels([](const fl::audio::detector::VibeLevels &levels) {
// Feed every block so the calibrated meter observes the actual quiet
// floor, rather than only the relatively loud blocks that trigger a
// beat callback.
#if defined(FL_IS_WASM)
// Keep this as one compact line per analysis frame. It deliberately
// polls both independent rhythm detectors: raw Beat is an onset
// detector; Tempo is the recurring-rhythm estimator. Calling these
// getters also enables their lazy detector instances for the next
// audio frame, so the trace diagnoses the real WASM signal path.
if (streamDetectorFrames.value()) {
const fl::audio::Sample &sample = gAudio->getSample();
"HYDRO frame=%lu spl=%.1f zcf=%.3f silent=%u "
"beat=%.3f bpm=%.1f tempo=%.3f tempo_bpm=%.1f "
"vibe_bass=%.3f vibe_att=%.3f vibe_conf=%.3f spike=%u "
"bass_raw=%.3f gate=%u\\n",
static_cast<unsigned long>(millis()), gSoundPressureDbSpl,
sample.zcf(), gAudio->isSilent() ? 1U : 0U,
gAudio->getBeatConfidence(), gAudio->getBPM(),
gAudio->getTempoConfidence(), gAudio->getTempoBPM(),
levels.bass, gAudio->getVibeBassAtt(), gBeatConfidence,
levels.bassSpike ? 1U : 0U, levels.bassRaw,
isMusicModeActive() ? 1U : 0U);
}
#endif
}
if (!levels.bassSpike ||
return;
}
if (levels.bass > kTriangleBassThreshold) {
}
});
}
void loop() {
static uint32_t lastMs = 0;
static bool firstFrame = true;
const uint32_t now = millis();
const uint32_t deltaMs = firstFrame ? 0 : (now - lastMs);
firstFrame = false;
lastMs = now;
if (gPendingTriangleLevel > 0.0f &&
static_cast<int32_t>(now - gTriangleFireAtMs) >= 0) {
}
if (deltaMs > 0) {
const float deltaSeconds = float(deltaMs) * 0.001f;
kMusicModeFadeSeconds, deltaSeconds);
deltaSeconds);
}
FastLED.show(); // Auto-pumps browser or I2S microphone audio.
}
delay(1);
}
void setup()
void loop()
fl::UIDescription description("Demo of the Animatrix effects. @author of fx is StefanPetrick")
fl::UITitle title("Animartrix")
#define I2S_CLK
#define I2S_SD
#define I2S_WS
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
static Config CreateInmp441(int pin_ws, int pin_sd, int pin_clk, AudioChannel channel, u16 sample_rate=44100ul, int i2s_num=0) FL_NO_EXCEPT
Definition input.h:153
constexpr EOrder GRB
Definition eorder.h:19
fl::UIButton trigger("Trigger")
fl::CRGB CRGB
Definition crgb.h:25
#define EVERY_N_MILLIS(N)
Checks whether to execute a block of code every N milliseconds.
Definition lib8tion.h:1000
float gBeatConfidence
#define PIN_PREVIEW
Definition hydropack.ino:40
constexpr float kDefaultMinimumBeatConfidence
Definition hydropack.ino:54
constexpr uint32_t kMaximumMusicBeatGapMs
Definition hydropack.ino:50
fl::UISlider stageThresholdDbSpl("Minimum Music Level (dB SPL)", kDefaultStageThresholdDbSpl, 60.0f, 105.0f, 1.0f)
CRGB previewLeds[kPreviewLedCount]
Definition hydropack.ino:65
float gCenterEnvelopeStage
void updateSoundLevel()
float gTriangleEnvelopeStage
uint8_t toByte(float value)
constexpr uint8_t kHydroPackLedCount
Definition hydropack.ino:45
constexpr float kTriangleEnvelopeFadeSeconds
Definition hydropack.ino:63
uint32_t gTriangleFireAtMs
constexpr uint8_t kPreviewLedCount
Definition hydropack.ino:46
fl::audio::SoundLevelMeter gSoundLevelMeter
constexpr uint8_t kTriangleLedCount
Definition hydropack.ino:42
float gSoundPressureDbSpl
constexpr float kTriangleBassThreshold
Definition hydropack.ino:59
constexpr uint8_t kRightTriangleStart
Definition hydropack.ino:44
void fireAnalyzer(float bass, float threshold, float minIntensity, float &trigger)
fl::audio::Config audioConfig
Definition hydropack.ino:91
uint32_t gLastQualifyingBeatMs
fl::UISlider minimumBeatConfidence("Minimum Vibe Beat Confidence", kDefaultMinimumBeatConfidence, 0.0f, 1.0f, 0.05f)
float gCenterTrigger
fl::ScreenMap makePreviewMap()
Definition hydropack.ino:85
float gCenterLevel
float applyAttackDecay(float value, float target, float attackSeconds, float decaySeconds, float deltaSeconds)
uint32_t gMusicWarmupStartedMs
float getVibeBeatConfidence(const fl::audio::detector::VibeLevels &levels)
float gMusicModeLevel
float gTriangleLevel
constexpr uint8_t kCenterStart
Definition hydropack.ino:43
bool isMusicModeActive()
constexpr float kDefaultStageThresholdDbSpl
Definition hydropack.ino:48
void renderPreview()
fl::ScreenMap previewMap
Definition hydropack.ino:89
constexpr float kMaximumMusicZcf
Definition hydropack.ino:53
constexpr uint32_t kMusicWarmupMs
Definition hydropack.ino:49
constexpr float kCenterEnvelopeFadeSeconds
Definition hydropack.ino:61
float gTriangleTrigger
constexpr uint32_t kTriangleLaunchDelayMs
Definition hydropack.ino:47
const fl::vec2f kHydroPackLedLayout[]
Definition hydropack.ino:69
uint32_t gMusicActiveUntilMs
fl::shared_ptr< fl::audio::Processor > gAudio
constexpr uint32_t kMusicHoldMs
Definition hydropack.ino:51
fl::UIAudio audioUi("Audio Input", audioConfig)
constexpr float kMusicModeFadeSeconds
Definition hydropack.ino:52
float gPendingTriangleLevel
void advanceDoubleEnvelope(float &trigger, float &stage, float &level, float attackSeconds, float decaySeconds, float deltaSeconds)
constexpr float kCenterEnvelopeAttackSeconds
Definition hydropack.ino:60
constexpr float kCenterBassThreshold
Definition hydropack.ino:58
constexpr float kTriangleEnvelopeAttackSeconds
Definition hydropack.ino:62
bool isMusicPresent()
float advanceLinearFade(float value, float target, float durationSeconds, float deltaSeconds)
#define FL_WARN(...)
Definition log.h:440
duration< fl::i64 > seconds
Seconds - duration with period of 1 second.
Definition chrono.h:109
constexpr common_type_t< T, U > max(T a, U b) FL_NO_EXCEPT
Definition math.h:75
void delay(u32 ms, bool run_async=true) FL_NO_EXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NO_EXCEPT
Definition math.h:71
constexpr enable_if< is_fixed_point< T >::value, T >::type clamp(T x, T lo, T hi) FL_NO_EXCEPT
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
vec2< float > vec2f
Definition geometry.h:333
void printf(const char *format) FL_NO_EXCEPT
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode)
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
Definition sample.cpp.hpp:9
constexpr enable_if< is_fixed_point< T >::value, T >::type step(T edge, T x) FL_NO_EXCEPT
#define FL_STATIC_ASSERT(...)
Portable compile-time assertion wrapper.
unsigned char uint8_t
Definition stdint.h:208
fl::u32 uint32_t
Definition stdint.h:218
fl::i32 int32_t
Definition stdint.h:219
#define Serial
Definition serial.h:304
Aggregator header for the fl/ui/ family of per-element UI types.