diff --git a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h index f74be4d7b2..28a8ef5abd 100644 --- a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h +++ b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h @@ -1,6 +1,8 @@ #pragma once #include "wled.h" +#include "colors.h" // include CHSV32 class by @dedehai +#include "fcn_declare.h" // utilities #ifdef _MoonModules_WLED_ // WLEDMM: use faster math approximations - up to 40% faster @@ -68,65 +70,240 @@ #warning WLEDMM usermod: CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! //======================================================================================================================== +#define ANIMARTRIX_UI_MONITOR "Speed(nothing),HUE Change(nothing),Audio Strength,Audio Decay,Audio Detector ID,Amplify,Amplify,;!,!;!;1f;c1=255,c2=32,c3=0,o1=0,o2=0" +static const char _data_FX_mode_AudioMon1D[] PROGMEM = "Y💡AudioMonitor 1D ☾@" ANIMARTRIX_UI_MONITOR; + +#define ANIMARTRIX_UI_CONTROLS "Speed,HUE Change,Audio Strength,Audio Decay,,cycle HUE,boost Brightness,boost Contrast;;1;2f;c1=0,c2=32,o2=0" + +static const char _data_FX_mode_Module_Experiment10[] PROGMEM = "Y💡Module_Experiment10 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment9[] PROGMEM = "Y💡Module_Experiment9 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment8[] PROGMEM = "Y💡Module_Experiment8 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment7[] PROGMEM = "Y💡Module_Experiment7 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment6[] PROGMEM = "Y💡Module_Experiment6 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment5[] PROGMEM = "Y💡Module_Experiment5 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment4[] PROGMEM = "Y💡Module_Experiment4 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Zoom2[] PROGMEM = "Y💡Zoom2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment3[] PROGMEM = "Y💡Module_Experiment3 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment2[] PROGMEM = "Y💡Module_Experiment2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Module_Experiment1[] PROGMEM = "Y💡Module_Experiment1 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Parametric_Water[] PROGMEM = "Y💡Parametric_Water ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Water[] PROGMEM = "Y💡Water ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido_6[] PROGMEM = "Y💡Complex_Kaleido_6 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido_5[] PROGMEM = "Y💡Complex_Kaleido_5 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido_4[] PROGMEM = "Y💡Complex_Kaleido_4 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido_3[] PROGMEM = "Y💡Complex_Kaleido_3 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido_2[] PROGMEM = "Y💡Complex_Kaleido_2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Complex_Kaleido[] PROGMEM = "Y💡Complex_Kaleido ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM10[] PROGMEM = "Y💡SM10 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM9[] PROGMEM = "Y💡SM9 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM8[] PROGMEM = "Y💡SM8 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM7[] PROGMEM = "Y💡SM7 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM6[] PROGMEM = "Y💡SM6 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM5[] PROGMEM = "Y💡SM5 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM4[] PROGMEM = "Y💡SM4 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM3[] PROGMEM = "Y💡SM3 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM2[] PROGMEM = "Y💡SM2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_SM1[] PROGMEM = "Y💡SM1 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Big_Caleido[] PROGMEM = "Y💡Big_Caleido ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_RGB_Blobs5[] PROGMEM = "Y💡RGB_Blobs5 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_RGB_Blobs4[] PROGMEM = "Y💡RGB_Blobs4 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_RGB_Blobs3[] PROGMEM = "Y💡RGB_Blobs3 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_RGB_Blobs2[] PROGMEM = "Y💡RGB_Blobs2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_RGB_Blobs[] PROGMEM = "Y💡RGB_Blobs ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Polar_Waves[] PROGMEM = "Y💡Polar_Waves ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Slow_Fade[] PROGMEM = "Y💡Slow_Fade ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Zoom[] PROGMEM = "Y💡Zoom ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Hot_Blob[] PROGMEM = "Y💡Hot_Blob ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Spiralus2[] PROGMEM = "Y💡Spiralus2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Spiralus[] PROGMEM = "Y💡Spiralus ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Yves[] PROGMEM = "Y💡Yves ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Scaledemo1[] PROGMEM = "Y💡Scaledemo1 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Lava1[] PROGMEM = "Y💡Lava1 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Caleido3[] PROGMEM = "Y💡Caleido3 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Caleido2[] PROGMEM = "Y💡Caleido2 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Caleido1[] PROGMEM = "Y💡Caleido1 ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Distance_Experiment[] PROGMEM = "Y💡Distance_Experiment ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Center_Field[] PROGMEM = "Y💡Center_Field ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Waves[] PROGMEM = "Y💡Waves ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Chasing_Spirals[] PROGMEM = "Y💡Chasing_Spirals ☾@" ANIMARTRIX_UI_CONTROLS; +static const char _data_FX_mode_Rotating_Blob[] PROGMEM = "Y💡Rotating_Blob ☾@" ANIMARTRIX_UI_CONTROLS; + +// local utility functions +// -static const char _data_FX_mode_Module_Experiment10[] PROGMEM = "Y💡Module_Experiment10 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment9[] PROGMEM = "Y💡Module_Experiment9 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment8[] PROGMEM = "Y💡Module_Experiment8 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment7[] PROGMEM = "Y💡Module_Experiment7 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment6[] PROGMEM = "Y💡Module_Experiment6 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment5[] PROGMEM = "Y💡Module_Experiment5 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment4[] PROGMEM = "Y💡Module_Experiment4 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Zoom2[] PROGMEM = "Y💡Zoom2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment3[] PROGMEM = "Y💡Module_Experiment3 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment2[] PROGMEM = "Y💡Module_Experiment2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Module_Experiment1[] PROGMEM = "Y💡Module_Experiment1 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Parametric_Water[] PROGMEM = "Y💡Parametric_Water ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Water[] PROGMEM = "Y💡Water ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido_6[] PROGMEM = "Y💡Complex_Kaleido_6 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido_5[] PROGMEM = "Y💡Complex_Kaleido_5 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido_4[] PROGMEM = "Y💡Complex_Kaleido_4 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido_3[] PROGMEM = "Y💡Complex_Kaleido_3 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido_2[] PROGMEM = "Y💡Complex_Kaleido_2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Complex_Kaleido[] PROGMEM = "Y💡Complex_Kaleido ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM10[] PROGMEM = "Y💡SM10 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM9[] PROGMEM = "Y💡SM9 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM8[] PROGMEM = "Y💡SM8 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM7[] PROGMEM = "Y💡SM7 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM6[] PROGMEM = "Y💡SM6 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM5[] PROGMEM = "Y💡SM5 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM4[] PROGMEM = "Y💡SM4 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM3[] PROGMEM = "Y💡SM3 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM2[] PROGMEM = "Y💡SM2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_SM1[] PROGMEM = "Y💡SM1 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Big_Caleido[] PROGMEM = "Y💡Big_Caleido ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_RGB_Blobs5[] PROGMEM = "Y💡RGB_Blobs5 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_RGB_Blobs4[] PROGMEM = "Y💡RGB_Blobs4 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_RGB_Blobs3[] PROGMEM = "Y💡RGB_Blobs3 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_RGB_Blobs2[] PROGMEM = "Y💡RGB_Blobs2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_RGB_Blobs[] PROGMEM = "Y💡RGB_Blobs ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Polar_Waves[] PROGMEM = "Y💡Polar_Waves ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Slow_Fade[] PROGMEM = "Y💡Slow_Fade ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Zoom[] PROGMEM = "Y💡Zoom ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Hot_Blob[] PROGMEM = "Y💡Hot_Blob ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Spiralus2[] PROGMEM = "Y💡Spiralus2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Spiralus[] PROGMEM = "Y💡Spiralus ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Yves[] PROGMEM = "Y💡Yves ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Scaledemo1[] PROGMEM = "Y💡Scaledemo1 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Lava1[] PROGMEM = "Y💡Lava1 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Caleido3[] PROGMEM = "Y💡Caleido3 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Caleido2[] PROGMEM = "Y💡Caleido2 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Caleido1[] PROGMEM = "Y💡Caleido1 ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Distance_Experiment[] PROGMEM = "Y💡Distance_Experiment ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Center_Field[] PROGMEM = "Y💡Center_Field ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Waves[] PROGMEM = "Y💡Waves ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Chasing_Spirals[] PROGMEM = "Y💡Chasing_Spirals ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; -static const char _data_FX_mode_Rotating_Blob[] PROGMEM = "Y💡Rotating_Blob ☾@Speed,,,,,,Gamma Correction;;1;2;o2=0"; +// Attach to audiosource, or fall back to simulateSound +static um_data_t* getAudioDataOrSim() { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + return um_data; +} + +// better "map" that can be used when in_min = out_min = 0. +static inline uint32_t map0(uint32_t val, uint32_t in_max, uint32_t out_max) { + // Fast and accurate (error always below 0.5) + if (in_max == 0) return 0; // avoid division by zero + return ( (val*out_max) + (in_max/2) ) / in_max; // +(in_max/2) for rounding +} +#if 0 // not used yet +// a variant of map0() that handles output ranges not starting at 0 - but still requires val in [0 ... in_max] +static inline int32_t map0(uint32_t val, uint32_t in_max, int32_t out_min, int32_t out_max) { + if (out_min > out_max) std::swap(out_min, out_max); // a hack, just to treat inverted ranges without producing overflows + int range = out_max - out_min; + return int(map0(val, in_max, unsigned(range))) + out_min; +} +#endif +// global settings - shared between ANIMartRIXMod and AnimartrixUsermod +// +static uint8_t animartrix_use_gamma = 1; // default = enabled. Can be disabled to get the "legacy" gamma-free look +#ifdef USERMOD_AUDIOREACTIVE +static uint8_t animartrix_detectorID = 4; // default = bass detector +#else +static uint8_t animartrix_detectorID = 0; // not AR usermod -> default = no audio +#endif + +// ANIMartRIXMod class +// class ANIMartRIXMod:public ANIMartRIX { private: + int hueshift = 0; // static HUE shift (16bit signed); default = neutral bool use_gamma = false; + bool cycle_hue = false; + bool boost_brightness = false; + bool boost_contrast = false; + + // --- audio core ------------------------------------------------------------------------------ + + // ToDo 1: sliders + // * for HUE change amount by audio (handling based on cycle_hue: static shift, or speedup/slowdown) + // * for AUDIO filtering: instant -> 1s decay -> what else? Maybe accumulate changes but use speed limit? + + // TODO 2 + // attach to audiosource + // filter raw audio input + // adjust HUE shift based on audio data + // (maybe) allow to adjust brightness + // (details) compare to audioreactive palettes by @netmindz + + // ToDo 3: user option to configure audio input + // none, peak detection, zcr(major frequency), pressure, volumeSmth, High freqs (fftbin[7-10]), mid freqs (fftbin[4-8]), low freqs (fftbin[0-4]) + + // detection engine + #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) + + // get audio - either soundSim or audioReactive UM + static float getAudio(unsigned detectorID) { + constexpr float M_LOG_256 = 5.54517744f; // log(256) = max value + constexpr float M_LOG_3 = 1.098612289f; // log(3) = minimal value + um_data_t *um_data = getAudioDataOrSim(); + float volumeSmth = *(float*) um_data->u_data[0]; + // int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + uint8_t *fftResult = (uint8_t*) um_data->u_data[2]; + float soundPressure = *(float*) um_data->u_data[9]; + //float agcSensitivity=*(float*) um_data->u_data[10]; + uint16_t zCr = *(uint16_t*) um_data->u_data[11]; + + bool isSilence = volumeSmth < 1.0f; + if (isSilence) return 0.0f; + + // extract audio based on selected detection method + float audioSample = 0.0f; + switch (detectorID) { + case DET_VOLUME: audioSample = volumeSmth; break; + case DET_PRESSURE: audioSample = soundPressure; break; + case DET_ZCR: + // zero crossings count + audioSample = float(zCr); + if (audioSample > 3.0f) + 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: + // low freqs RMS + audioSample = 0.6f * sqrtf(float(fftResult[1])*float(fftResult[1]) // 86 - 129 hz + + float(fftResult[2])*float(fftResult[2])); // 129 - 216 hz + break; + case DET_MID: + // mid freqs RMS with some boost (voices, chorus, some instruments) + audioSample = sqrtf(float(fftResult[5])*float(fftResult[5]) // 430 - 560 hz + + float(fftResult[7])*float(fftResult[7]) // 818 - 1120 hz + + float(fftResult[8])*float(fftResult[8]) // 1120 - 1421 hz + + float(fftResult[10])*float(fftResult[10])) /2.5f; // 1895 - 2412 hz + break; + case DET_HIGH: + // high freqs RMS (hats, pipes, high rattle stuff) + audioSample = sqrtf(float(fftResult[12])*float(fftResult[12]) // 3015 - 3704 hz + + float(fftResult[13])*float(fftResult[13]) // 3704 - 4479 hz + + float(fftResult[15])*float(fftResult[15])) / 2.2f; // 7106 - 9259 hz + break; + case DET_NONE: + // falls through + default: break; + } + return (audioSample > 0.8f) ? audioSample : 0.0f; // clamp silence and underflows + } + + + // filter audio + 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 + } + + // --- audio core end -------------------------------------------------------------------------- + public: + // audio processing, called once per frame per segment + void handleAudioHUE(unsigned detectorID) { + unsigned audioDecay = map0(SEGMENT.custom2, 255, 3500); // 0 (instant) up to 4 seconds + 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 of UI slider values + uint8_t myAudioDecay = 32; // default slider value + uint8_t myAudioStrength = 127; + void initEffect() { if ((SEGENV.call == 0) || (SEGMENT.virtualWidth() != num_x) || (SEGMENT.virtualHeight() != num_y)) { init(SEGMENT.virtualWidth(), SEGMENT.virtualHeight(), false); @@ -137,37 +314,174 @@ class ANIMartRIXMod:public ANIMartRIX { } else { speedFactor = (float) map(SEGMENT.speed, 128, 255, 10, 100) / 10.0f; } - use_gamma = SEGENV.check2; setSpeedFactor(speedFactor); + + use_gamma = animartrix_use_gamma > 0; // from global usermod options + cycle_hue = SEGENV.check1; // from segment checkboxes + boost_brightness = SEGENV.check2; + boost_contrast = SEGENV.check3; + + if (cycle_hue) { + unsigned tt = strip.now; // use strip time as timebase - change to millis() if you see jitter or stuttering + hueshift = (tt << 4) | (tt & 0x0F); // default shift based on time, without speedup or slowdown => one cycle in 4 seconds + if (SEGMENT.intensity > 128) { + // tt = (uint64_t(tt) * (31 + SEGMENT.intensity - 127)) / 32; // => faster up to 4x (128/32) + hueshift = (uint64_t(tt) * (31 + SEGMENT.intensity - 127)) / 2; // try to preserve accuracy: time/32 * 16 => time/2 + } else if (SEGMENT.intensity < 127) { + //tt = (uint64_t(tt) * 22) / (21 + 127 - SEGMENT.intensity); // => slower down to 1/7 (22/148) + hueshift = (uint64_t(tt) * (22*16)) / (21 + 127 - SEGMENT.intensity); // try to preserve accuracy, by embedding "<<4" (*16) into the main multiplication + } + } else { // !cycle_hue + hueshift = (128 - SEGMENT.intensity) * 256; // static HUE shift + } + myAudioStrength = SEGMENT.custom1; + myAudioDecay = SEGMENT.custom2; + handleAudioHUE(animartrix_detectorID); } - void setPixelColor(int x, int y, rgb pixel) override { - uint8_t colR, colG, colB; - if (use_gamma) { - colR = gamma8(pixel.red); colG = gamma8(pixel.green); colB = gamma8(pixel.blue); + // AUDIO DEBUG: simplified variant of initEffect() + inline int getAudioHUE() const { return hueshift; } + void initMonitor() { + #if !defined(WLEDMM_NO_GAMMA) + use_gamma = animartrix_use_gamma > 0; // from global usermod options + #else + use_gamma = false; // ToDO: move to usermod options + #endif + cycle_hue = false; + boost_brightness = false; + boost_contrast = false; + hueshift = 0; + myAudioStrength = SEGMENT.custom1; + myAudioDecay = SEGMENT.custom2; + } + + // enhance middle ranges contrast (S-Function) + static inline float enhanceContrast(float color) { + if (color < 1.0f) return 0.0f; // shortcut for black + #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32P4) + // floating point - faster when FPU is present + float x = color / 255.0f; // normalize to [0, 1] + float y = x * x * (3.0f - 2.0f * x); // smoothstep (S-curve) for contrast + float result = y * 255.0f; // scale back to [0, 255] + return min(max(result, 0.0f), 255.0f); // clamp to allowed range + #else + // fixed point with rounding - faster when no FPU + unsigned xcol = color; + unsigned ycol1 = ((xcol * xcol * 3) +127); // first part: y1 = 255 * (x * x * 3) + unsigned ycol2 = ((xcol * xcol * 2 * xcol) +32512) / 255; // second part: y2 = 255 * (x * x * 2 * x) + if (ycol2 > ycol1) return 0.0f; // catch underflow (overflow can't happen) + else return min(255.0f, float(ycol1 - ycol2) / 255.0f); // clamp to allowed range + #endif + } + + // enhance brightness (sqrt function) + static inline uint_fast8_t enhanceBrightness(float color) { + // square root - provides gentle and jump-free enhancement of lower brightness pixels + if (color < 0.125f) return 0; // shortcut for black + if (color > 255.0f) return 255; // shortcut for max color value + // floating point: slow due to sqrtf() + // return roundf(sqrtf(color/255.0f) * 255.0f); + // fixed point: faster + uint32_t col32 = 0.5f + (255.0f * color); // = ( color / 255 ) * 65025 ; 16bit fixed-point representation of color/255 + return sqrt32_bw(col32); // => equal to sqrt((color / 255) * 255; produces [0-255] output + } + + // gamma correction + static inline uint32_t applyGamma24(uint32_t colIn) { + #ifdef _MoonModules_WLED_ // upstream WLED does not need gamma-correction before setPixelColor + uint8_t colR = gamma8(R(colIn)); + uint8_t colG = gamma8(G(colIn)); + uint8_t colB = gamma8(B(colIn)); + return RGBW32(colR, colG, colB, 0U); + #else + return colIn; // do nothing + #endif + } + + inline uint32_t processColor(rgb pixel) const { + if (boost_contrast) { + // enhance contrast - keep "float" for better color accuracy + pixel.red = enhanceContrast(pixel.red); + pixel.green = enhanceContrast(pixel.green); + pixel.blue = enhanceContrast(pixel.blue); + } + uint32_t colOut; + if (boost_brightness) { + // enhance brightness, convert colors from float to integer + uint8_t colR = enhanceBrightness(pixel.red); + uint8_t colG = enhanceBrightness(pixel.green); + uint8_t colB = enhanceBrightness(pixel.blue); + colOut = RGBW32(colR, colG, colB, 0U); } else { - colR = pixel.red; colG = pixel.green; colB = pixel.blue; + // color conversion only; +0.5f for rounding + uint8_t colR = pixel.red+0.5f; + uint8_t colG = pixel.green+0.5f; + uint8_t colB = pixel.blue+0.5f; + colOut = RGBW32(colR, colG, colB,0U); } - SEGMENT.setPixelColorXY(x, y, RGBW32(colR,colG,colB,0)); + + // experimental: HUE shift + if (cycle_hue || (abs(hueshift) > 255)) { // cycle HUE selected, or manual HUE if at least 1 left/right from center of slider + CHSV32 cc; + rgb2hsv(colOut, cc); + cc.h = cc.h + unsigned(hueshift); // works due to 2's complement + hsv2rgb(cc, colOut); + } + + return use_gamma ? applyGamma24(colOut) : colOut; + } + + void setPixelColor(int x, int y, rgb pixel) override { + SEGMENT.setPixelColorXY(x, y, processColor(pixel)); } void setPixelColor(int index, rgb pixel) override { - uint8_t colR, colG, colB; - if (use_gamma) { - colR = gamma8(pixel.red); colG = gamma8(pixel.green); colB = gamma8(pixel.blue); - } else { - colR = pixel.red; colG = pixel.green; colB = pixel.blue; - } // get x and y, so we can us setPixelColorXY() - faster in WLEDMM int x = index % num_x; int y = index / num_x; - SEGMENT.setPixelColorXY(x,y, RGBW32(colR,colG,colB,0)); + SEGMENT.setPixelColorXY(x,y, processColor(pixel)); } // Add any extra custom effects not part of the ANIMartRIX libary here }; ANIMartRIXMod anim; +ANIMartRIXMod animAudioMon; // second object instance for monitor, to avoid overlaping of audio processing +ANIMartRIXMod animAudioMon0; // used if custom3 (detector) == 0 (use global) +uint16_t mode_AudioMon() { + // debug 1D audio monitor (gravimeter syle) + SEGMENT.custom3 = min(SEGMENT.custom3, uint8_t(NUM_DETECTORS-1)); + unsigned detectorID = SEGMENT.custom3 == 0 ? animartrix_detectorID : SEGMENT.custom3; + float volumeSmth = 0.0f; + + if (SEGMENT.custom3 == 0) { + animAudioMon0.initMonitor(); + SEGMENT.custom2 = anim.myAudioDecay; // steal AudioDecay from main effect + SEGMENT.custom1 = anim.myAudioStrength; // steal from main effect + animAudioMon0.handleAudioHUE(detectorID); + volumeSmth = float(uint16_t(animAudioMon0.getAudioHUE())) / 255.0f; + } else { + animAudioMon.initMonitor(); + animAudioMon.handleAudioHUE(detectorID); + volumeSmth = float(uint16_t(animAudioMon.getAudioHUE())) / 255.0f; + } + if (SEGMENT.check1) volumeSmth *= 1.5f; // amplify by 1.5 for better visibility + if (SEGMENT.check2) volumeSmth *= 1.5f; // amplify again by 1.5 + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + } + SEGMENT.fade_out(253); + + float mySampleAvg = volumeSmth * float(SEGLEN-1) /255.0f; // map to pixels available in current segment + unsigned segmentSampleAvg = (volumeSmth+2) / 4; + int tempsamp = constrain(roundf(mySampleAvg),0,SEGLEN-1); // Keep the sample from overflowing. + for (int i=0; i + +#if !defined(FASTLED_VERSION) // pull in FastLED if we don't have it yet (we need the CRGB type) + #define FASTLED_INTERNAL + #define USE_GET_MILLISECOND_TIMER + #include +#endif + + +#if 0 // WLEDMM not used yet +// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color +// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts +// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB +struct CRGBW { + union { + uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t w; + }; + uint8_t raw[4]; // Access as an array in the order B, G, R, W + }; + + // Default constructor + inline CRGBW() __attribute__((always_inline)) = default; + + // Constructor from a 32-bit color (0xWWRRGGBB) + constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} + + // Constructor with r, g, b, w values + constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} + + // Constructor from CRGB + constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} + + // Access as an array + inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } + + // Assignment from 32-bit color + inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } + + // Assignment from r, g, b, w + inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } + + // Conversion operator to uint32_t + inline operator uint32_t() const __attribute__((always_inline)) { + return color32; + } + /* + // Conversion operator to CRGB + inline operator CRGB() const __attribute__((always_inline)) { + return CRGB(r, g, b); + } + + CRGBW& scale32 (uint8_t scaledown) // 32bit math + { + if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit + uint32_t scale = scaledown + 1; + uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + color32 = rb | wg; + return *this; + }*/ + +}; + +#endif + + +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions - credits @dedehai + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; + +static inline void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) +{ + unsigned int remainder, region, p, q, t; + unsigned int h = hsv.h; + unsigned int s = hsv.s; + unsigned int v = hsv.v; + if (s == 0) { + rgb = v << 16 | v << 8 | v; + return; + } + region = h / 10923; // 65536 / 6 = 10923 + remainder = (h - (region * 10923)) * 6; + p = (v * (255 - s)) >> 8; + q = (v * (255 - ((s * remainder) >> 16))) >> 8; + t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; + switch (region) { + case 0: + rgb = v << 16 | t << 8 | p; break; + case 1: + rgb = q << 16 | v << 8 | p; break; + case 2: + rgb = p << 16 | v << 8 | t; break; + case 3: + rgb = p << 16 | q << 8 | v; break; + case 4: + rgb = t << 16 | p << 8 | v; break; + default: + rgb = v << 16 | p << 8 | q; break; + } +} + +static inline void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version +{ + hsv.raw = 0; + int32_t r = (rgb>>16)&0xFF; + int32_t g = (rgb>>8)&0xFF; + int32_t b = rgb&0xFF; + int32_t minval, maxval, delta; + minval = min(r, g); + minval = min(minval, b); + maxval = max(r, g); + maxval = max(maxval, b); + if (maxval == 0) return; // black + hsv.v = maxval; + delta = maxval - minval; + hsv.s = (255 * delta) / maxval; + if (hsv.s == 0) return; // gray value + if (maxval == r) hsv.h = (10923 * (g - b)) / delta; + else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; + else hsv.h = 43690 + (10923 * (r - g)) / delta; +} + +#if 0 // WLEDMM not used yet +static inline void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb + uint32_t crgb; + hsv2rgb(CHSV32(hue, sat, 255), crgb); + rgb[0] = byte((crgb) >> 16); + rgb[1] = byte((crgb) >> 8); + rgb[2] = byte(crgb); +} + +// fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy +// note: inlining uses less code than actual function calls +static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) { + uint32_t rb = (((c & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; + uint32_t wg = (((c>>8) & 0x00FF00FF) * scale) & ~0x00FF00FF; + return rb | wg; +} +#endif + +#endif // WLED_COLORS_H