Skip to content

Animartrix plus: HUE shift, contrast & brightness boost#344

Open
softhack007 wants to merge 28 commits intomdevfrom
animartrix_plus
Open

Animartrix plus: HUE shift, contrast & brightness boost#344
softhack007 wants to merge 28 commits intomdevfrom
animartrix_plus

Conversation

@softhack007
Copy link
Copy Markdown
Collaborator

@softhack007 softhack007 commented Mar 2, 2026

  • add contrast and brightness boost checkboxes
  • add static HUE shift -> slider changes the main color (HUE)
  • add dynamic HUE cycling -> time-based, slider for speed of change
  • audioreactive HUE shift
  • gamma correction moved into global UM settings
  • toDO: audioreactive intensity
  • toDO: separate audio context per effect instance, using segment::allocateData()
  • toDo: decide if having two sliders (static HUE shift, HUE change speed) is better
  • toDo: Screenshots

Breaking: the first checkbox was previously used for "correct gamma", which is now part of the global usermod settings. This means that gamma correction needs to be switched off in UM settings, instead of setting it with each effect.
Due to the "re-purposed" slider, old preset may start HUE cycling.


Animartix_hue_cycle_part2.mp4
image image

Future idea: alternative to CHSV32-based HUE rotation

check if the matrix-based HUE rotation described here works, too: https://agatedragon.blog/2024/04/02/hue-shift-shader/ .

It looks like three matrices (3x3) are needed: convert color vector to YIQ format, rotate (I,Q) but leave Y untouched, and finally convert back to color with another matrix. All the three matrices can be pre-calculated per frame, and pre-multiplied (use matrix multiplication!) into a single combined "color shift" matrix. Then on pixel level, a simple vector x matrix multiplication should be enough.

Summary by CodeRabbit

  • New Features

    • Audio-reactive effects and a new Audio Monitor mode for visualizing/responding to sound.
    • Hue cycling and audio-driven hue control, plus brightness/contrast boosts and gamma toggle.
    • High-precision CHSV color type and HSV↔RGB conversions for improved color fidelity.
    • New UI controls for speed, hue change, cycle HUE, boost Brightness/Contrast, gamma, and audio detector selection.
  • Refactor

    • Unified color-processing pipeline so hue shift, brightness, contrast, and gamma apply consistently.
  • Config

    • Gamma and audio detector exposed in configuration and UI metadata.

* convert pixel color from RGBW32 to CHSV32,
* shift HUE value,
* convert back to RGBW32

HUE shift is either static (slider), or continuous based on elapsed time.
* slider for HUE shift (static shift, or dynamic speed of change)
* checkbox for HUE cycling (dynamic)
* checkboxes for brightness and contrast boost
* remove checkbox for gamma correction, need to move this into usermod global settings.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 2, 2026

📝 Walkthrough

Walkthrough

Adds audio-reactive features and an AudioMon mode, expands color processing (gamma/contrast/brightness, 16-bit hue via CHSV32), introduces color utilities header, global audio/gamma state and mapping helpers, and exposes config/UI hooks for gamma and audio detector settings.

Changes

Cohort / File(s) Summary
ANIMartRIXMod core & audio pipeline
usermods/usermod_v2_animartrix/usermod_v2_animartrix.h
Adds audio pipeline (getAudio, processAudio, handleAudioHUE, initMonitor), new members (hueshift, cycle_hue, boost_brightness, boost_contrast), audio detector enums, and processColor/getAudioHUE helpers. Review audio logic and new public helpers (contrast/brightness/gamma).
Audio utilities & global state
usermods/usermod_v2_animartrix/usermod_v2_animartrix.h
Introduces free/static utilities getAudioDataOrSim(), map0(), and globals animartrix_use_gamma, animartrix_detectorID. Adds mode hook mode_AudioMon and UI/data constants for AudioMon. Check global state usage and thread-safety assumptions.
Usermod lifecycle & config/UI integration
usermods/usermod_v2_animartrix/usermod_v2_animartrix.h
Guards setup() against double registration, registers AudioMon mode, and adds config methods addToConfig, readFromConfig, appendConfigData, plus expanded addToJsonInfo metadata. Verify cfg.json keys and UI exposure.
Color utilities header
wled00/colors.h
Adds CHSV32 (16-bit hue + 8-bit sat/val, raw 32-bit), constructors/conversions, and hsv2rgb/rgb2hsv functions for higher-resolution hue handling. Validate conversion accuracy and alignment with existing color code.
Mode registration & UI data
usermods/.../usermod_v2_animartrix.h
Adds 1D AudioMon UI mode data (_data_FX_mode_AudioMon1D), UI macro definitions, and registers animAudioMon / animAudioMon0. Review UI string formatting and mode registration interactions.

