diff --git a/platformio.ini b/platformio.ini index ec73bc5658..6340dc9b8d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -258,6 +258,7 @@ lib_deps = esp32async/AsyncTCP @ 3.4.7 bitbank2/AnimatedGIF@^1.4.7 https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e + ${esp32.AR_lib_deps} build_flags = -D CONFIG_ASYNC_TCP_USE_WDT=0 -D CONFIG_ASYNC_TCP_STACK_SIZE=8192 @@ -280,7 +281,7 @@ extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs # additional build flags for audioreactive - must be applied globally AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) -AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility +AR_lib_deps = wled-audioreactive = https://github.com/MoonModules/WLED-AudioReactive-Usermod [esp32_idf_V4] @@ -391,7 +392,7 @@ extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM2D -custom_usermods = audioreactive +custom_usermods = [env:esp8266_2m] board = esp_wroom_02 @@ -419,7 +420,7 @@ board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D -custom_usermods = audioreactive +custom_usermods = [env:esp01_1m_full] board = esp01_1m @@ -449,14 +450,14 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME= ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D -custom_usermods = audioreactive +custom_usermods = [env:esp32dev] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -custom_usermods = audioreactive +custom_usermods = build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} @@ -475,7 +476,7 @@ build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 @@ -491,7 +492,7 @@ board_build.flash_mode = dio board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 @@ -508,7 +509,7 @@ board = esp32-poe platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 @@ -523,7 +524,7 @@ board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.extended_partitions} -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\" -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 @@ -560,7 +561,7 @@ board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\" -D WLED_WATCHDOG_TIMEOUT=0 @@ -582,7 +583,7 @@ board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\" -D WLED_WATCHDOG_TIMEOUT=0 @@ -603,7 +604,7 @@ platform_packages = ${esp32s3.platform_packages} board = esp32s3camlcd ;; this is the only standard board with "opi_opi" board_build.arduino.memory_type = opi_opi upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\" -D WLED_WATCHDOG_TIMEOUT=0 @@ -644,7 +645,7 @@ board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\" -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") @@ -664,7 +665,7 @@ board = lolin_s2_mini board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = qio board_build.f_flash = 80000000L -custom_usermods = audioreactive +custom_usermods = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\" -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp deleted file mode 100644 index d91e1bf2d3..0000000000 --- a/usermods/audioreactive/audio_reactive.cpp +++ /dev/null @@ -1,2081 +0,0 @@ - -#include "wled.h" - -#ifdef ARDUINO_ARCH_ESP32 - -#include -#include - -#endif - -#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) -#include -#endif - -/* - * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality - * - * This is an audioreactive v2 usermod. - * .... - */ - -#if !defined(FFTTASK_PRIORITY) -#define FFTTASK_PRIORITY 1 // standard: looptask prio -//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp -//#define FFTTASK_PRIORITY 4 // above asyc_tcp -#endif - -// Comment/Uncomment to toggle usb serial debugging -// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) -// #define FFT_SAMPLING_LOG // FFT result debugging -// #define SR_DEBUG // generic SR DEBUG messages - -#ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define DEBUGSR_PRINT(x) - #define DEBUGSR_PRINTLN(x) - #define DEBUGSR_PRINTF(x...) -#endif - -#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT.print(x) - #define PLOT_PRINTLN(x) DEBUGOUT.println(x) - #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define PLOT_PRINT(x) - #define PLOT_PRINTLN(x) - #define PLOT_PRINTF(x...) -#endif - -#define MAX_PALETTES 3 - -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group - -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! - -// audioreactive variables -#ifdef ARDUINO_ARCH_ESP32 - #ifndef SR_AGC // Automatic gain control mode - #define SR_AGC 0 // default mode = off - #endif -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample -static uint8_t soundAgc = SR_AGC; // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value) -#endif -//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getFrameTime() -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects - -// TODO: probably best not used by receive nodes -//static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 - -// user settable parameters for limitSoundDynamics() -#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF -static bool limiterOn = false; // bool: enable / disable dynamics limiter -#else -static bool limiterOn = true; -#endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec - -// peak detection -#ifdef ARDUINO_ARCH_ESP32 -static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode -#endif -static void autoResetPeak(void); // peak auto-reset function -static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) - -#ifdef ARDUINO_ARCH_ESP32 - -// use audio source class (ESP32 specific) -#include "audio_source.h" -constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) -constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) - -// globals -static uint8_t inputLevel = 128; // UI slider value -#ifndef SR_SQUELCH - uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) -#else - uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) -#endif -#ifndef SR_GAIN - uint8_t sampleGain = 60; // sample gain (config value) -#else - uint8_t sampleGain = SR_GAIN; // sample gain (config value) -#endif -// user settable options for FFTResult scaling -static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root - -// -// AGC presets -// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" -// -#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy -const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax -const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone -const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone -const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level -const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% -const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) -const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% -const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec -const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs -const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter -const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter -const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) -// AGC presets end - -static AudioSource *audioSource = nullptr; -static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. - -//////////////////// -// Begin FFT Code // -//////////////////// - -// some prototypes, to ensure consistent interfaces -static float fftAddAvg(int from, int to); // average of several FFT result bins -void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results -static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels - -static TaskHandle_t FFT_Task = nullptr; - -// Table of multiplication factors so that we can even out the frequency response. -static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; - -// globals and FFT Output variables shared with animations -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; -#endif - -// FFT Task variables (filtering and post-processing) -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) -#ifdef SR_DEBUG -static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. -#endif - -// audio source parameters and constant -constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms -//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms -//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms -#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling -//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling -//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling - -// FFT Constants -constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 -constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. -// the following are observed values, supported by a bit of "educated guessing" -//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels -#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels -#define LOG_256 5.54517744f // log(256) - -// These are the input and output vectors. Input vectors receive computed results from FFT. -static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins -static float* vImag = nullptr; // imaginary parts - -// Create FFT object -// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 -// these options actually cause slow-downs on all esp32 processors, don't use them. -// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 -// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 -// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() -// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags - -#include // FFT object is created in FFTcode -// Helper functions - -// compute average of several FFT result bins -static float fftAddAvg(int from, int to) { - float result = 0.0f; - for (int i = from; i <= to; i++) { - result += vReal[i]; - } - return result / float(to - from + 1); -} - -// -// FFT main task -// -void FFTcode(void * parameter) -{ - DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); - - // allocate FFT buffers on first call - if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float)); - if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float)); - if ((vReal == nullptr) || (vImag == nullptr)) { - // something went wrong - if (vReal) free(vReal); vReal = nullptr; - if (vImag) free(vImag); vImag = nullptr; - return; - } - // Create FFT object with weighing factor storage - ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); - - // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; - - TickType_t xLastWakeTime = xTaskGetTickCount(); - for(;;) { - delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. - // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. - - // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - continue; - } - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - uint64_t start = esp_timer_get_time(); - bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid -#endif - - // get a fresh batch of samples from I2S - if (audioSource) audioSource->getSamples(vReal, samplesFFT); - memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0 - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (start < esp_timer_get_time()) { // filter out overflows - uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding - sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth - } - start = esp_timer_get_time(); // start measuring FFT time -#endif - - xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay - - // band pass filter - can reduce noise floor by a factor of 50 - // downside: frequencies below 100Hz will be ignored - if (useBandPassFilter) runMicFilter(samplesFFT, vReal); - - // find highest sample in the batch - float maxSample = 0.0f; // max sample from FFT batch - for (int i=0; i < samplesFFT; i++) { - // pick our our current mic sample - we take the max value from all samples that go into FFT - if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts - if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); - } - // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function - // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. - micDataReal = maxSample; - -#ifdef SR_DEBUG - if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization -#else - if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. -#endif - - // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) - FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy - //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection - FFT.compute( FFTDirection::Forward ); // Compute FFT - FFT.complexToMagnitude(); // Compute magnitudes - vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. - - FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - haveDoneFFT = true; -#endif - - } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, samplesFFT * sizeof(float)); - FFT_MajorPeak = 1; - FFT_Magnitude = 0.001; - } - - for (int i = 0; i < samplesFFT; i++) { - float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way - vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. - } // for() - - // mapping of FFT result bins to frequency channels - if (fabsf(sampleAvg) > 0.5f) { // noise gate open -#if 0 - /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. - * - * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. - * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. - * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. - * End frequency = Start frequency * multiplier ^ 16 - * Multiplier = (End frequency/ Start frequency) ^ 1/16 - * Multiplier = 1.320367784 - */ // Range - fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate -#else - /* new mapping, optimized for 22050 Hz by softhack007 */ - // bins frequency range - if (useBandPassFilter) { - // skip frequencies below 100hz - fftCalc[ 0] = 0.8f * fftAddAvg(3,4); - fftCalc[ 1] = 0.9f * fftAddAvg(4,5); - fftCalc[ 2] = fftAddAvg(5,6); - fftCalc[ 3] = fftAddAvg(6,7); - // don't use the last bins from 206 to 255. - fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass - fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass - fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass - fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - } - fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange - fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange - fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange - fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange - fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange - fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid - fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid - fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid - fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping -#endif - } else { // noise gate closed - just decay old values - for (int i=0; i < NUM_GEQ_CHANNELS; i++) { - fftCalc[i] *= 0.85f; // decay to zero - if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; - } - } - - // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) - postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows - uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth - } -#endif - // run peak detection - autoResetPeak(); - detectSamplePeak(); - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC - #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - - } // for(;;)ever -} // FFTcode() task end - - -/////////////////////////// -// Pre / Postprocessing // -/////////////////////////// - -static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) -{ - // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency - //constexpr float alpha = 0.04f; // 150Hz - //constexpr float alpha = 0.03f; // 110Hz - constexpr float alpha = 0.0225f; // 80hz - //constexpr float alpha = 0.01693f;// 60hz - // high frequency cutoff parameter - //constexpr float beta1 = 0.75f; // 11Khz - //constexpr float beta1 = 0.82f; // 15Khz - //constexpr float beta1 = 0.8285f; // 18Khz - constexpr float beta1 = 0.85f; // 20Khz - - constexpr float beta2 = (1.0f - beta1) / 2.0f; - static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter - static float lowfilt = 0.0f; // IIR low frequency cutoff filter - - for (int i=0; i < numSamples; i++) { - // FIR lowpass, to remove high frequency noise - float highFilteredSample; - if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes - else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array - last_vals[1] = last_vals[0]; - last_vals[0] = sampleBuffer[i]; - sampleBuffer[i] = highFilteredSample; - // IIR highpass, to remove low frequency noise - lowfilt += alpha * (sampleBuffer[i] - lowfilt); - sampleBuffer[i] = sampleBuffer[i] - lowfilt; - } -} - -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels -{ - for (int i=0; i < numberOfChannels; i++) { - - if (noiseGateOpen) { // noise gate open - // Adjustment for frequency curves. - fftCalc[i] *= fftResultPink[i]; - if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function - // Manual linear adjustment of gain using sampleGain adjustment for different input types. - fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment - if(fftCalc[i] < 0) fftCalc[i] = 0; - } - - // smooth results - rise fast, fall slower - if(fftCalc[i] > fftAvg[i]) // rise fast - fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] - else { // fall slow - if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero - else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero - } - // constrain internal vars - just to be sure - fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); - fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); - - float currentResult; - if(limiterOn == true) - currentResult = fftAvg[i]; - else - currentResult = fftCalc[i]; - - switch (FFTScalingMode) { - case 1: - // Logarithmic scaling - currentResult *= 0.42f; // 42 is the answer ;-) - currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks - if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function - else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined - currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] - break; - case 2: - // Linear scaling - currentResult *= 0.30f; // needs a bit more damping, get stay below 255 - currentResult -= 4.0f; // giving a bit more room for peaks - if (currentResult < 1.0f) currentResult = 0.0f; - currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies - break; - case 3: - // square root scaling - currentResult *= 0.38f; - currentResult -= 6.0f; - if (currentResult > 1.0f) currentResult = sqrtf(currentResult); - else currentResult = 0.0f; // special handling, because sqrt(0) = undefined - currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] - break; - - case 0: - default: - // no scaling - leave freq bins as-is - currentResult -= 4; // just a bit more room for peaks - break; - } - - // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. - if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user - float post_gain = (float)inputLevel/128.0f; - if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; - currentResult *= post_gain; - } - fftResult[i] = constrain((int)currentResult, 0, 255); - } -} -//////////////////// -// Peak detection // -//////////////////// - -// peak detection is called from FFT task when vReal[] contains valid FFT results -static void detectSamplePeak(void) { - bool havePeak = false; - // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. - // Poor man's beat detection by seeing if sample > Average + some value. - // This goes through ALL of the 255 bins - but ignores stupid settings - // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. - if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { - havePeak = true; - } - - if (havePeak) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } -} - -#endif - -static void autoResetPeak(void) { - uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime()); - if (millis() - timeOfPeak > peakDelay) { // Auto-reset of samplePeak after at least one complete frame has passed. - samplePeak = false; - if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData - } -} - - -//////////////////// -// usermod class // -//////////////////// - -//class name. Use something descriptive and leave the ": public Usermod" part :) -class AudioReactive : public Usermod { - - private: -#ifdef ARDUINO_ARCH_ESP32 - - #ifndef AUDIOPIN - int8_t audioPin = -1; - #else - int8_t audioPin = AUDIOPIN; - #endif - #ifndef SR_DMTYPE // I2S mic type - uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S - #define SR_DMTYPE 1 // default type = I2S - #else - uint8_t dmType = SR_DMTYPE; - #endif - #ifndef I2S_SDPIN // aka DOUT - int8_t i2ssdPin = 32; - #else - int8_t i2ssdPin = I2S_SDPIN; - #endif - #ifndef I2S_WSPIN // aka LRCL - int8_t i2swsPin = 15; - #else - int8_t i2swsPin = I2S_WSPIN; - #endif - #ifndef I2S_CKPIN // aka BCLK - int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ - #else - int8_t i2sckPin = I2S_CKPIN; - #endif - #ifndef MCLK_PIN - int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ - #else - int8_t mclkPin = MCLK_PIN; - #endif -#endif - - // new "V2" audiosync struct - 44 Bytes - struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps - char header[6]; // 06 Bytes offset 0 - uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet - float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes offset 18 - uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet - float FFT_Magnitude; // 04 Bytes offset 36 - float FFT_MajorPeak; // 04 Bytes offset 40 - }; - - // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility - struct audioSyncPacket_v1 { - char header[6]; // 06 Bytes - uint8_t myVals[32]; // 32 Bytes - int sampleAgc; // 04 Bytes - int sampleRaw; // 04 Bytes - float sampleAvg; // 04 Bytes - bool samplePeak; // 01 Bytes - uint8_t fftResult[16]; // 16 Bytes - double FFT_Magnitude; // 08 Bytes - double FFT_MajorPeak; // 08 Bytes - }; - - #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync - - // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) - #ifdef UM_AUDIOREACTIVE_ENABLE - bool enabled = true; - #else - bool enabled = false; - #endif - - bool initDone = false; - bool addPalettes = false; - int8_t palettes = 0; - - // variables for UDP sound sync - WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) - unsigned long lastTime = 0; // last time of running UDP Microphone Sync - const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED - uint16_t audioSyncPort= 11988;// default port for UDP sound sync - - bool updateIsRunning = false; // true during OTA. - -#ifdef ARDUINO_ARCH_ESP32 - // used for AGC - int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) - double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error - - - // variables used by getSample() and agcAvg() - int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. - double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller - float expAdjF = 0.0f; // Used for exponential filter. - float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. - int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) - int16_t rawSampleAgc = 0; // not smoothed AGC sample -#endif - - // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample - int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc - float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc - - // used to feed "Info" Page - unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket - int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) - float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds - unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset - #define CYCLE_SAMPLEMAX 3500 // time window for merasuring - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _config[]; - static const char _dynamics[]; - static const char _frequency[]; - static const char _inputLvl[]; -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - static const char _analogmic[]; -#endif - static const char _digitalmic[]; - static const char _addPalettes[]; - static const char UDP_SYNC_HEADER[]; - static const char UDP_SYNC_HEADER_v1[]; - - // private methods - void removeAudioPalettes(void); - void createAudioPalettes(void); - CRGB getCRGBForBand(int x, int pal); - void fillAudioPalettes(void); - - //////////////////// - // Debug support // - //////////////////// - void logAudio() - { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable - #ifdef MIC_LOGGER - // Debugging functions for audio input and sound processing. Comment out the values you want to see - PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); - #ifdef ARDUINO_ARCH_ESP32 - //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); - //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); - //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); - #endif - PLOT_PRINTLN(); - #endif - - #ifdef FFT_SAMPLING_LOG - #if 0 - for(int i=0; i maxVal) maxVal = fftResult[i]; - if(fftResult[i] < minVal) minVal = fftResult[i]; - } - for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { - PLOT_PRINT(i); PLOT_PRINT(":"); - PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); - } - if(printMaxVal) { - PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); - } - if(printMinVal) { - PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter - } - if(mapValuesToPlotterSpace) - PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis - else { - PLOT_PRINTF("max:%04d ", 256); - } - PLOT_PRINTLN(); - #endif // FFT_SAMPLING_LOG - } // logAudio() - - -#ifdef ARDUINO_ARCH_ESP32 - ////////////////////// - // Audio Processing // - ////////////////////// - - /* - * A "PI controller" multiplier to automatically adjust sound sensitivity. - * - * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: - * 0. don't amplify anything below squelch (but keep previous gain) - * 1. gain input = maximum signal observed in the last 5-10 seconds - * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal - * 3. the amplification depends on signal level: - * a) normal zone - very slow adjustment - * b) emergency zone (<10% or >90%) - very fast adjustment - */ - void agcAvg(unsigned long the_time) - { - const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - - float lastMultAgc = multAgc; // last multiplier used - float multAgcTemp = multAgc; // new multiplier - float tmpAgc = sampleReal * multAgc; // what-if amplified signal - - float control_error; // "control error" input for PI control - - if (last_soundAgc != soundAgc) - control_integrated = 0.0; // new preset - reset integrator - - // For PI controller, we need to have a constant "frequency" - // so let's make sure that the control loop is not running at insane speed - static unsigned long last_time = 0; - unsigned long time_now = millis(); - if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock - - if (time_now - last_time > 2) { - last_time = time_now; - - if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0)) { - // MIC signal is "squelched" - deliver silence - tmpAgc = 0; - // we need to "spin down" the intgrated error buffer - if (fabs(control_integrated) < 0.01) control_integrated = 0.0; - else control_integrated *= 0.91; - } else { - // compute new setpoint - if (tmpAgc <= agcTarget0Up[AGC_preset]) - multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint - else - multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint - } - // limit amplification - if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - - // compute error terms - control_error = multAgcTemp - lastMultAgc; - - if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping - && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) - control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping - else - control_integrated *= 0.9; // spin down that beasty integrator - - // apply PI Control - tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain - if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower energy zone - multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; - multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; - } else { // "normal zone" - multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; - multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; - } - - // limit amplification again - PI controller sometimes "overshoots" - //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 - if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - } - - // NOW finally amplify the signal - tmpAgc = sampleReal * multAgcTemp; // apply gain to signal - if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold - //tmpAgc = constrain(tmpAgc, 0, 255); - if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit - if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure - - // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc - multAgc = multAgcTemp; - rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; - // update smoothed AGC sample - if (fabsf(tmpAgc) < 1.0f) - sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero - else - sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path - - sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value - last_soundAgc = soundAgc; - } // agcAvg() - - // post-processing and filtering of MIC sample (micDataReal) from FFTcode() - void getSample() - { - float sampleAdj; // Gain adjusted sample value - float tmpSample; // An interim sample variable used for calculations. - const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. - const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - - #ifdef WLED_DISABLE_SOUND - micIn = perlin8(millis(), millis()); // Simulated analog read - micDataReal = micIn; - #else - #ifdef ARDUINO_ARCH_ESP32 - micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; - #else - // this is the minimal code for reading analog mic input on 8266. - // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. - static unsigned long lastAnalogTime = 0; - static float lastAnalogValue = 0.0f; - if (millis() - lastAnalogTime > 20) { - micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. - lastAnalogTime = millis(); - lastAnalogValue = micDataReal; - yield(); - } else micDataReal = lastAnalogValue; - micIn = int(micDataReal); - #endif - #endif - - micLev += (micDataReal-micLev) / 12288.0f; - if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal - - micIn -= micLev; // Let's center it to 0 now - // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. - float micInNoDC = fabsf(micDataReal - micLev); - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); - expAdjF = fabsf(expAdjF); // Now (!) take the absolute value - - expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate - if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0" - - tmpSample = expAdjF; - micIn = abs(micIn); // And get the absolute value of each sample - - sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment - sampleReal = tmpSample; - - sampleAdj = fmax(fmin(sampleAdj, 255), 0); // Question: why are we limiting the value to 8 bits ??? - sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! - - // keep "peak" sample, but decay value if current sample is below peak - if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { - sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax); // new peak - with some filtering - // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume - if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } - } else { - if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) - sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly - else - sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec - } - if (sampleMax < 0.5f) sampleMax = 0.0f; - - sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. - sampleAvg = fabsf(sampleAvg); // make sure we have a positive value - } // getSample() - -#endif - - /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). - * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) - */ - // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) - void limitSampleDynamics(void) { - const float bigChange = 196; // just a representative number - a large, expected sample value - static unsigned long last_time = 0; - static float last_volumeSmth = 0.0f; - - if (limiterOn == false) return; - - long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up - float deltaSample = volumeSmth - last_volumeSmth; - - if (attackTime > 0) { // user has defined attack time > 0 - float maxAttack = bigChange * float(delta_time) / float(attackTime); - if (deltaSample > maxAttack) deltaSample = maxAttack; - } - if (decayTime > 0) { // user has defined decay time > 0 - float maxDecay = - bigChange * float(delta_time) / float(decayTime); - if (deltaSample < maxDecay) deltaSample = maxDecay; - } - - volumeSmth = last_volumeSmth + deltaSample; - - last_volumeSmth = volumeSmth; - last_time = millis(); - } - - - ////////////////////// - // UDP Sound Sync // - ////////////////////// - - // try to establish UDP sound sync connection - void connectUDPSoundSync(void) { - // This function tries to establish a UDP sync connection if needed - // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection - static unsigned long last_connection_attempt = 0; - - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled - if (udpSyncConnected) return; // already connected - if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable - if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; - - // if we arrive here, we need a UDP connection but don't have one - last_connection_attempt = millis(); - connected(); // try to start UDP - } - -#ifdef ARDUINO_ARCH_ESP32 - void transmitAudioData() - { - if (!udpSyncConnected) return; - //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); - - audioSyncPacket transmitData; - memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized - - strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); - // transmit samples that were not modified by limitSampleDynamics() - transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; - transmitData.samplePeak = udpSamplePeak ? 1:0; - udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); - } - - transmitData.FFT_Magnitude = my_magnitude; - transmitData.FFT_MajorPeak = FFT_MajorPeak; - - if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - } - return; - } // transmitAudioData() - -#endif - - static bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; - } - static bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; - } - - void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket receivedPacket; - memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean - memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# - - // update samples for effects - volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = volumeRaw; - sampleAvg = volumeSmth; - rawSampleAgc = volumeRaw; - sampleAgc = volumeSmth; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket.samplePeak >0 ? true:false; - if (samplePeak) timeOfPeak = millis(); - //userVar1 = samplePeak; - } - //These values are only computed by ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; - my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - } - - void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { - audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); - // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); - volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); - sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; - sampleAgc = volumeSmth; - rawSampleAgc = volumeRaw; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket->samplePeak >0 ? true:false; - if (samplePeak) timeOfPeak = millis(); - //userVar1 = samplePeak; - } - //These values are only available on the ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0, 11025.0); // restrict value to range expected by effects - } - - bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. - { - if (!udpSyncConnected) return false; - bool haveFreshData = false; - - size_t packetSize = fftUdp.parsePacket(); -#ifdef ARDUINO_ARCH_ESP32 - if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 -#endif - if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { - //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays - fftUdp.read(fftBuff, packetSize); - - // VERIFY THAT THIS IS A COMPATIBLE PACKET - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { - decodeAudioData(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; - receivedFormat = 2; - } else { - if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { - decodeAudioData_v1(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); - haveFreshData = true; - receivedFormat = 1; - } else receivedFormat = 0; // unknown format - } - } - return haveFreshData; - } - - - ////////////////////// - // usermod functions// - ////////////////////// - - public: - //Functions called by WLED or other usermods - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - * It is called *AFTER* readFromConfig() - */ - void setup() override - { - disableSoundProcessing = true; // just to be sure - if (!initDone) { - // usermod exchangeable data - // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers - um_data = new um_data_t; - um_data->u_size = 8; - um_data->u_type = new um_types_t[um_data->u_size]; - um_data->u_data = new void*[um_data->u_size]; - um_data->u_data[0] = &volumeSmth; //*used (New) - um_data->u_type[0] = UMT_FLOAT; - um_data->u_data[1] = &volumeRaw; // used (New) - um_data->u_type[1] = UMT_UINT16; - um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) - um_data->u_type[2] = UMT_BYTE_ARR; - um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[3] = UMT_BYTE; - um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) - um_data->u_type[4] = UMT_FLOAT; - um_data->u_data[5] = &my_magnitude; // used (New) - um_data->u_type[5] = UMT_FLOAT; - um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[6] = UMT_BYTE; - um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[7] = UMT_BYTE; - } - - -#ifdef ARDUINO_ARCH_ESP32 - - // Reset I2S peripheral for good measure - i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed - #if !defined(CONFIG_IDF_TARGET_ESP32C3) - delay(100); - periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 - #endif - delay(100); // Give that poor microphone some time to setup. - - useBandPassFilter = false; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone - #endif - - switch (dmType) { - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - // stub cases for not-yet-supported I2S modes on other ESP32 chips - case 0: //ADC analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: //PDM Microphone - #endif - #endif - case 1: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 2: - DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); - audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 3: - DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 4: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: - DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); - break; - #endif - case 6: - DEBUGSR_PRINTLN(F("AR: ES8388 Source")); - audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - // ADC over I2S is only possible on "classic" ESP32 - case 0: - DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); - audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog - if (audioSource) audioSource->initialize(audioPin); - break; - #endif - - case 254: // dummy "network receive only" mode - if (audioSource) delete audioSource; audioSource = nullptr; - disableSoundProcessing = true; - audioSyncEnabled = 2; // force udp sound receive mode - enabled = true; - break; - - case 255: // 255 = -1 = no audio source - // falls through to default - default: - if (audioSource) delete audioSource; audioSource = nullptr; - disableSoundProcessing = true; - enabled = false; - break; - } - delay(250); // give microphone enough time to initialise - - if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise -#endif - if (enabled) onUpdateBegin(false); // create FFT task, and initialize network - - -#ifdef ARDUINO_ARCH_ESP32 - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif - disableSoundProcessing = true; - } -#endif - if (enabled) disableSoundProcessing = false; // all good - enable audio processing - if (enabled) connectUDPSoundSync(); - if (enabled && addPalettes) createAudioPalettes(); - initDone = true; - } - - - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() override - { - if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection - udpSyncConnected = false; - fftUdp.stop(); - } - - if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifdef ARDUINO_ARCH_ESP32 - udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); - #else - udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); - #endif - } - } - - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() override - { - static unsigned long lastUMRun = millis(); - - if (!enabled) { - disableSoundProcessing = true; // keep processing suspended (FFT task) - lastUMRun = millis(); // update time keeping - return; - } - // We cannot wait indefinitely before processing audio data - if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice - - // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) - if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed - &&( (realtimeMode == REALTIME_MODE_GENERIC) - ||(realtimeMode == REALTIME_MODE_E131) - ||(realtimeMode == REALTIME_MODE_UDP) - ||(realtimeMode == REALTIME_MODE_ADALIGHT) - ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed - { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - disableSoundProcessing = true; - } else { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping - disableSoundProcessing = false; - } - - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode -#ifdef ARDUINO_ARCH_ESP32 - if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source - - - // Only run the sampling code IF we're not in Receive mode or realtime mode - if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { - if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) - - unsigned long t_now = millis(); // remember current time - int userloopDelay = int(t_now - lastUMRun); - if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. - - #ifdef WLED_DEBUG - // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS - //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); - //} - #endif - - // run filters, and repeat in case of loop delays (hick-up compensation) - if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem - if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs - do { - getSample(); // run microphone sampling filters - agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg - userloopDelay -= 2; // advance "simulated time" by 2ms - } while (userloopDelay > 0); - lastUMRun = t_now; // update time keeping - - // update samples for effects (raw, smooth) - volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; - volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - // update FFTMagnitude, taking into account AGC amplification - my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects - if (soundAgc) my_magnitude *= multAgc; - if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute - - limitSampleDynamics(); - } // if (!disableSoundProcessing) -#endif - - autoResetPeak(); // auto-reset sample peak after strip minShowDelay - if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected - - connectUDPSoundSync(); // ensure we have a connection - if needed - - // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { - // Only run the audio listener code if we're in Receive mode - static float syncVolumeSmth = 0; - bool have_new_sample = false; - if (millis() - lastTime > delayMs) { - have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); -#ifdef ARDUINO_ARCH_ESP32 - else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. -#endif - lastTime = millis(); - } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups - } - - #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) - static unsigned long lastMicLoggerTime = 0; - if (millis()-lastMicLoggerTime > 20) { - lastMicLoggerTime = millis(); - logAudio(); - } - #endif - - // Info Page: keep max sample from last 5 seconds -#ifdef ARDUINO_ARCH_ESP32 - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing - if (sampleAvg < 1) maxSample5sec = 0; // noise gate - } else { - if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume - } -#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing - if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate - if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values - } else { - if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { - // Only run the transmit code IF we're in Transmit mode - transmitAudioData(); - lastTime = millis(); - } -#endif - - fillAudioPalettes(); - } - - - bool getUMData(um_data_t **data) override - { - if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit - *data = um_data; - return true; - } - -#ifdef ARDUINO_ARCH_ESP32 - void onUpdateBegin(bool init) override - { -#ifdef WLED_DEBUG - fftTime = sampleTime = 0; -#endif - // gracefully suspend FFT task (if running) - disableSoundProcessing = true; - - // reset sound data - micDataReal = 0.0f; - volumeRaw = 0; volumeSmth = 0; - sampleAgc = 0; sampleAvg = 0; - sampleRaw = 0; rawSampleAgc = 0; - my_magnitude = 0; FFT_Magnitude = 0; FFT_MajorPeak = 1; - multAgc = 1; - // reset FFT data - memset(fftCalc, 0, sizeof(fftCalc)); - memset(fftAvg, 0, sizeof(fftAvg)); - memset(fftResult, 0, sizeof(fftResult)); - for(int i=(init?0:1); i don't process audio - updateIsRunning = init; - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - /** - * handleButton() can be used to override default button behaviour. Returning true - * will prevent button working in a default way. - */ - bool handleButton(uint8_t b) override { - yield(); - // crude way of determining if audio input is analog - // better would be for AudioSource to implement getType() - if (enabled - && dmType == 0 && audioPin>=0 - && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) - ) { - return true; - } - return false; - } - -#endif - //////////////////////////// - // Settings and Info Page // - //////////////////////////// - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - void addToJsonInfo(JsonObject& root) override - { -#ifdef ARDUINO_ARCH_ESP32 - char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 -#endif - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - - String uiDomString = F(""); - infoArr.add(uiDomString); - - if (enabled) { -#ifdef ARDUINO_ARCH_ESP32 - // Input Level Slider - if (disableSoundProcessing == false) { // only show slider when audio processing is running - if (soundAgc > 0) { - infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies - } else { - infoArr = user.createNestedArray(F("Audio Input Level")); - } - uiDomString = F("
"); // - infoArr.add(uiDomString); - } -#endif - // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG - - // current Audio input - infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { - // UDP sound sync - receive mode - infoArr.add(F("UDP sound sync")); - if (udpSyncConnected) { - if (millis() - last_UDPTime < 2500) - infoArr.add(F(" - receiving")); - else - infoArr.add(F(" - idle")); - } else { - infoArr.add(F(" - no connection")); - } -#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 - } else { - infoArr.add(F("sound sync Off")); - } -#else // ESP32 only - } else { - // Analog or I2S digital input - if (audioSource && (audioSource->isInitialized())) { - // audio source successfully configured - if (audioSource->getType() == AudioSource::Type_I2SAdc) { - infoArr.add(F("ADC analog")); - } else { - infoArr.add(F("I2S digital")); - } - // input level or "silence" - if (maxSample5sec > 1.0f) { - float my_usage = 100.0f * (maxSample5sec / 255.0f); - snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); - infoArr.add(myStringBuffer); - } else { - infoArr.add(F(" - quiet")); - } - } else { - // error during audio source setup - infoArr.add(F("not initialized")); - infoArr.add(F(" - check pin settings")); - } - } - - // Sound processing (FFT and input filters) - infoArr = user.createNestedArray(F("Sound Processing")); - if (audioSource && (disableSoundProcessing == false)) { - infoArr.add(F("running")); - } else { - infoArr.add(F("suspended")); - } - - // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("Manual Gain")); - float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets - infoArr.add(roundf(myGain*100.0f) / 100.0f); - infoArr.add("x"); - } - if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("AGC Gain")); - infoArr.add(roundf(multAgc*100.0f) / 100.0f); - infoArr.add("x"); - } -#endif - // UDP Sound Sync status - infoArr = user.createNestedArray(F("UDP Sound Sync")); - if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { - infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { - infoArr.add(F("receive mode")); - } - } else - infoArr.add("off"); - if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { - if (receivedFormat == 1) infoArr.add(F(" v1")); - if (receivedFormat == 2) infoArr.add(F(" v2")); - } - - #if defined(WLED_DEBUG) || defined(SR_DEBUG) - #ifdef ARDUINO_ARCH_ESP32 - infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(float(sampleTime)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("FFT time")); - infoArr.add(float(fftTime)/100.0f); - if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow - infoArr.add("! ms"); - else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability - infoArr.add(" ms!"); - else - infoArr.add(" ms"); - - DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); - #endif - #endif - } - } - - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject& root) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - JsonObject usermod = root[FPSTR(_name)]; - if (usermod.isNull()) { - usermod = root.createNestedObject(FPSTR(_name)); - } - usermod["on"] = enabled; - } - - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject& root) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - bool prevEnabled = enabled; - JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - if (prevEnabled != enabled) onUpdateBegin(!enabled); - if (addPalettes) { - // add/remove custom/audioreactive palettes - if (prevEnabled && !enabled) removeAudioPalettes(); - if (!prevEnabled && enabled) createAudioPalettes(); - } - } -#ifdef ARDUINO_ARCH_ESP32 - if (usermod[FPSTR(_inputLvl)].is()) { - inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); - } -#endif - } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } - } - - void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<10) { - // if palettes were removed during JSON call re-add them - createAudioPalettes(); - } - } - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will make your settings editable through the Usermod Settings page automatically. - * - * Usermod Settings Overview: - * - Numeric values are treated as floats in the browser. - * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float - * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and - * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. - * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. - * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a - * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. - * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type - * used in the Usermod when reading the value from ArduinoJson. - * - Pin values can be treated differently from an integer value by using the key name "pin" - * - "pin" can contain a single or array of integer values - * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins - * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) - * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used - * - * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings - * - * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. - * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. - * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) override - { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_addPalettes)] = addPalettes; - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); - amic["pin"] = audioPin; - #endif - - JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); - dmic["type"] = dmType; - JsonArray pinArray = dmic.createNestedArray("pin"); - pinArray.add(i2ssdPin); - pinArray.add(i2swsPin); - pinArray.add(i2sckPin); - pinArray.add(mclkPin); - - JsonObject cfg = top.createNestedObject(FPSTR(_config)); - cfg[F("squelch")] = soundSquelch; - cfg[F("gain")] = sampleGain; - cfg[F("AGC")] = soundAgc; - - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); - freqScale[F("scale")] = FFTScalingMode; -#endif - - JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); - dynLim[F("limiter")] = limiterOn; - dynLim[F("rise")] = attackTime; - dynLim[F("fall")] = decayTime; - - JsonObject sync = top.createNestedObject("sync"); - sync["port"] = audioSyncPort; - sync["mode"] = audioSyncEnabled; - } - - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - * - * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) - * - * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present - * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them - * - * This function is guaranteed to be called on boot, but could also be called every time settings are updated - */ - bool readFromConfig(JsonObject& root) override - { - JsonObject top = root[FPSTR(_name)]; - bool configComplete = !top.isNull(); - bool oldEnabled = enabled; - bool oldAddPalettes = addPalettes; - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); - configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); - #else - audioPin = -1; // MCU does not support analog mic - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM - #endif - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); - - configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); - configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); - configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); -#endif - configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); - configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); - - if (initDone) { - // add/remove custom/audioreactive palettes - if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); - if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); - } // else setup() will create palettes - return configComplete; - } - - - void appendConfigData(Print& uiScript) override - { - uiScript.print(F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style -#ifdef ARDUINO_ARCH_ESP32 - uiScript.print(F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] - uiScript.print(F("dd=addDropdown(ux,'digitalmic:type');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - uiScript.print(F("addOption(dd,'Generic Analog',0);")); - #endif - uiScript.print(F("addOption(dd,'Generic I2S',1);")); - uiScript.print(F("addOption(dd,'ES7243',2);")); - uiScript.print(F("addOption(dd,'SPH0654',3);")); - uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - uiScript.print(F("addOption(dd,'Generic I2S PDM',5);")); - #endif - uiScript.print(F("addOption(dd,'ES8388',6);")); - - uiScript.print(F("dd=addDropdown(ux,'config:AGC');")); - uiScript.print(F("addOption(dd,'Off',0);")); - uiScript.print(F("addOption(dd,'Normal',1);")); - uiScript.print(F("addOption(dd,'Vivid',2);")); - uiScript.print(F("addOption(dd,'Lazy',3);")); - - uiScript.print(F("dd=addDropdown(ux,'dynamics:limiter');")); - uiScript.print(F("addOption(dd,'Off',0);")); - uiScript.print(F("addOption(dd,'On',1);")); - uiScript.print(F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - uiScript.print(F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); - uiScript.print(F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); - - uiScript.print(F("dd=addDropdown(ux,'frequency:scale');")); - uiScript.print(F("addOption(dd,'None',0);")); - uiScript.print(F("addOption(dd,'Linear (Amplitude)',2);")); - uiScript.print(F("addOption(dd,'Square Root (Energy)',3);")); - uiScript.print(F("addOption(dd,'Logarithmic (Loudness)',1);")); -#endif - - uiScript.print(F("dd=addDropdown(ux,'sync:mode');")); - uiScript.print(F("addOption(dd,'Off',0);")); -#ifdef ARDUINO_ARCH_ESP32 - uiScript.print(F("addOption(dd,'Send',1);")); -#endif - uiScript.print(F("addOption(dd,'Receive',2);")); -#ifdef ARDUINO_ARCH_ESP32 - uiScript.print(F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - uiScript.print(F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); - uiScript.print(F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); - uiScript.print(F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - uiScript.print(F("addInfo(uxp,3,'only use -1, 0, 1 or 3','I2S MCLK');")); - #else - uiScript.print(F("addInfo(uxp,3,'master clock','I2S MCLK');")); - #endif -#endif - } - - - /* - * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. - * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. - * Commonly used for custom clocks (Cronixie, 7 segment) - */ - //void handleOverlayDraw() override - //{ - //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black - //} - - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() override - { - return USERMOD_ID_AUDIOREACTIVE; - } -}; - -void AudioReactive::removeAudioPalettes(void) { - DEBUG_PRINTLN(F("Removing audio palettes.")); - while (palettes>0) { - customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); -} - -void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); - if (palettes) return; - DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; pal -#include -#include // needed for SPH0465 timing workaround (classic ESP32) -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) -#include -#include -#endif -// type of i2s_config_t.SampleRate was changed from "int" to "unsigned" in IDF 4.4.x -#define SRate_t uint32_t -#else -#define SRate_t int -#endif - -//#include -//#include -//#include -//#include - -// see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents -// and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes -#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) - // there are two things in these MCUs that could lead to problems with audio processing: - // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) - // * single core, so FFT task might slow down other things like LED updates - #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) - #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3. - #else - #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3. - #endif -#endif - -/* ToDo: remove. ES7243 is controlled via compiler defines - Until this configuration is moved to the webinterface -*/ - -// if you have problems to get your microphone work on the left channel, uncomment the following line -//#define I2S_USE_RIGHT_CHANNEL // (experimental) define this to use right channel (digital mics only) - -// Uncomment the line below to utilize ADC1 _exclusively_ for I2S sound input. -// benefit: analog mic inputs will be sampled contiously -> better response times and less "glitches" -// WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; -// for example if you want to read "analog buttons" -//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continuously sample analog ADC microphone. WARNING will cause analogRead() lock-up - -// data type requested from the I2S driver - currently we always use 32bit -//#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible - -#ifdef I2S_USE_16BIT_SAMPLES -#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT -#define I2S_datatype int16_t -#define I2S_unsigned_datatype uint16_t -#define I2S_data_size I2S_BITS_PER_CHAN_16BIT -#undef I2S_SAMPLE_DOWNSCALE_TO_16BIT -#else -#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_32BIT -//#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_24BIT -#define I2S_datatype int32_t -#define I2S_unsigned_datatype uint32_t -#define I2S_data_size I2S_BITS_PER_CHAN_32BIT -#define I2S_SAMPLE_DOWNSCALE_TO_16BIT -#endif - -/* There are several (confusing) options in IDF 4.4.x: - * I2S_CHANNEL_FMT_RIGHT_LEFT, I2S_CHANNEL_FMT_ALL_RIGHT and I2S_CHANNEL_FMT_ALL_LEFT stands for stereo mode, which means two channels will transport different data. - * I2S_CHANNEL_FMT_ONLY_RIGHT and I2S_CHANNEL_FMT_ONLY_LEFT they are mono mode, both channels will only transport same data. - * I2S_CHANNEL_FMT_MULTIPLE means TDM channels, up to 16 channel will available, and they are stereo as default. - * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. -*/ - -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) -// espressif bug: only_left has no sound, left and right are swapped -// https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) -// https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) -// https://github.com/espressif/esp-idf/issues/6625 I2S: left/right channels are swapped for read (IDFGH-4826) -#ifdef I2S_USE_RIGHT_CHANNEL -#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT -#define I2S_MIC_CHANNEL_TEXT "right channel only (work-around swapped channel bug in IDF 4.4)." -#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT -#define I2S_PDM_MIC_CHANNEL_TEXT "right channel only" -#else -//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ALL_LEFT -//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_RIGHT_LEFT -#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT -#define I2S_MIC_CHANNEL_TEXT "left channel only (work-around swapped channel bug in IDF 4.4)." -#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT -#define I2S_PDM_MIC_CHANNEL_TEXT "left channel only." -#endif - -#else -// not swapped -#ifdef I2S_USE_RIGHT_CHANNEL -#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT -#define I2S_MIC_CHANNEL_TEXT "right channel only." -#else -#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT -#define I2S_MIC_CHANNEL_TEXT "left channel only." -#endif -#define I2S_PDM_MIC_CHANNEL I2S_MIC_CHANNEL -#define I2S_PDM_MIC_CHANNEL_TEXT I2S_MIC_CHANNEL_TEXT - -#endif - - -/* Interface class - AudioSource serves as base class for all microphone types - This enables accessing all microphones with one single interface - which simplifies the caller code -*/ -class AudioSource { - public: - /* All public methods are virtual, so they can be overridden - Everything but the destructor is also removed, to make sure each mic - Implementation provides its version of this function - */ - virtual ~AudioSource() {}; - - /* Initialize - This function needs to take care of anything that needs to be done - before samples can be obtained from the microphone. - */ - virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0; - - /* Deinitialize - Release all resources and deactivate any functionality that is used - by this microphone - */ - virtual void deinitialize() = 0; - - /* getSamples - Read num_samples from the microphone, and store them in the provided - buffer - */ - virtual void getSamples(float *buffer, uint16_t num_samples) = 0; - - /* check if the audio source driver was initialized successfully */ - virtual bool isInitialized(void) {return(_initialized);} - - /* identify Audiosource type - I2S-ADC or I2S-digital */ - typedef enum{Type_unknown=0, Type_I2SAdc=1, Type_I2SDigital=2} AudioSourceType; - virtual AudioSourceType getType(void) {return(Type_I2SDigital);} // default is "I2S digital source" - ADC type overrides this method - - protected: - /* Post-process audio sample - currently on needed for I2SAdcSource*/ - virtual I2S_datatype postProcessSample(I2S_datatype sample_in) {return(sample_in);} // default method can be overriden by instances (ADC) that need sample postprocessing - - // Private constructor, to make sure it is not callable except from derived classes - AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) : - _sampleRate(sampleRate), - _blockSize(blockSize), - _initialized(false), - _sampleScale(sampleScale) - {}; - - SRate_t _sampleRate; // Microphone sampling rate - int _blockSize; // I2S block size - bool _initialized; // Gets set to true if initialization is successful - float _sampleScale; // pre-scaling factor for I2S samples -}; - -/* Basic I2S microphone source - All functions are marked virtual, so derived classes can replace them -*/ -class I2SSource : public AudioSource { - public: - I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - AudioSource(sampleRate, blockSize, sampleScale) { - _config = { - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = _sampleRate, - .bits_per_sample = I2S_SAMPLE_RESOLUTION, - .channel_format = I2S_MIC_CHANNEL, -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), - //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, - .dma_buf_count = 8, - .dma_buf_len = _blockSize, - .use_apll = 0, - .bits_per_chan = I2S_data_size, -#else - .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 8, - .dma_buf_len = _blockSize, - .use_apll = false -#endif - }; - } - - virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("I2SSource:: initialize().")); - if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { - if (!PinManager::allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || - !PinManager::allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); - return; - } - } - - // i2ssckPin needs special treatment, since it might be unused on PDM mics - if (i2sckPin != I2S_PIN_NO_CHANGE) { - if (!PinManager::allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); - return; - } - } else { - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - #if !defined(SOC_I2S_SUPPORTS_PDM_RX) - #warning this MCU does not support PDM microphones - #endif - #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - // This is an I2S PDM microphone, these microphones only use a clock and - // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA - // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c - - // note to self: PDM has known bugs on S3, and does not work on C3 - // * S3: PDM sample rate only at 50% of expected rate: https://github.com/espressif/esp-idf/issues/9893 - // * S3: I2S PDM has very low amplitude: https://github.com/espressif/esp-idf/issues/8660 - // * C3: does not support PDM to PCM input. SoC would allow PDM RX, but there is no hardware to directly convert to PCM so it will not work. https://github.com/espressif/esp-idf/issues/8796 - - _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3 - _config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel. - _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality - #endif - } - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - if (mclkPin != I2S_PIN_NO_CHANGE) { - _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches. - // //_config.fixed_mclk = 512 * _sampleRate; - // //_config.fixed_mclk = 256 * _sampleRate; - } - - #if !defined(SOC_I2S_SUPPORTS_APLL) - #warning this MCU does not have an APLL high accuracy clock for audio - // S3: not supported; S2: supported; C3: not supported - _config.use_apll = false; // APLL not supported on this MCU - #endif - #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 - #endif -#endif - - // Reserve the master clock pin if provided - _mclkPin = mclkPin; - if (mclkPin != I2S_PIN_NO_CHANGE) { - if(!PinManager::allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); - return; - } else - _routeMclk(mclkPin); - } - - _pinConfig = { -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) - .mck_io_num = mclkPin, // "classic" ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only. i2s_set_pin() will fail if wrong mck_io_num is provided. -#endif - .bck_io_num = i2sckPin, - .ws_io_num = i2swsPin, - .data_out_num = I2S_PIN_NO_CHANGE, - .data_in_num = i2ssdPin - }; - - //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); - - esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); - if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); - return; - } - - DEBUGSR_PRINTF("AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\n", _config.use_apll? "uses":"without", _config.fixed_mclk); - DEBUGSR_PRINTF("AR: %d bits, Sample scaling factor = %6.4f\n", _config.bits_per_sample, _sampleScale); - if (_config.mode & I2S_MODE_PDM) { - DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in PDM MASTER mode.")); - } else { - DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); - } - - err = i2s_set_pin(I2S_NUM_0, &_pinConfig); - if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver - return; - } - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. - if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver - return; - } -#endif - _initialized = true; - } - - virtual void deinitialize() { - _initialized = false; - esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); - return; - } - if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); - if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); - if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); - // Release the master clock pin - if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); - } - - virtual void getSamples(float *buffer, uint16_t num_samples) { - if (_initialized) { - esp_err_t err; - size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ - I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ - - err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to get samples: %d\n", err); - return; - } - - // For correct operation, we need to read exactly sizeof(samples) bytes from i2s - if (bytes_read != sizeof(newSamples)) { - DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); - return; - } - - // Store samples in sample buffer and update DC offset - for (int i = 0; i < num_samples; i++) { - - newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples) - - float currSample = 0.0f; -#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT - currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places -#else - currSample = (float) newSamples[i]; // 16bit input -> use as-is -#endif - buffer[i] = currSample; - buffer[i] *= _sampleScale; // scale samples - } - } - } - - protected: - void _routeMclk(int8_t mclkPin) { -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - // MCLK routing by writing registers is not needed any more with IDF > 4.4.0 - #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) - // this way of MCLK routing only works on "classic" ESP32 - /* Enable the mclk routing depending on the selected mclk pin (ESP32: only 0,1,3) - Only I2S_NUM_0 is supported - */ - if (mclkPin == GPIO_NUM_0) { - PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1); - WRITE_PERI_REG(PIN_CTRL,0xFFF0); - } else if (mclkPin == GPIO_NUM_1) { - PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3); - WRITE_PERI_REG(PIN_CTRL, 0xF0F0); - } else { - PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2); - WRITE_PERI_REG(PIN_CTRL, 0xFF00); - } - #endif -#endif - } - - i2s_config_t _config; - i2s_pin_config_t _pinConfig; - int8_t _mclkPin; -}; - -/* ES7243 Microphone - This is an I2S microphone that requires initialization over - I2C before I2S data can be received -*/ -class ES7243 : public I2SSource { - private: - - void _es7243I2cWrite(uint8_t reg, uint8_t val) { - #ifndef ES7243_ADDR - #define ES7243_ADDR 0x13 // default address - #endif - Wire.beginTransmission(ES7243_ADDR); - Wire.write((uint8_t)reg); - Wire.write((uint8_t)val); - uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK - if (i2cErr != 0) { - DEBUGSR_PRINTF("AR: ES7243 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES7243_ADDR, reg, val); - } - } - - void _es7243InitAdc() { - _es7243I2cWrite(0x00, 0x01); - _es7243I2cWrite(0x06, 0x00); - _es7243I2cWrite(0x05, 0x1B); - _es7243I2cWrite(0x01, 0x00); // 0x00 for 24 bit to match INMP441 - not sure if this needs adjustment to get 16bit samples from I2S - _es7243I2cWrite(0x08, 0x43); - _es7243I2cWrite(0x05, 0x13); - } - -public: - ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; - }; - - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN(F("ES7243:: initialize();")); - if ((i2sckPin < 0) || (mclkPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - return; - } - - // First route mclk, then configure ADC over I2C, then configure I2S - _es7243InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - } - - void deinitialize() { - I2SSource::deinitialize(); - } -}; - -/* ES8388 Sound Module - This is an I2S sound processing unit that requires initialization over - I2C before I2S data can be received. -*/ -class ES8388Source : public I2SSource { - private: - - void _es8388I2cWrite(uint8_t reg, uint8_t val) { -#ifndef ES8388_ADDR - Wire.beginTransmission(0x10); - #define ES8388_ADDR 0x10 // default address -#else - Wire.beginTransmission(ES8388_ADDR); -#endif - Wire.write((uint8_t)reg); - Wire.write((uint8_t)val); - uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK - if (i2cErr != 0) { - DEBUGSR_PRINTF("AR: ES8388 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8388_ADDR, reg, val); - } - } - - void _es8388InitAdc() { - // https://dl.radxa.com/rock2/docs/hw/ds/ES8388%20user%20Guide.pdf Section 10.1 - // http://www.everest-semi.com/pdf/ES8388%20DS.pdf Better spec sheet, more clear. - // https://docs.google.com/spreadsheets/d/1CN3MvhkcPVESuxKyx1xRYqfUit5hOdsG45St9BCUm-g/edit#gid=0 generally - // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. - // Registries are decimal, settings are binary as that's how everything is listed in the docs - // ...which makes it easier to reference the docs. - // - _es8388I2cWrite( 8,0b00000000); // I2S to slave - _es8388I2cWrite( 2,0b11110011); // Power down DEM and STM - _es8388I2cWrite(43,0b10000000); // Set same LRCK - _es8388I2cWrite( 0,0b00000101); // Set chip to Play & Record Mode - _es8388I2cWrite(13,0b00000010); // Set MCLK/LRCK ratio to 256 - _es8388I2cWrite( 1,0b01000000); // Power up analog and lbias - _es8388I2cWrite( 3,0b00000000); // Power up ADC, Analog Input, and Mic Bias - _es8388I2cWrite( 4,0b11111100); // Power down DAC, Turn on LOUT1 and ROUT1 and LOUT2 and ROUT2 power - _es8388I2cWrite( 2,0b01000000); // Power up DEM and STM and undocumented bit for "turn on line-out amp" - - // #define use_es8388_mic - - #ifdef use_es8388_mic - // The mics *and* line-in are BOTH connected to LIN2/RIN2 on the AudioKit - // so there's no way to completely eliminate the mics. It's also hella noisy. - // Line-in works OK on the AudioKit, generally speaking, as the mics really need - // amplification to be noticeable in a quiet room. If you're in a very loud room, - // the mics on the AudioKit WILL pick up sound even in line-in mode. - // TL;DR: Don't use the AudioKit for anything, use the LyraT. - // - // The LyraT does a reasonable job with mic input as configured below. - - // Pick one of these. If you have to use the mics, use a LyraT over an AudioKit if you can: - _es8388I2cWrite(10,0b00000000); // Use Lin1/Rin1 for ADC input (mic on LyraT) - //_es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (mic *and* line-in on AudioKit) - - _es8388I2cWrite( 9,0b10001000); // Select Analog Input PGA Gain for ADC to +24dB (L+R) - _es8388I2cWrite(16,0b00000000); // Set ADC digital volume attenuation to 0dB (left) - _es8388I2cWrite(17,0b00000000); // Set ADC digital volume attenuation to 0dB (right) - _es8388I2cWrite(38,0b00011011); // Mixer - route LIN1/RIN1 to output after mic gain - - _es8388I2cWrite(39,0b01000000); // Mixer - route LIN to mixL, +6dB gain - _es8388I2cWrite(42,0b01000000); // Mixer - route RIN to mixR, +6dB gain - _es8388I2cWrite(46,0b00100001); // LOUT1VOL - 0b00100001 = +4.5dB - _es8388I2cWrite(47,0b00100001); // ROUT1VOL - 0b00100001 = +4.5dB - _es8388I2cWrite(48,0b00100001); // LOUT2VOL - 0b00100001 = +4.5dB - _es8388I2cWrite(49,0b00100001); // ROUT2VOL - 0b00100001 = +4.5dB - - // Music ALC - the mics like Auto Level Control - // You can also use this for line-in, but it's not really needed. - // - _es8388I2cWrite(18,0b11111000); // ALC: stereo, max gain +35.5dB, min gain -12dB - _es8388I2cWrite(19,0b00110000); // ALC: target -1.5dB, 0ms hold time - _es8388I2cWrite(20,0b10100110); // ALC: gain ramp up = 420ms/93ms, gain ramp down = check manual for calc - _es8388I2cWrite(21,0b00000110); // ALC: use "ALC" mode, no zero-cross, window 96 samples - _es8388I2cWrite(22,0b01011001); // ALC: noise gate threshold, PGA gain constant, noise gate enabled - #else - _es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input ("line-in") - _es8388I2cWrite( 9,0b00000000); // Select Analog Input PGA Gain for ADC to 0dB (L+R) - _es8388I2cWrite(16,0b01000000); // Set ADC digital volume attenuation to -32dB (left) - _es8388I2cWrite(17,0b01000000); // Set ADC digital volume attenuation to -32dB (right) - _es8388I2cWrite(38,0b00001001); // Mixer - route LIN2/RIN2 to output - - _es8388I2cWrite(39,0b01010000); // Mixer - route LIN to mixL, 0dB gain - _es8388I2cWrite(42,0b01010000); // Mixer - route RIN to mixR, 0dB gain - _es8388I2cWrite(46,0b00011011); // LOUT1VOL - 0b00011110 = +0dB, 0b00011011 = LyraT balance fix - _es8388I2cWrite(47,0b00011110); // ROUT1VOL - 0b00011110 = +0dB - _es8388I2cWrite(48,0b00011110); // LOUT2VOL - 0b00011110 = +0dB - _es8388I2cWrite(49,0b00011110); // ROUT2VOL - 0b00011110 = +0dB - #endif - - } - - public: - ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; - }; - - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN(F("ES8388Source:: initialize();")); - if ((i2sckPin < 0) || (mclkPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - return; - } - - // First route mclk, then configure ADC over I2C, then configure I2S - _es8388InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - } - - void deinitialize() { - I2SSource::deinitialize(); - } - -}; - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) -#if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC) - #warning this MCU does not support analog sound input -#endif -#endif - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) -// ADC over I2S is only availeable in "classic" ESP32 - -/* ADC over I2S Microphone - This microphone is an ADC pin sampled via the I2S interval - This allows to use the I2S API to obtain ADC samples with high sample rates - without the need of manual timing of the samples -*/ -class I2SAdcSource : public I2SSource { - public: - I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { - _config = { - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), - .sample_rate = _sampleRate, - .bits_per_sample = I2S_SAMPLE_RESOLUTION, - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), -#else - .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), -#endif - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 8, - .dma_buf_len = _blockSize, - .use_apll = false, - .tx_desc_auto_clear = false, - .fixed_mclk = 0 - }; - } - - /* identify Audiosource type - I2S-ADC*/ - AudioSourceType getType(void) {return(Type_I2SAdc);} - - void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("I2SAdcSource:: initialize().")); - _myADCchannel = 0x0F; - if(!PinManager::allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); - return; - } - _audioPin = audioPin; - - // Determine Analog channel. Only Channels on ADC1 are supported - int8_t channel = digitalPinToAnalogChannel(_audioPin); - if (channel > 9) { - DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin); - return; - } else { - adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); - _myADCchannel = channel; - } - - // Install Driver - esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); - return; - } - - adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution - - // Enable I2S mode of ADC - err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to set i2s adc mode: %d\n", err); - return; - } - - // see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino - adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_11); // configure ADC input amplification - - #if defined(I2S_GRAB_ADC1_COMPLETELY) - // according to docs from espressif, the ADC needs to be started explicitly - // fingers crossed - err = i2s_adc_enable(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); - //return; - } - #else - // bugfix: do not disable ADC initially - its already disabled after driver install. - //err = i2s_adc_disable(I2S_NUM_0); - // //err = i2s_stop(I2S_NUM_0); - //if (err != ESP_OK) { - // DEBUGSR_PRINTF("Failed to initially disable i2s adc: %d\n", err); - //} - #endif - - _initialized = true; - } - - - I2S_datatype postProcessSample(I2S_datatype sample_in) { - static I2S_datatype lastADCsample = 0; // last good sample - static unsigned int broken_samples_counter = 0; // number of consecutive broken (and fixed) ADC samples - I2S_datatype sample_out = 0; - - // bring sample down down to 16bit unsigned - I2S_unsigned_datatype rawData = * reinterpret_cast (&sample_in); // C++ acrobatics to get sample as "unsigned" - #ifndef I2S_USE_16BIT_SAMPLES - rawData = (rawData >> 16) & 0xFFFF; // scale input down from 32bit -> 16bit - I2S_datatype lastGoodSample = lastADCsample / 16384 ; // prepare "last good sample" accordingly (26bit-> 12bit with correct sign handling) - #else - rawData = rawData & 0xFFFF; // input is already in 16bit, just mask off possible junk - I2S_datatype lastGoodSample = lastADCsample * 4; // prepare "last good sample" accordingly (10bit-> 12bit) - #endif - - // decode ADC sample data fields - uint16_t the_channel = (rawData >> 12) & 0x000F; // upper 4 bit = ADC channel - uint16_t the_sample = rawData & 0x0FFF; // lower 12bit -> ADC sample (unsigned) - I2S_datatype finalSample = (int(the_sample) - 2048); // convert unsigned sample to signed (centered at 0); - - if ((the_channel != _myADCchannel) && (_myADCchannel != 0x0F)) { // 0x0F means "don't know what my channel is" - // fix bad sample - finalSample = lastGoodSample; // replace with last good ADC sample - broken_samples_counter ++; - if (broken_samples_counter > 256) _myADCchannel = 0x0F; // too many bad samples in a row -> disable sample corrections - //Serial.print("\n!ADC rogue sample 0x"); Serial.print(rawData, HEX); Serial.print("\tchannel:");Serial.println(the_channel); - } else broken_samples_counter = 0; // good sample - reset counter - - // back to original resolution - #ifndef I2S_USE_16BIT_SAMPLES - finalSample = finalSample << 16; // scale up from 16bit -> 32bit; - #endif - - finalSample = finalSample / 4; // mimic old analog driver behaviour (12bit -> 10bit) - sample_out = (3 * finalSample + lastADCsample) / 4; // apply low-pass filter (2-tap FIR) - //sample_out = (finalSample + lastADCsample) / 2; // apply stronger low-pass filter (2-tap FIR) - - lastADCsample = sample_out; // update ADC last sample - return(sample_out); - } - - - void getSamples(float *buffer, uint16_t num_samples) { - /* Enable ADC. This has to be enabled and disabled directly before and - * after sampling, otherwise Wifi dies - */ - if (_initialized) { - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - // old code - works for me without enable/disable, at least on ESP32. - //esp_err_t err = i2s_start(I2S_NUM_0); - esp_err_t err = i2s_adc_enable(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); - return; - } - #endif - - I2SSource::getSamples(buffer, num_samples); - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - // old code - works for me without enable/disable, at least on ESP32. - err = i2s_adc_disable(I2S_NUM_0); //i2s_adc_disable() may cause crash with IDF 4.4 (https://github.com/espressif/arduino-esp32/issues/6832) - //err = i2s_stop(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); - return; - } - #endif - } - } - - void deinitialize() { - PinManager::deallocatePin(_audioPin, PinOwner::UM_Audioreactive); - _initialized = false; - _myADCchannel = 0x0F; - - esp_err_t err; - #if defined(I2S_GRAB_ADC1_COMPLETELY) - // according to docs from espressif, the ADC needs to be stopped explicitly - // fingers crossed - err = i2s_adc_disable(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); - } - #endif - - i2s_stop(I2S_NUM_0); - err = i2s_driver_uninstall(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); - return; - } - } - - private: - int8_t _audioPin; - int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined" -}; -#endif - -/* SPH0645 Microphone - This is an I2S microphone with some timing quirks that need - special consideration. -*/ - -// https://github.com/espressif/esp-idf/issues/7192 SPH0645 i2s microphone issue when migrate from legacy esp-idf version (IDFGH-5453) -// a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin(). -class SPH0654 : public I2SSource { - public: - SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) - {} - - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("SPH0654:: initialize();")); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) -// these registers are only existing in "classic" ESP32 - REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9)); - REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT); -#else - #warning FIX ME! Please. -#endif - } -}; -#endif diff --git a/usermods/audioreactive/library.json b/usermods/audioreactive/library.json deleted file mode 100644 index fb2254a0ed..0000000000 --- a/usermods/audioreactive/library.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "audioreactive", - "build": { - "libArchive": false, - "extraScript": "override_sqrt.py" - }, - "dependencies": [ - { - "owner": "kosme", - "name": "arduinoFFT", - "version": "2.0.1", - "platforms": "espressif32" - } - ] -} diff --git a/usermods/audioreactive/override_sqrt.py b/usermods/audioreactive/override_sqrt.py deleted file mode 100644 index 36aa79df4b..0000000000 --- a/usermods/audioreactive/override_sqrt.py +++ /dev/null @@ -1,5 +0,0 @@ -Import('env') - -for lb in env.GetLibBuilders(): - if lb.name == "arduinoFFT": - lb.env.Append(CPPDEFINES=[("sqrt_internal", "sqrtf")]) diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md deleted file mode 100644 index 5ee575ffff..0000000000 --- a/usermods/audioreactive/readme.md +++ /dev/null @@ -1,73 +0,0 @@ -# Audioreactive usermod - -Enables controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter. -Supported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...). - -Does audio processing and provides data structure that specially written effects can use. - -**does not** provide effects or draw anything to an LED strip/matrix. - -## Additional Documentation - -This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): - -* [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) -* [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. -* [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) -* [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) -* [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) - -## Supported MCUs - -This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. - -It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. - -Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. - -Currently ESP8266 is not supported, due to low speed and small RAM of this chip. -There are however plans to create a lightweight audioreactive for the 8266, with reduced features. - -## Installation - -Add 'ADS1115_v2' to `custom_usermods` in your platformio environment. - -## Configuration - -All parameters are runtime configurable. Some may require a hard reset after changing them (I2S microphone or selected GPIOs). - -If you want to define default GPIOs during compile time, use the following (default values in parentheses): - -* `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S -* `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) -* `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) -* `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) -* `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) -* `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) -* `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) -* `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) - -Other options: - -* `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) -* `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default - -**NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. - -### Advanced Compile-Time Options - -You can use the following additional flags in your `build_flags` - -* `-D SR_SQUELCH=x` : Default "squelch" setting (10) -* `-D SR_GAIN=x` : Default "gain" setting (60) -* `-D SR_AGC=x` : (Only ESP32) Default "AGC (Automatic Gain Control)" setting (0): 0=off, 1=normal, 2=vivid, 3=lazy -* `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). -* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM resources (not recommended unless you absolutely need this). -* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this *will* cause conflicts(lock-up) with any analogRead() call. -* `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) -* `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. - -## Release notes - -* 2022-06 Ported from [soundreactive WLED](https://github.com/atuline/WLED) - by @blazoncek (AKA Blaz Kristan) and the [SR-WLED team](https://github.com/atuline/WLED/wiki#sound-reactive-wled-fork-team). -* 2022-11 Updated to align with "[MoonModules/WLED](https://amg.wled.me)" audioreactive usermod - by @softhack007 (AKA Frank Möhle). diff --git a/wled00/const.h b/wled00/const.h index 6d1825d574..1ec008e5ec 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -440,6 +440,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) +#define ERR_REBOOT_NEEDED 98 // reboot needed after changing hardware setting +#define ERR_POWEROFF_NEEDED 99 // power-cycle needed after changing hardware setting // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 84b5595df7..a1a1855f10 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -332,6 +332,7 @@ class Usermod { protected: // Shim for oappend(), which used to exist in utils.cpp template static inline void oappend(const T& t) { oappend_shim->print(t); }; + template static inline void oappendi(const T& t) { oappend_shim->print(t); }; #ifdef ESP8266 // Handle print(PSTR()) without crashing by detecting PROGMEM strings static void oappend(const char* c) { if ((intptr_t) c >= 0x40000000) oappend_shim->print(FPSTR(c)); else oappend_shim->print(c); };