Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,12 @@ build_flags =
-D FT_MOONLIGHT=1
-D FT_MONITOR=1
-D EFFECTS_STACK_SIZE=3072 ; psramFound() ? 4 * 1024 : 3 * 1024
-D DRIVERS_STACK_SIZE=4096 ; psramFound() ? 4 * 1024 : 3 * 1024, 4096 is sufficient for now
-D DRIVERS_STACK_SIZE=6144 ; psramFound() ? 4 * 1024 : 3 * 1024, 4096 is sufficient for now. Update: due to FastLED audio I had to increase to 6144 (might want to move audio to a separate task)

; -D FASTLED_TESTING ; causes duplicate definition of initSpiHardware(); - workaround: removed implementation in spi_hw_manager_esp32.cpp.hpp
-D FASTLED_BUILD=\"20260221\"
-D FASTLED_BUILD=\"20260223\"
lib_deps =
https://github.com/FastLED/FastLED#9d0b0eb9b5e59e4093982e0c2bdcfdff72ca80cb ; master 20260221
https://github.com/FastLED/FastLED#d03ffd69c68f1a00f883243f78b2a0e9bfb66298 ; master 20260223
https://github.com/ewowi/WLED-sync#25f280b5e8e47e49a95282d0b78a5ce5301af4fe ; sourceIP + fftUdp.clear() if arduino >=3 (20251104)