Sequence Diagram(s)

sequenceDiagram
    participant AudioDetector as Audio Detector
    participant Animartrix as ANIMartRIXMod
    participant ColorUtils as Color Utils (CHSV32/gamma)
    participant LEDStrip as LED Strip

    AudioDetector->>Animartrix: provide sample / detector reading
    Animartrix->>Animartrix: getAudioDataOrSim() / processAudio()
    Animartrix->>Animartrix: handleAudioHUE() -> compute hueshift / audioHUE
    Animartrix->>ColorUtils: processColor(rgb) (apply hue shift, contrast, brightness, gamma)
    ColorUtils-->>Animartrix: processed RGB
    Animartrix->>LEDStrip: write pixel data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I twitch my ears to bass and treble,
I nudge the hue, make color revel,
Sixteen-bit dawn, a gamma sigh,
Pixels pulse — a carrot sky,
Hop, blink, the strip comes alive.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: HUE shift (static, dynamic, and audio-reactive), contrast boost, and brightness boost. However, it omits significant additions like gamma correction settings and audio monitor functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch animartrix_plus

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

 UI typo: “Brighness” → “Brightness”.
coderabbitai[bot]

This comment was marked as resolved.

With strip.now in milliseconds, the fast-path overflows after ~7.5 hours and the slow-path after ~54.2 hours, causing visible hue jumps in continuous uptime scenarios.

=> Cast the multiplication operands to uint64_t before division.
avoid losing accuracy in hue shift calculation
coderabbitai[bot]

This comment was marked as resolved.

explains rationale behind enhanceBrightness fixed-point math using integer sqrt()
coderabbitai[bot]

This comment was marked as resolved.

AnimarTrix effects were designed with hardware-level gamma correction in mind => change default to apply gamma.

Note: this may create an unexpected / different look for older presets. If old behaviour is needed, disable gamma correction in UM settings (option will be added soon)
improves accuracy by embedding "<<4" into the main multiplication
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

gravimeter-like 1D effect for audio debugging