; 💫 currently only enabled on s3 as esp32dev runs over 100%
Expand Down
17 changes: 14 additions & 3 deletions src/MoonBase/Nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class LiveScriptNode : public Node {
// layout
void onLayout() override; // call map in LiveScript

~LiveScriptNode();
~LiveScriptNode() override;

// LiveScript functions
void compileAndRun();
Expand Down Expand Up @@ -346,6 +346,16 @@ static struct SharedData {
size_t clientListSize;

Coord3D gravity;

// FastLED Audio
bool vocalsActive = false;
float vocalConfidence = 0.0f;
float bassLevel = 0.0f;
float midLevel = 0.0f;
float trebleLevel = 0.0f;
bool beat = false;
uint8_t percussionType = UINT8_MAX;

} sharedData;

/**
Expand All @@ -359,10 +369,11 @@ static struct SharedData {
#include "MoonLight/Nodes/Drivers/D_ArtnetIn.h"
#include "MoonLight/Nodes/Drivers/D_ArtnetOut.h"
#include "MoonLight/Nodes/Drivers/D_AudioSync.h"
#include "MoonLight/Nodes/Drivers/D_FastLED.h"
#include "MoonLight/Nodes/Drivers/D_FastLEDAudio.h"
#include "MoonLight/Nodes/Drivers/D_FastLEDDriver.h"
#include "MoonLight/Nodes/Drivers/D_Hub75.h"
#include "MoonLight/Nodes/Drivers/D_Infrared.h"
#include "MoonLight/Nodes/Drivers/D_IMU.h"
#include "MoonLight/Nodes/Drivers/D_Infrared.h"
#include "MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h"
#include "MoonLight/Nodes/Drivers/D__Sandbox.h"
#include "MoonLight/Nodes/Effects/E_FastLED.h"
Expand Down
4 changes: 4 additions & 0 deletions src/MoonLight/Modules/ModuleDrivers.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ class ModuleDrivers : public NodeManager {
addControlValue(control, getNameAndTags<SpiralLayout>());
addControlValue(control, getNameAndTags<SingleRowLayout>());
addControlValue(control, getNameAndTags<SingleColumnLayout>());
addControlValue(control, getNameAndTags<TubesLayout>());

// Drivers, Most used first
addControlValue(control, getNameAndTags<ParallelLEDDriver>());
addControlValue(control, getNameAndTags<FastLEDDriver>());
addControlValue(control, getNameAndTags<FastLEDAudioDriver>());
addControlValue(control, getNameAndTags<ArtNetInDriver>());
addControlValue(control, getNameAndTags<ArtNetOutDriver>());
addControlValue(control, getNameAndTags<AudioSyncDriver>());
Expand Down Expand Up @@ -135,10 +137,12 @@ class ModuleDrivers : public NodeManager {
if (!node) node = checkAndAlloc<TorontoBarGourdsLayout>(name);
if (!node) node = checkAndAlloc<SingleRowLayout>(name);
if (!node) node = checkAndAlloc<SingleColumnLayout>(name);
if (!node) node = checkAndAlloc<TubesLayout>(name);

// Drivers most used first
if (!node) node = checkAndAlloc<ParallelLEDDriver>(name);
if (!node) node = checkAndAlloc<FastLEDDriver>(name);
if (!node) node = checkAndAlloc<FastLEDAudioDriver>(name);
if (!node) node = checkAndAlloc<ArtNetInDriver>(name);
if (!node) node = checkAndAlloc<ArtNetOutDriver>(name);
if (!node) node = checkAndAlloc<AudioSyncDriver>(name);
Expand Down
2 changes: 2 additions & 0 deletions src/MoonLight/Modules/ModuleEffects.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class ModuleEffects : public NodeManager {

// FastLED effects
addControlValue(control, getNameAndTags<RainbowEffect>());
addControlValue(control, getNameAndTags<FLAudioEffect>());

// Moving head effects, alphabetically
addControlValue(control, getNameAndTags<AmbientMoveEffect>());
Expand Down Expand Up @@ -291,6 +292,7 @@ class ModuleEffects : public NodeManager {

// FastLED
if (!node) node = checkAndAlloc<RainbowEffect>(name);
if (!node) node = checkAndAlloc<FLAudioEffect>(name);

// Moving head effects, alphabetically

Expand Down
205 changes: 205 additions & 0 deletions src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
@title MoonLight
@file D_FastLEDAudio.h
@repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs
@Authors https://github.com/MoonModules/MoonLight/commits/main
@Doc https://moonmodules.org/MoonLight/moonlight/overview/
@Copyright © 2026 Github MoonLight Commit Authors
@license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
@license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information.
**/

#pragma once

#if FT_MOONLIGHT

#include "fl/audio.h"
#include "fl/audio/audio_processor.h"
#include "fl/audio_input.h"
#include "fl/time_alpha.h"

// https://github.com/FastLED/FastLED/blob/master/src/fl/audio/README.md

class FastLEDAudioDriver : public Node {
private:
// Member variables for audio configuration
fl::AudioConfigI2S* i2sConfig = nullptr;
fl::AudioConfig* config = nullptr;
fl::shared_ptr<fl::IAudioInput> audioInput;

public:
static const char* name() { return "FastLED Audio"; }
static uint8_t dim() { return _NoD; }
static const char* tags() { return "☸️"; }

fl::AudioProcessor audioProcessor;

update_handler_id_t ioUpdateHandler;

bool signalConditioning = true;
bool autoGain = false;
bool noiseFloorTracking = false;
uint8_t channel = fl::Left;

void setup() override {
addControl(signalConditioning, "signalConditioning", "checkbox");
addControl(autoGain, "autoGain", "checkbox");
addControl(noiseFloorTracking, "noiseFloorTracking", "checkbox");
addControl(channel, "channel", "select");
addControlValue("Left");
addControlValue("Right");
addControlValue("Both");

ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, true);
readPins(); // Node added at runtime so initial IO update not received so run explicitly

audioProcessor.onBeat([]() {
sharedData.beat = true;
// EXT_LOGD(ML_TAG, "onBeat");
});

audioProcessor.onVocalStart([]() {
sharedData.vocalsActive = true;
// EXT_LOGD(ML_TAG, "onVocalStart");
});

audioProcessor.onVocalEnd([]() {
sharedData.vocalsActive = false;
// EXT_LOGD(ML_TAG, "onVocalEnd");
});

audioProcessor.onVocalConfidence([](float confidence) {
sharedData.vocalConfidence = sharedData.vocalsActive ? confidence : 0.0;
// EXT_LOGD(ML_TAG, "onVocalConfidence %d", confidence);
});

audioProcessor.onBass([](float level) {
if (level > 0.01f) {
sharedData.bassLevel = level;
// EXT_LOGD(ML_TAG, "onBass: %f", level);
}
});

audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onMid: %f", level);
}
});
Comment on lines 83 to 88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: copy-paste log label in onMid callback says "onBass".

Line 84 reads "onBass: %f" inside the onMid lambda — a copy-paste leftover. Also, the onVocalConfidence log (line 71) uses %d for a float argument; should be %f. Both are currently commented out, but worth fixing before uncomment.

🔧 Proposed fix
     audioProcessor.onMid([](float level) {
       if (level > 0.01f) {
         sharedData.midLevel = level;
-        // EXT_LOGD(ML_TAG, "onBass: %f", level);
+        // EXT_LOGD(ML_TAG, "onMid: %f", level);
       }
     });
     audioProcessor.onVocalConfidence([](float confidence) {
       sharedData.vocalConfidence = sharedData.vocalsActive ? confidence : 0.0;
-      // EXT_LOGD(ML_TAG, "onVocalConfidence %d", confidence);
+      // EXT_LOGD(ML_TAG, "onVocalConfidence %f", confidence);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onBass: %f", level);
}
});
audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onMid: %f", level);
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 81 - 86, Fix the
two incorrect log formats in the audio callbacks: in the audioProcessor.onMid
lambda change the log label from "onBass" to "onMid" so it matches the callback
(reference: audioProcessor.onMid and sharedData.midLevel, ML_TAG), and in the
onVocalConfidence log change the printf format specifier from %d to %f to match
the float argument (reference: audioProcessor.onVocalConfidence). Update those
commented log lines accordingly so they are correct when uncommented.


audioProcessor.onTreble([](float level) {
if (level > 0.01f) {
sharedData.trebleLevel = level;
// EXT_LOGD(ML_TAG, "onTreble: %f", level);
}
});
audioProcessor.onPercussion([](fl::PercussionType type) {
// EXT_LOGD(ML_TAG, "onPercussion: %d", type);
sharedData.percussionType = (uint8_t)type;
});
}

void onUpdate(const Char<20>& oldValue, const JsonObject& control) override {
if (control["name"] == "signalConditioning") {
audioProcessor.setSignalConditioningEnabled(signalConditioning);
}
if (control["name"] == "autoGain") {
audioProcessor.setAutoGainEnabled(autoGain);
}
if (control["name"] == "noiseFloorTracking") {
audioProcessor.setNoiseFloorTrackingEnabled(noiseFloorTracking);
}
if (control["name"] == "channel" && oldValue != "") { // not on boot as readPins will do it then
// recreate with the new channel
stopAudio();
startAudio();
}
}

uint8_t pinI2SSD = UINT8_MAX;
uint8_t pinI2SWS = UINT8_MAX;
uint8_t pinI2SSCK = UINT8_MAX;

bool updatePin(uint8_t& pin, const uint8_t pinUsage) {
bool i2sPinsChanged = false;
moduleIO->read(
[&](ModuleState& state) {
for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
if (pinObject["usage"] == pinUsage && pin != pinObject["GPIO"]) {
pin = pinObject["GPIO"];
i2sPinsChanged = true;
}
}
},
name());
return i2sPinsChanged; // an empty pin means it is not allocated anymore
}

void readPins() {
if (safeModeMB) {
EXT_LOGW(ML_TAG, "Safe mode enabled, not adding pins");
return;
}

bool changed = updatePin(pinI2SWS, pin_I2S_WS);
changed = updatePin(pinI2SSD, pin_I2S_SD) || changed;
changed = updatePin(pinI2SSCK, pin_I2S_SCK) || changed;

if (changed) {
EXT_LOGI(ML_TAG, "(re)creating audioInput %d %d %d", pinI2SWS, pinI2SSD, pinI2SSCK);
stopAudio();
if (pinI2SWS != UINT8_MAX && pinI2SSD != UINT8_MAX && pinI2SSCK != UINT8_MAX) startAudio();
}
}

void loop() override {
if (!audioInput) return;

sharedData.beat = false;
sharedData.percussionType = UINT8_MAX;

while (fl::AudioSample sample = audioInput->read()) {
audioProcessor.update(sample);
}
}

void startAudio() {
// Create configuration objects
i2sConfig = new fl::AudioConfigI2S(pinI2SWS, pinI2SSD, pinI2SSCK, 0, channel == 1 ? fl::Right : channel == 2 ? fl::Both : fl::Left, 44100, 16, fl::Philips);

config = new fl::AudioConfig(*i2sConfig);

fl::string errorMsg;
audioInput = fl::IAudioInput::create(*config, &errorMsg);
if (!audioInput) {
EXT_LOGE(ML_TAG, "Failed to create audio input: %s", errorMsg.c_str());
return;
}
audioInput->start();
}

void stopAudio() {
if (audioInput) {
audioInput->stop();
audioInput.reset(); // Explicitly release shared_ptr, even makes it a nullptr...
}

// Clean up raw pointers
if (config) {
delete config;
config = nullptr;
}

if (i2sConfig) {
delete i2sConfig;
i2sConfig = nullptr;
}
}

~FastLEDAudioDriver() override {
stopAudio();
moduleIO->removeUpdateHandler(ioUpdateHandler);
}
};

#endif
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
@title MoonLight
@file FastLED.h
@file D_FastLEDDriver.h
@repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs
@Authors https://github.com/MoonModules/MoonLight/commits/main
@Doc https://moonmodules.org/MoonLight/moonlight/overview/
Expand All @@ -22,6 +22,9 @@ class FastLEDDriver : public DriverNode {
static uint8_t dim() { return _NoD; }
static const char* tags() { return "☸️"; }

update_handler_id_t ioUpdateHandler;
update_handler_id_t controlUpdateHandler;

Char<32> version = FASTLED_BUILD;
Char<32> status = "NoInit";
Char<32> engine = "Auto";
Expand Down Expand Up @@ -59,7 +62,7 @@ class FastLEDDriver : public DriverNode {
addControl(version, "version", "text", 0, 20, true);
addControl(status, "status", "text", 0, 32, true);

moduleIO->addUpdateHandler(
ioUpdateHandler = moduleIO->addUpdateHandler(
[this](const String& originId) {
uint8_t nrOfPins = MIN(layerP.nrOfLedPins, layerP.nrOfAssignedPins);

Expand All @@ -70,7 +73,7 @@ class FastLEDDriver : public DriverNode {
// should we check here for maxPower changes?
},
false);
moduleControl->addUpdateHandler([this](const String& originId) {
controlUpdateHandler = moduleControl->addUpdateHandler([this](const String& originId) {
// brightness changes here?
});

Expand Down Expand Up @@ -302,7 +305,7 @@ class FastLEDDriver : public DriverNode {
CRGB* leds = (CRGB*)layerP.lights.channelsD;
uint16_t startLed = 0;

FastLED.reset(ResetFlags::CHANNELS);
FastLED.clear(ClearFlags::CHANNELS);

for (uint8_t pinIndex = 0; pinIndex < nrOfPins; pinIndex++) {
EXT_LOGD(ML_TAG, "ledPin p:%d #:%d rgb:%d aff:%s", pins[pinIndex], layerP.ledsPerPin[pinIndex], rgbOrder, options.mAffinity.c_str());
Expand Down Expand Up @@ -349,7 +352,10 @@ class FastLEDDriver : public DriverNode {
auto& events = FastLED.channelEvents();
events.onChannelCreated.clear();
events.onChannelEnqueued.clear();
FastLED.reset(ResetFlags::CHANNELS);
FastLED.clear(ClearFlags::CHANNELS);

moduleIO->removeUpdateHandler(ioUpdateHandler);
moduleControl->removeUpdateHandler(controlUpdateHandler);
}
};

Expand Down
9 changes: 7 additions & 2 deletions src/MoonLight/Nodes/Drivers/D_IMU.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class IMUDriver : public Node {
static uint8_t dim() { return _NoD; }
static const char* tags() { return "☸️"; }

update_handler_id_t ioUpdateHandler;

bool motionTrackingReady = false; // set true if DMP init was successful

Coord3D gyro; // in degrees (not radians)
Expand All @@ -36,7 +38,7 @@ class IMUDriver : public Node {
addControlValue("BMI160"); // not supported yet

// Subscribe to IO updates to detect when I2C becomes ready
moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
// Read current I2C state in case boot-time handler dispatch already occurred
moduleIO->read([this](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name());
requestInitBoard = true;
Expand Down Expand Up @@ -207,7 +209,10 @@ class IMUDriver : public Node {
}
};

~IMUDriver() override { stopBoard(); }
~IMUDriver() override {
stopBoard();
moduleIO->removeUpdateHandler(ioUpdateHandler);
}

private:
MPU6050 mpu;
Expand Down
Loading