if ((deltaSample > 0.0f) || (decayMS < 1)) {
// fast attack/decay with minimal filtering
lastAudioData[detectorID] += 0.9f * audioSmooth * deltaSample; // 0.9 for damping of jumps
Copy link
Copy Markdown
Collaborator Author

@softhack007 softhack007 Mar 4, 2026

Choose a reason for hiding this comment

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

tinkering help: adjust "0.9" if you still find effects too "flash-like" (smaller = smoother)

audioSample = 255.0f * (logf(audioSample) - M_LOG_3) / M_LOG_256; // log scaling - amplifies lower frequencies, reduces noise
else audioSample = 0.0f;
break;
case DET_BASS:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tinkering help: the FFT based ones below looked sufficiently "distinct" in my own tests. comments/feedback welcome 😀

//========================================================================================================================

#define ANIMARTRIX_UI_MONITOR "Speed(nothing),HUE Change(nothing),Audio Strength,Audio Decay,Audio Detector ID,Amplify,Amplify,;!;1v;c1=255,c2=64,c3=0,o1=0,o2=0,m12=7,si=0"
static const char _data_FX_mode_AudioMon1D[] PROGMEM = "Y💡AudioMonitor 1D ☾@" ANIMARTRIX_UI_MONITOR;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tinkering help: this is your friend if you want to see the audio-based hue shift as a bar.

not needed, as AR core already does some filtering
correct missing filter 4
add input validation
correct metadata errors, change default for c2 from 64 to 32
"anim" is one global object - create separate object for Monitor, to prevent interferences between effects wtih different filter options
@softhack007 softhack007 self-assigned this Mar 5, 2026
* use different state object when running monitor with detectorID == 0
* monitor takes over main effect parameters when detectorID == 0
* minor tinkering
* adding rounding in monitor effect drawing routine
Copilot AI review requested due to automatic review settings March 5, 2026 16:33
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds extended color processing controls to Animartrix effects (static/dynamic HUE shift, brightness/contrast boost, audio-reactive HUE) and moves gamma correction to a global usermod setting.

Changes:

  • Introduces CHSV32 + fast HSV/RGB conversion helpers for higher-precision HUE operations.
  • Extends Animartrix effect UI controls (cycle HUE, boost brightness/contrast) and adds an audio monitor effect.
  • Adds persistent usermod config options for gamma correction and audio detector selection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
wled00/colors.h New shared color utilities (CHSV32, hsv2rgb, rgb2hsv) to support precise HUE shifting.
usermods/usermod_v2_animartrix/usermod_v2_animartrix.h Updates Animartrix effects to apply HUE shift/boosts, adds audio-driven HUE logic + monitor mode, and adds global gamma/audio config persistence.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +260 to +269
unsigned lastAudioTime[NUM_DETECTORS] = {0};

float processAudio(unsigned detectorID, unsigned decayMS) {
constexpr float audioSmooth = 0.88f; // audio smoothing factor - to avoid instant flashes and very hard jumps
if (detectorID >= NUM_DETECTORS) detectorID = 0;
if (detectorID == 0) return 0.0f;

unsigned timestamp = millis();
long delta_time = timestamp - lastAudioTime[detectorID];
delta_time = min(max(delta_time , 1L), 1000L); // clamp to meaningful values
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

millis() wrap-around can break this timing math: timestamp and lastAudioTime[] are unsigned, but the subtraction happens in unsigned arithmetic and is then stored in a signed long, which can become implementation-defined/negative at wrap. Use uint32_t (or unsigned long) for both the stored timestamps and the delta, and rely on unsigned wrap-safe subtraction.

Suggested change
unsigned lastAudioTime[NUM_DETECTORS] = {0};
float processAudio(unsigned detectorID, unsigned decayMS) {
constexpr float audioSmooth = 0.88f; // audio smoothing factor - to avoid instant flashes and very hard jumps
if (detectorID >= NUM_DETECTORS) detectorID = 0;
if (detectorID == 0) return 0.0f;
unsigned timestamp = millis();
long delta_time = timestamp - lastAudioTime[detectorID];
delta_time = min(max(delta_time , 1L), 1000L); // clamp to meaningful values
unsigned long lastAudioTime[NUM_DETECTORS] = {0};
float processAudio(unsigned detectorID, unsigned decayMS) {
constexpr float audioSmooth = 0.88f; // audio smoothing factor - to avoid instant flashes and very hard jumps
if (detectorID >= NUM_DETECTORS) detectorID = 0;
if (detectorID == 0) return 0.0f;
unsigned long timestamp = millis();
unsigned long delta_time = timestamp - lastAudioTime[detectorID];
delta_time = min(max(delta_time , 1UL), 1000UL); // clamp to meaningful values

Copilot uses AI. Check for mistakes.
float audioShift = 255.0f * processAudio(detectorID, audioDecay); // 0.0 ... 65025 = full "HUE turn"
float audioStrength = float(SEGMENT.custom1) / 255.0f; // multiplier [0...1]
audioShift = audioShift * audioStrength;
if (SEGMENT.custom1 > 1) hueshift = hueshift + unsigned(audioShift);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

This addition mixes signed (hueshift is int) and unsigned (unsigned(audioShift)), which triggers unsigned integer promotions. If hueshift is negative, the expression can wrap to a huge unsigned value and then convert back to int (implementation-defined), producing erratic HUE jumps. Keep the arithmetic in a signed type (e.g., convert audioShift to int32_t with explicit rounding/clamping) before adding to hueshift.

Suggested change
if (SEGMENT.custom1 > 1) hueshift = hueshift + unsigned(audioShift);
if (SEGMENT.custom1 > 1) {
int32_t audioShiftInt = (audioShift > 0.0f) ? (int32_t)(audioShift + 0.5f) : 0;
hueshift += audioShiftInt;
}

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +204
#define NUM_DETECTORS 7 // total, including "none"
#define DET_NONE 0 // no audio
#define DET_VOLUME 1 // AR volumeSmth (relative)
#define DET_PRESSURE 2 // AR soundPressure (absolute)
#define DET_ZCR 3 // AR ZeroCrossingCount (music "density")
#define DET_BASS 4 // AR bass (FFT channel 1+2)
#define DET_MID 5 // AR voices & melody (FFT channel 5,7,8,10)
#define DET_HIGH 6 // AR high frequencies - high-hats, pipes, high pitch (FFT channel 12,13,15)

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

These #defines are declared inside the class body but leak into the global preprocessor namespace for the rest of the translation unit (and they’re used later outside the class). Consider replacing them with an enum/enum class and a static constexpr size_t kNumDetectors (or similar) to avoid macro pollution and make scoping/type-checking clearer.

Suggested change
#define NUM_DETECTORS 7 // total, including "none"
#define DET_NONE 0 // no audio
#define DET_VOLUME 1 // AR volumeSmth (relative)
#define DET_PRESSURE 2 // AR soundPressure (absolute)
#define DET_ZCR 3 // AR ZeroCrossingCount (music "density")
#define DET_BASS 4 // AR bass (FFT channel 1+2)
#define DET_MID 5 // AR voices & melody (FFT channel 5,7,8,10)
#define DET_HIGH 6 // AR high frequencies - high-hats, pipes, high pitch (FFT channel 12,13,15)
static constexpr unsigned NUM_DETECTORS = 7; // total, including "none"
enum DetectorId : unsigned {
DET_NONE = 0, // no audio
DET_VOLUME = 1, // AR volumeSmth (relative)
DET_PRESSURE = 2, // AR soundPressure (absolute)
DET_ZCR = 3, // AR ZeroCrossingCount (music "density")
DET_BASS = 4, // AR bass (FFT channel 1+2)
DET_MID = 5, // AR voices & melody (FFT channel 5,7,8,10)
DET_HIGH = 6 // AR high frequencies - high-hats, pipes, high pitch (FFT channel 12,13,15)
};

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h`:
- Around line 259-289: The audio smoothing state (lastAudioData, lastAudioTime)
used by processAudio is currently shared globally via the anim object and must
be moved to per-segment storage; create a small struct (e.g. AudioState { float
lastAudioData[NUM_DETECTORS]; unsigned lastAudioTime[NUM_DETECTORS]; }) and
allocate it in segment::allocateData() (or reuse SEGENV.data) and store a
pointer in the segment/env; update processAudio to fetch the per-segment
AudioState from SEGENV.data (or the segment pointer) instead of the local
arrays, initialize the arrays on allocation, and replace all references to
lastAudioData/lastAudioTime with the per-segment struct fields so each segment
maintains its own audio smoothing state and avoids cross-segment bleed.
- Around line 206-215: The getAudio function dereferences fixed indices of
um_data->u_data without verifying um_data and the array shape; add guards to
ensure um_data is non-null, um_data->u_size >= 12 (or at least covers indices
0,2,9,11) and each u_data[index] pointer is non-null before casting/reading
(e.g., check um_data, um_data->u_size, then validate um_data->u_data[0], [2],
[9], [11]); if any check fails, return a safe default (such as 0.0f) or handle
the missing fields gracefully to avoid crashes while preserving existing
behavior. Ensure these checks are placed at the top of getAudio so subsequent
reads of volumeSmth, fftResult, soundPressure, and zCr are safe.
- Around line 267-270: The delta-time computation uses signed math (long
delta_time) which can become negative after millis() rollovers; change it to
unsigned type and perform the subtraction unsigned-to-unsigned (e.g., unsigned
long timestamp = millis(); unsigned long delta_time = timestamp -
lastAudioTime[detectorID];) then clamp using unsigned-safe bounds before
converting or comparing; update references to delta_time (the clamp and the
comparison delta_time < 3) so they operate on the unsigned variable while still
using lastAudioData[detectorID] and detectorID as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74bf2f01-d4de-441d-ace8-ff954d28230a

📥 Commits

Reviewing files that changed from the base of the PR and between 82a8cab and a0a86df.

📒 Files selected for processing (1)
  • usermods/usermod_v2_animartrix/usermod_v2_animartrix.h

Comment on lines +259 to +289
float lastAudioData[NUM_DETECTORS] = {0.0f};
unsigned lastAudioTime[NUM_DETECTORS] = {0};

float processAudio(unsigned detectorID, unsigned decayMS) {
constexpr float audioSmooth = 0.88f; // audio smoothing factor - to avoid instant flashes and very hard jumps
if (detectorID >= NUM_DETECTORS) detectorID = 0;
if (detectorID == 0) return 0.0f;

unsigned timestamp = millis();
long delta_time = timestamp - lastAudioTime[detectorID];
delta_time = min(max(delta_time , 1L), 1000L); // clamp to meaningful values
if (delta_time < 3) return min(max(0.0f, lastAudioData[detectorID]), 255.0f); // too early, value has not changed since last time

float newAudio = getAudio(detectorID);
float deltaSample = newAudio - lastAudioData[detectorID]; // positive attack, negative during decay

if ((deltaSample >= 0.0f) || (decayMS < 1)) {
// fast attack/decay with minimal filtering
// lastAudioData[detectorID] += 0.9f * audioSmooth * deltaSample; // experimental - 0.9 for damping of jumps
lastAudioData[detectorID] += audioSmooth * deltaSample;
} else {
// slow decay: time-based linear decay; similar to AR limitSampleDynamics() function
constexpr float bigChange = 184; // a large, expected sample value that decays to 0 in decayMS millis
float maxDecay = - bigChange * float(delta_time) / float(decayMS); // allowed decay for elapsed time (must be negative!)
if (deltaSample < maxDecay) deltaSample = maxDecay; // limit delta if new value is too low
lastAudioData[detectorID] += audioSmooth * deltaSample;
}

lastAudioTime[detectorID] = timestamp;
return min(max(0.0f, lastAudioData[detectorID]), 255.0f); // clamp result to 0..255, but keep exact value internally
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Audio smoothing state is shared across segments/effect instances.

lastAudioData and lastAudioTime are instance fields, but regular effects all run through the single global anim object (Line 447). This couples detector state between segments and can produce cross-segment bleed/timing interference under multi-segment use. Please move audio state to per-segment storage (segment::allocateData() / SEGENV.data) or another segment-scoped context.

I can draft a small per-segment state struct and integration patch if you want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h` around lines 259 -
289, The audio smoothing state (lastAudioData, lastAudioTime) used by
processAudio is currently shared globally via the anim object and must be moved
to per-segment storage; create a small struct (e.g. AudioState { float
lastAudioData[NUM_DETECTORS]; unsigned lastAudioTime[NUM_DETECTORS]; }) and
allocate it in segment::allocateData() (or reuse SEGENV.data) and store a
pointer in the segment/env; update processAudio to fetch the per-segment
AudioState from SEGENV.data (or the segment pointer) instead of the local
arrays, initialize the arrays on allocation, and replace all references to
lastAudioData/lastAudioTime with the per-segment struct fields so each segment
maintains its own audio smoothing state and avoids cross-segment bleed.

Comment on lines +267 to +270
unsigned timestamp = millis();
long delta_time = timestamp - lastAudioTime[detectorID];
delta_time = min(max(delta_time , 1L), 1000L); // clamp to meaningful values
if (delta_time < 3) return min(max(0.0f, lastAudioData[detectorID]), 255.0f); // too early, value has not changed since last time
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use unsigned elapsed-time math for millis() delta.

Line 268 stores timestamp - lastAudioTime[...] into long. After long uptimes, signed conversion can produce negative values and skew decay clamping. Keep this delta unsigned end-to-end.

⏱️ Proposed fix
-      unsigned timestamp = millis();
-      long delta_time = timestamp - lastAudioTime[detectorID];
-      delta_time = min(max(delta_time , 1L), 1000L);           // clamp to meaningful values
+      uint32_t timestamp = millis();
+      uint32_t delta_time = timestamp - lastAudioTime[detectorID];
+      delta_time = constrain(delta_time, uint32_t(1), uint32_t(1000)); // clamp to meaningful values

Based on learnings: In MoonModules/WLED-MM, the unsigned timePassed = millis() - last_time; pattern is rollover-safe and preferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h` around lines 267 -
270, The delta-time computation uses signed math (long delta_time) which can
become negative after millis() rollovers; change it to unsigned type and perform
the subtraction unsigned-to-unsigned (e.g., unsigned long timestamp = millis();
unsigned long delta_time = timestamp - lastAudioTime[detectorID];) then clamp
using unsigned-safe bounds before converting or comparing; update references to
delta_time (the clamp and the comparison delta_time < 3) so they operate on the
unsigned variable while still using lastAudioData[detectorID] and detectorID as
before.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
usermods/usermod_v2_animartrix/usermod_v2_animartrix.h (2)

267-270: ⚠️ Potential issue | 🟠 Major

Keep the millis() delta unsigned end-to-end.

timestamp - lastAudioTime[detectorID] is rollover-safe as unsigned math, but narrowing it to long breaks that once uptime crosses the signed 32-bit range. For detectors that still have lastAudioTime == 0, this can pin the code in the < 3 early-return path until the next wrap.

Proposed fix
-      unsigned timestamp = millis();
-      long delta_time = timestamp - lastAudioTime[detectorID];
-      delta_time = min(max(delta_time , 1L), 1000L);           // clamp to meaningful values
+      uint32_t timestamp = millis();
+      uint32_t delta_time = timestamp - lastAudioTime[detectorID];
+      delta_time = constrain(delta_time, uint32_t(1), uint32_t(1000)); // clamp to meaningful values

Based on learnings: In MoonModules/WLED-MM and embedded systems code, the pattern unsigned timePassed = millis() - last_time; is rollover-safe and proven to work correctly even when millis() wraps around.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h` around lines 267 -
270, The subtraction of millis() and lastAudioTime[detectorID] must remain
unsigned end-to-end: change delta_time from signed long to an unsigned type
(e.g., unsigned or uint32_t) so the expression "timestamp -
lastAudioTime[detectorID]" stays rollover-safe, then perform the clamp and the
"< 3" early-return using that unsigned delta_time and keep
lastAudioData[detectorID] handling unchanged; update any min/max/cast uses
around delta_time to operate on the same unsigned type to avoid signed
narrowing.

259-260: ⚠️ Potential issue | 🟠 Major

Move the audio smoothing state to segment-local storage.

These arrays still live on a shared ANIMartRIXMod instance, so regular Animartrix segments reuse each other’s detector history and decay timing. Please move them into SEGENV.data / segment::allocateData() so each effect instance keeps independent audio state.

I can draft the small per-segment AudioState struct and wiring if you want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h` around lines 259 -
260, The two per-detector arrays (lastAudioData and lastAudioTime) are currently
fields on ANIMartRIXMod and must be moved into per-segment storage: define a
small struct AudioState containing float lastAudioData[NUM_DETECTORS] and
unsigned lastAudioTime[NUM_DETECTORS], allocate it from segment::allocateData()
and store the pointer in SEGENV.data for each segment, initialize the arrays
when allocated, update all references that used ANIMartRIXMod::lastAudioData /
lastAudioTime to read/write via a cast of SEGENV.data to AudioState*, and remove
the original fields from ANIMartRIXMod so each segment keeps independent audio
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@usermods/usermod_v2_animartrix/usermod_v2_animartrix.h`:
- Around line 267-270: The subtraction of millis() and lastAudioTime[detectorID]
must remain unsigned end-to-end: change delta_time from signed long to an
unsigned type (e.g., unsigned or uint32_t) so the expression "timestamp -
lastAudioTime[detectorID]" stays rollover-safe, then perform the clamp and the
"< 3" early-return using that unsigned delta_time and keep
lastAudioData[detectorID] handling unchanged; update any min/max/cast uses
around delta_time to operate on the same unsigned type to avoid signed
narrowing.
- Around line 259-260: The two per-detector arrays (lastAudioData and
lastAudioTime) are currently fields on ANIMartRIXMod and must be moved into
per-segment storage: define a small struct AudioState containing float
lastAudioData[NUM_DETECTORS] and unsigned lastAudioTime[NUM_DETECTORS], allocate
it from segment::allocateData() and store the pointer in SEGENV.data for each
segment, initialize the arrays when allocated, update all references that used
ANIMartRIXMod::lastAudioData / lastAudioTime to read/write via a cast of
SEGENV.data to AudioState*, and remove the original fields from ANIMartRIXMod so
each segment keeps independent audio state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e706c9b5-e9a6-41a4-8257-5127e11047c6

📥 Commits

Reviewing files that changed from the base of the PR and between a0a86df and a634eed.

📒 Files selected for processing (1)
  • usermods/usermod_v2_animartrix/usermod_v2_animartrix.h

@softhack007
Copy link
Copy Markdown
Collaborator Author

softhack007 commented Mar 24, 2026

ToDO for completing this PR:

@netmindz
Copy link
Copy Markdown
Collaborator

netmindz commented Apr 3, 2026

I will try and find some tine over the long weekend to try out this branch

@softhack007
Copy link
Copy Markdown
Collaborator Author

@coderabbitai please re-review this PR. Use the new review instructions and coding guidelines (.coderabbit.yaml) that were just added in the main mdev branch.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll fetch the new .coderabbit.yaml configuration and the PR's changed files to conduct a proper re-review.


🏁 Script executed:

#!/bin/bash
# Get the .coderabbit.yaml from mdev branch
gh api repos/MoonModules/WLED-MM/contents/.coderabbit.yaml?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "NOT FOUND"

echo "---SEPARATOR---"

# Get the PR diff
gh pr diff 344 --repo MoonModules/WLED-MM 2>/dev/null | head -1500

Length of output: 42206


Now let me fetch the detailed coding guidelines.


🏁 Script executed:

#!/bin/bash
# Get the cpp instructions from mdev branch
echo "=== cpp.instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/cpp.instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "NOT FOUND"

echo ""
echo "=== copilot-instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/copilot-instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "NOT FOUND"

echo ""
echo "=== esp-idf.instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/esp-idf.instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "NOT FOUND"

Length of output: 66297


@softhack007 — re-review conducted against the new .coderabbit.yaml guidelines (.github/cpp.instructions.md + .github/copilot-instructions.md). Here are the findings, organized by severity.


usermods/usermod_v2_animartrix/usermod_v2_animartrix.h

🔴 Critical

1. #define macros inside a class body pollute the global namespace

The #define NUM_DETECTORS, DET_NONE, DET_VOLUME, … DET_HIGH directives are placed inside the private: section of ANIMartRIXMod, but the C preprocessor has no concept of class scope — they become globally visible after the class definition. Per cpp.instructions.md, prefer constexpr for compile-time constants:

// Inside ANIMartRIXMod private:
static constexpr unsigned NUM_DETECTORS = 7; // total, including "none"
enum DetectorID : uint8_t {
  DET_NONE     = 0,
  DET_VOLUME   = 1,
  DET_PRESSURE = 2,
  DET_ZCR      = 3,
  DET_BASS     = 4,
  DET_MID      = 5,
  DET_HIGH     = 6,
};

2. Float-to-unsigned conversion is undefined behavior

In handleAudioHUE():

if (SEGMENT.custom1 > 1) hueshift = hueshift + unsigned(audioShift);

audioShift is float. Per cpp.instructions.md: "Converting a negative float directly to an unsigned integer type is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C6) can produce different results." Cast through int first:

if (SEGMENT.custom1 > 1) hueshift = hueshift + int(audioShift); // float→int (defined), then add to hueshift (int)

3. Global ANIMartRIXMod objects defined in a header without static

ANIMartRIXMod animAudioMon;
ANIMartRIXMod animAudioMon0;

These are object definitions (not declarations) in a header file. If this header were ever included from more than one translation unit, the ODR would be violated and the linker would error. Add static:

static ANIMartRIXMod animAudioMon;
static ANIMartRIXMod animAudioMon0;

(The existing ANIMartRIXMod anim; above them has the same issue and should be corrected consistently.)

4. Effect function writes to a segment parameter

SEGMENT.custom3 = min(SEGMENT.custom3, uint8_t(NUM_DETECTORS-1));

Effect functions are expected to read segment parameters, not write them. Writing here means any preset or config save taken while mode_AudioMon is active will persist the clamped value, silently overwriting what the user configured. Move the clamping to the read site as a local variable:

unsigned detectorID = min((unsigned)SEGMENT.custom3, NUM_DETECTORS - 1u);
detectorID = (detectorID == 0) ? animartrix_detectorID : detectorID;

🟡 Major

5. Mixed indentation — tabs used instead of 2 spaces

Per cpp.instructions.md: "Indent with 2 spaces (no tabs in C++ files)." Several new blocks use hard tabs instead. Notable offenders:

  • map0() body
  • addToConfig(), readFromConfig(), appendConfigData() bodies (mix of 2-space and tab)
  • Some case arms inside getAudio()'s switch block

6. Missing // WLEDMM markers

Per cpp.instructions.md, WLED-MM-specific additions must be marked. The entire new audio-processing pipeline, the new config lifecycle methods, and the new mode_AudioMon function are unmarked. At minimum, key entry points should carry a // WLEDMM: comment explaining what was added and why.

7. #include <vector> in wled00/colors.h with no usage

`#include` <vector>

std::vector is not used anywhere in colors.h. On ESP32, pulling in <vector> through a utility header adds STL allocator overhead and increases compilation time. Remove it.

8. #if 0 dead-code blocks

There are three dead blocks:

  • colors.h: the CRGBW struct block
  • colors.h: colorHStoRGB() / fast_color_scale() at the bottom
  • usermod_v2_animartrix.h: the two-argument map0() overload

Per the guidelines, dead code must be justified or removed. If these are intended future additions, add a // TODO(WLEDMM): comment with a brief explanation; otherwise remove them to keep the PR diff clean and reduce maintenance burden.


🔵 Minor

9. Missing function-level comments on non-trivial functions

Per cpp.instructions.md: "Every non-trivial function should have a brief comment above it." The following new public/package-visible functions lack any doc comment: handleAudioHUE(), enhanceContrast(), enhanceBrightness(), applyGamma24(), processColor(), initMonitor(), getAudio() (the core detection function), processAudio().

10. #define ANIMARTRIX_UI_CONTROLS / ANIMARTRIX_UI_MONITOR naming

These two macros are fine as macros (string concatenation requires #define). However, NUM_DETECTORS and the DET_* constants are pure integer values and should be constexpr/enum as noted in item 1.

11. cycle_hue HUE time formula — large intermediate lost to truncation

hueshift = (tt << 4) | (tt & 0x0F); // default shift based on time

tt is unsigned (from strip.now, which is uint32_t). tt << 4 shifts a 32-bit value left by 4, and then ORs in the bottom 4 bits. The result is essentially tt * 16 | (tt & 0xF) — which is the same as just tt * 16 since (tt & 0xF) is already present in bits 4-7 after the shift. Confirm whether the | (tt & 0x0F) part is intentional or a remnant.

12. static keyword on file-scope globals in header (existing anim)

ANIMartRIXMod anim;  // pre-existing

This should also be static to match the fix for animAudioMon / animAudioMon0 (item 3).


wled00/colors.h

13. rgb2hsvint32_t intermediates for hue are correct but undocumented

if (maxval == r) hsv.h = (10923 * (g - b)) / delta;

g - b can be negative, which wraps the uint16_t assignment by design (2's complement). This is correct but non-obvious. A brief comment here would help reviewers and future maintainers understand that negative intermediate values are intentional.

14. CHSV32 raw member layout depends on compiler struct padding

The union { struct { uint16_t h; uint8_t s; uint8_t v; }; uint32_t raw; } assumes h occupies the low 16 bits of raw. On a little-endian system (all ESP32 variants) this is correct, but a static_assert would make this explicit and catch any future cross-platform issue:

static_assert(sizeof(CHSV32) == 4, "CHSV32 layout mismatch");

Summary table

# Severity File Issue
1 🔴 Critical animartrix.h #define macros inside class scope → global pollution
2 🔴 Critical animartrix.h unsigned(audioShift) float→unsigned UB on RISC-V
3 🔴 Critical animartrix.h animAudioMon / animAudioMon0 not static in header
4 🔴 Critical animartrix.h Effect writes to SEGMENT.custom3
5 🟡 Major animartrix.h Tab indentation instead of 2-space
6 🟡 Major animartrix.h Missing // WLEDMM markers on new code
7 🟡 Major colors.h #include <vector> unused
8 🟡 Major both #if 0 dead-code blocks without justification
9 🔵 Minor animartrix.h Missing doc comments on non-trivial new functions
10 🔵 Minor animartrix.h DET_* integer macros → prefer enum/constexpr
11 🔵 Minor animartrix.h hueshift shift-OR formula may be unintentional
12 🔵 Minor animartrix.h anim global also needs static
13 🔵 Minor colors.h Negative HUE intermediate wrap deserves a comment
14 🔵 Minor colors.h Missing static_assert for CHSV32 layout

Items 1–4 should be resolved before merge; items 5–8 are strongly recommended; 9–14 are polish.


🧠 Learnings used
Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h:155-157
Timestamp: 2025-10-22T21:50:25.839Z
Learning: In WLED-MM PR `#270`, only Effect (Mode) IDs are migrated to 16-bit; Palette IDs and counts remain 8-bit. Usermod code should widen mode-related indices/loops to uint16_t while keeping palette-related indices/loops as uint8_t.

Learnt from: DedeHai
Repo: MoonModules/WLED-MM PR: 253
File: usermods/audioreactive/audio_source.h:1256-1261
Timestamp: 2025-07-12T04:20:14.546Z
Learning: In WLED AudioReactive usermod, DC offset removal optimization in DMAadcSource::getSamples() should be implemented after PR `#248` merges, which will change the sample buffer from float to int16. This eliminates the need for temporary integer buffers and allows direct integer arithmetic on the native buffer type for better memory efficiency on resource-constrained MCUs like C3 and S2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants