From d1f106b94ca1e18cac3339fa3cc5e2653374372c Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:09:54 +0200 Subject: [PATCH 1/6] a few more formatting options align assignments, bitfield definitions and array initializers --- .clang-format | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.clang-format b/.clang-format index 53e938a1..f2c8c6c4 100644 --- a/.clang-format +++ b/.clang-format @@ -20,3 +20,11 @@ MaxEmptyLinesToKeep: 2 # existing code frequently ReflowComments: false # don't rewrap comment text/wording SortIncludes: Never # WLED sometimes orders includes deliberately (platform/feature guards); alphabetising risks reordering them and breaking a build IndentPPDirectives: BeforeHash # makes nested #ifdef easier to read +AlignConsecutiveBitFields: AcrossEmptyLines # align bitfield definitions +AllowShortBlocksOnASingleLine: true # while (true) { continue; } can be put on a single line +AlignArrayOfStructures: Right # aligns array initializer colums +AlignConsecutiveAssignments: # aligns the "=" in consecutive assignment rows. A matter of taste, but I do like it. + Enabled: true + AlignCompound: true + AcrossEmptyLines: false + AcrossComments: false From 986d2601e370f8ce3a47c25b90d872c9e3f608bc Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:21:21 +0200 Subject: [PATCH 2/6] my favourite clang-format additions * align trailing comments (optional) * allows consecutive short case statements (aligned) * better formatting for parameters and argument lists --- .clang-format | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index f2c8c6c4..86a71658 100644 --- a/.clang-format +++ b/.clang-format @@ -8,18 +8,31 @@ # Requires clang-format 16+ (AlignTrailingComments' Kind-based syntax below # needs it). Verified to parse cleanly on clang-format 16, 18 and 22. BasedOnStyle: Google +TabWidth: 2 +UseTab: Never # always use spaces, no tabs ColumnLimit: 240 # WLED convention: wide lines, not the 80-col default AlignTrailingComments: Leave # preserves existing comment-column alignment; forcing "Never" re-collapses spacing on every touched line, breaking alignment with untouched neighbours +# optional: use this if you prefer to see all in-line comments aligned to each other +#AlignTrailingComments: +# Kind: Always +# OverEmptyLines: 2 AllowShortCaseLabelsOnASingleLine: true # matches the `case X: doThing(); break;` style used throughout +AlignConsecutiveShortCaseStatements: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true + AlignCaseColons: false +AlignConsecutiveMacros: Consecutive AllowShortFunctionsOnASingleLine: Inline # keeps short wrapper/getter functions on one line, closer to existing code than Google's stricter default AllowShortIfStatementsOnASingleLine: AllIfsAndElse # un-deprecates the old "Always" value used before; matches existing single-line if/else usage BreakConstructorInitializers: BeforeComma # non-deprecated replacement for BreakConstructorInitializersBeforeComma InsertNewlineAtEOF: true # text files should end with a newline -KeepEmptyLinesAtTheStartOfBlocks: true # deliberately kept in its deprecated spelling: the modern nested "KeepEmptyLines" key errors out on clang-format 16 and 18, and CodeRabbit's suggested "KeepEmptyLinesAtTopOfFunctions" is an unknown key on 14/16/18/22 alike. This old alias parses cleanly on all of them. +KeepEmptyLinesAtTheStartOfBlocks: true # deliberately kept in its deprecated spelling: the modern nested "KeepEmptyLines" key errors out on clang-format 16 and 18, and CodeRabbit's suggested "KeepEmptyLinesAtTopOfFunctions" is an unknown key on 14/16/18/22 alike. This old alias parses cleanly on all of them. MaxEmptyLinesToKeep: 2 # existing code frequently uses double blank lines; Google's default of 1 would touch far more of it ReflowComments: false # don't rewrap comment text/wording SortIncludes: Never # WLED sometimes orders includes deliberately (platform/feature guards); alphabetising risks reordering them and breaking a build IndentPPDirectives: BeforeHash # makes nested #ifdef easier to read + AlignConsecutiveBitFields: AcrossEmptyLines # align bitfield definitions AllowShortBlocksOnASingleLine: true # while (true) { continue; } can be put on a single line AlignArrayOfStructures: Right # aligns array initializer colums @@ -28,3 +41,5 @@ AlignConsecutiveAssignments: # aligns the "=" in consecu AlignCompound: true AcrossEmptyLines: false AcrossComments: false +BinPackArguments: true +BinPackParameters: true From 0a4035f22aad915604a31a2a4ef93c4f89cd98f1 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:21:59 +0200 Subject: [PATCH 3/6] full re-format of audio_reactive.h clang-format --- audio_reactive.h | 5371 +++++++++++++++++++++++----------------------- 1 file changed, 2689 insertions(+), 2682 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 8b198096..2fcf2347 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -14,32 +14,32 @@ #ifdef ARDUINO_ARCH_ESP32 -// backwards compatibility hacks for esp-idf v3 -#if (ESP_IDF_VERSION_MAJOR < 4) && defined(CONFIG_IDF_TARGET_ESP32) && !defined(SOC_CPU_HAS_FPU) - #if !defined(SOC_CPU_HAS_FPU) - #define SOC_CPU_HAS_FPU 1 - #endif - #if !defined(SOC_CPU_CORES_NUM) - #define SOC_CPU_CORES_NUM 2 - #endif - #if !defined(SOC_I2S_SUPPORTS_ADC) - #define SOC_I2S_SUPPORTS_ADC 1 - #endif - #if !defined(SOC_I2S_SUPPORTS_PDM_RX) - #define SOC_I2S_SUPPORTS_PDM_RX 1 - #endif - #if !defined(SOC_I2S_NUM) - #define SOC_I2S_NUM 2 - #endif -#endif + // backwards compatibility hacks for esp-idf v3 + #if (ESP_IDF_VERSION_MAJOR < 4) && defined(CONFIG_IDF_TARGET_ESP32) && !defined(SOC_CPU_HAS_FPU) + #if !defined(SOC_CPU_HAS_FPU) + #define SOC_CPU_HAS_FPU 1 + #endif + #if !defined(SOC_CPU_CORES_NUM) + #define SOC_CPU_CORES_NUM 2 + #endif + #if !defined(SOC_I2S_SUPPORTS_ADC) + #define SOC_I2S_SUPPORTS_ADC 1 + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) + #define SOC_I2S_SUPPORTS_PDM_RX 1 + #endif + #if !defined(SOC_I2S_NUM) + #define SOC_I2S_NUM 2 + #endif + #endif -#include -#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) - #include -#endif + #include + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) + #include + #endif -// AI: below section was generated by an AI -#if defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION_MAJOR >= 5) + // AI: below section was generated by an AI + #if defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION_MAJOR >= 5) /* * Bootloop workaround for classic ESP32 on esp-idf 5.x (WLED version 17.x): * AR still uses the legacy I2S driver (driver/i2s.h) which pulls in the legacy ADC driver. @@ -52,11 +52,12 @@ * so these paths are never executed; digital I2S/PDM mics don't use them. * Delete this block once the usermod is ported to the new I2S driver (driver/i2s_std.h). */ -#include + #include // Stub: would configure the (removed) I2S built-in ADC mode. Never called for digital mics. -extern "C" esp_err_t adc_i2s_mode_init(int adcUnit, int channel) { - (void)adcUnit; (void)channel; +extern "C" esp_err_t adc_i2s_mode_init(int adcUnit, int channel) { + (void)adcUnit; + (void)channel; return ESP_ERR_NOT_SUPPORTED; } // Stub: would select the ADC data source for I2S DMA. Never called for digital mics. @@ -72,15 +73,15 @@ extern "C" esp_err_t adc1_dma_mode_acquire(void) { extern "C" esp_err_t adc1_lock_release(void) { return ESP_OK; } -#endif + #endif // AI: end -#include + #include #endif #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) -#include + #include #endif /* @@ -102,32 +103,32 @@ extern "C" esp_err_t adc1_lock_release(void) { #endif #ifdef _MoonModules_WLED_ -#pragma message "building audioreactive V2.0 for WLED-MM" + #pragma message "building audioreactive V2.0 for WLED-MM" #endif // sanity checks for MCU capability flags #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) - #if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) - #error "configuration error - SOC_CPU_HAS_FPU or SOC_CPU_CORES_NUM does not match your MCU" - #endif - #if !defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(CONFIG_IDF_TARGET_ESP32P4) - #error "configuration error - SOC_I2S_SUPPORTS_PDM_RX does not match your MCU" - #endif + #if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) + #error "configuration error - SOC_CPU_HAS_FPU or SOC_CPU_CORES_NUM does not match your MCU" + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(CONFIG_IDF_TARGET_ESP32P4) + #error "configuration error - SOC_I2S_SUPPORTS_PDM_RX does not match your MCU" + #endif #endif -#if defined(SOC_I2S_SUPPORTS_PDM_RX) && (defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2)) - #warning "PDM input is not supported yet on your MCU" +#if defined(SOC_I2S_SUPPORTS_PDM_RX) && (defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2)) + #warning "PDM input is not supported yet on your MCU" #endif #if defined(SOC_I2S_SUPPORTS_ADC) && (!defined(CONFIG_IDF_TARGET_ESP32) || (ESP_IDF_VERSION_MAJOR > 4)) - #warning "analog ADC input is not supported yet on your MCU" + #warning "analog ADC input is not supported yet on your MCU" #endif #if !defined(WLEDMM_FASTPATH) && defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) -#warning "please define WLEDMM_FASTPATH for full performance on your board" + #warning "please define WLEDMM_FASTPATH for full performance on your board" #endif #if defined(WLEDMM_FASTPATH) && defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) -#define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap + #define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap #endif @@ -135,27 +136,27 @@ extern "C" esp_err_t adc1_lock_release(void) { //#define SR_STATS #if !defined(FFTTASK_PRIORITY) -#if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) -// FASTPATH: use higher priority, to avoid that webserver (ws, json, etc) delays sample processing -//#define FFTTASK_PRIORITY 3 // competing with async_tcp -#define FFTTASK_PRIORITY 4 // above async_tcp -#else -#define FFTTASK_PRIORITY 1 // standard: looptask prio -//#define FFTTASK_PRIORITY 2 // above looptask, below async_tcp -#endif + #if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) + // FASTPATH: use higher priority, to avoid that webserver (ws, json, etc) delays sample processing + //#define FFTTASK_PRIORITY 3 // competing with async_tcp + #define FFTTASK_PRIORITY 4 // above async_tcp + #else + #define FFTTASK_PRIORITY 1 // standard: looptask prio + //#define FFTTASK_PRIORITY 2 // above looptask, below async_tcp + #endif #endif #if defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) -// this applies "pink noise scaling" to FFT results before computing the major peak for effects. -// currently only for ESP32-S3 and classic ESP32, due to increased runtime -#define FFT_MAJORPEAK_HUMAN_EAR + // this applies "pink noise scaling" to FFT results before computing the major peak for effects. + // currently only for ESP32-S3 and classic ESP32, due to increased runtime + #define FFT_MAJORPEAK_HUMAN_EAR #endif // high-resolution type for input filters #if defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) -#define SR_HIRES_TYPE double // ESP32 and ESP32-S3 (with FPU) are fast enough to use "double" + #define SR_HIRES_TYPE double // ESP32 and ESP32-S3 (with FPU) are fast enough to use "double" #else -#define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3) + #define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3) #endif // Comment/Uncomment to toggle usb serial debugging @@ -164,8 +165,8 @@ extern "C" esp_err_t adc1_lock_release(void) { // #define SR_DEBUG // generic SR DEBUG messages #ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) DEBUGOUT(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUTLN(x) + #define DEBUGSR_PRINT(x) DEBUGOUT(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUTLN(x) #define DEBUGSR_PRINTF(x...) DEBUGOUTF(x) #else #define DEBUGSR_PRINT(x) @@ -174,36 +175,36 @@ extern "C" esp_err_t adc1_lock_release(void) { #endif #if defined(SR_DEBUG) -#define ERRORSR_PRINT(x) DEBUGSR_PRINT(x) -#define ERRORSR_PRINTLN(x) DEBUGSR_PRINTLN(x) -#define ERRORSR_PRINTF(x...) DEBUGSR_PRINTF(x) + #define ERRORSR_PRINT(x) DEBUGSR_PRINT(x) + #define ERRORSR_PRINTLN(x) DEBUGSR_PRINTLN(x) + #define ERRORSR_PRINTF(x...) DEBUGSR_PRINTF(x) #else -#if defined(WLED_DEBUG) -#define ERRORSR_PRINT(x) DEBUG_PRINT(x) -#define ERRORSR_PRINTLN(x) DEBUG_PRINTLN(x) -#define ERRORSR_PRINTF(x...) DEBUG_PRINTF(x) -#else - #define ERRORSR_PRINT(x) - #define ERRORSR_PRINTLN(x) - #define ERRORSR_PRINTF(x...) -#endif + #if defined(WLED_DEBUG) + #define ERRORSR_PRINT(x) DEBUG_PRINT(x) + #define ERRORSR_PRINTLN(x) DEBUG_PRINTLN(x) + #define ERRORSR_PRINTF(x...) DEBUG_PRINTF(x) + #else + #define ERRORSR_PRINT(x) + #define ERRORSR_PRINTLN(x) + #define ERRORSR_PRINTF(x...) + #endif #endif // WLED does not have WLED-MM's USER_PRINT* macros #ifndef USER_PRINT - #define USER_PRINT(x) DEBUGSR_PRINT(x) - #define USER_PRINTLN(x) DEBUGSR_PRINTLN(x) + #define USER_PRINT(x) DEBUGSR_PRINT(x) + #define USER_PRINTLN(x) DEBUGSR_PRINTLN(x) #define USER_PRINTF(x...) DEBUGSR_PRINTF(x) #define USER_FLUSH() - + #define oappendi oappend #endif #if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT(x) - #define PLOT_PRINTLN(x) DEBUGOUTLN(x) + #define PLOT_PRINT(x) DEBUGOUT(x) + #define PLOT_PRINTLN(x) DEBUGOUTLN(x) #define PLOT_PRINTF(x...) DEBUGOUTF(x) - #define PLOT_FLUSH() DEBUGOUTFlush() + #define PLOT_FLUSH() DEBUGOUTFlush() #else #define PLOT_PRINT(x) #define PLOT_PRINTLN(x) @@ -214,99 +215,99 @@ extern "C" esp_err_t adc1_lock_release(void) { // sanity checks - only needed for WLED-MM #if defined(ARDUINO_ARCH_ESP32) && defined(_MoonModules_WLED_) // we need more space in for oappend() stack buffer -> SETTINGS_STACK_BUF_SIZE and CONFIG_ASYNC_TCP_STACK_SIZE - #if SETTINGS_STACK_BUF_SIZE < 3904 // 3904 is required for WLEDMM-0.14.0-b28 + #if SETTINGS_STACK_BUF_SIZE < 3904 // 3904 is required for WLEDMM-0.14.0-b28 #warning please increase SETTINGS_STACK_BUF_SIZE >= 3904 #endif - #if (CONFIG_ASYNC_TCP_STACK_SIZE - SETTINGS_STACK_BUF_SIZE) < 4352 // at least 4096+256 words of free task stack is needed by async_tcp alone + #if (CONFIG_ASYNC_TCP_STACK_SIZE - SETTINGS_STACK_BUF_SIZE) < 4352 // at least 4096+256 words of free task stack is needed by async_tcp alone #error remaining async_tcp stack will be too low - please increase CONFIG_ASYNC_TCP_STACK_SIZE #endif #endif // audiosync constants -#define AUDIOSYNC_NONE 0x00 // UDP sound sync off -#define AUDIOSYNC_SEND 0x01 // UDP sound sync - send mode -#define AUDIOSYNC_REC 0x02 // UDP sound sync - receiver mode -#define AUDIOSYNC_REC_PLUS 0x06 // UDP sound sync - receiver + local mode (uses local input if no receiving udp sound) +#define AUDIOSYNC_NONE 0x00 // UDP sound sync off +#define AUDIOSYNC_SEND 0x01 // UDP sound sync - send mode +#define AUDIOSYNC_REC 0x02 // UDP sound sync - receiver mode +#define AUDIOSYNC_REC_PLUS 0x06 // UDP sound sync - receiver + local mode (uses local input if no receiving udp sound) // adiosync timeouts (milliseconds) -#define AUDIOSYNC_IDLE_MS 2500U // timeout for "receiver idle" -> mode 6 switches back to local audio -#define AUDIOSYNC_REC_DEAD_MS 25000U // timeout for "receiver reconnect" -#define AUDIOSYNC_SEND_STOP_MS 30000U // timeout until "silence stop" - - -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as it is shared between tasks. -static uint8_t audioSyncEnabled = AUDIOSYNC_NONE; // bit field: bit 0 - send, bit 1 - receive, bit 2 - use local if not receiving -static bool audioSyncSequence = true; // if true, the receiver will drop out-of-sequence packets -static uint8_t audioSyncPurge = 1; // 0: process each packet (don't purge); 1: auto-purge old packets; 2: only process latest received packet (always purge) -static bool audioSyncSilenceStop = true; // if true, the sender will stop transmitting UDP packets after 30 seconds of continuous silence -static bool audioSyncBroadcast = false; // if true, use subnet broadcast for sending instead of multicast; receivers on multicast sockets will still receive broadcast packets -static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync -static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start) - -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! -#define MAX_PALETTES 5 // number of audioreactive palettes provided by this usermod +#define AUDIOSYNC_IDLE_MS 2500U // timeout for "receiver idle" -> mode 6 switches back to local audio +#define AUDIOSYNC_REC_DEAD_MS 25000U // timeout for "receiver reconnect" +#define AUDIOSYNC_SEND_STOP_MS 30000U // timeout until "silence stop" + + +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as it is shared between tasks. +static uint8_t audioSyncEnabled = AUDIOSYNC_NONE; // bit field: bit 0 - send, bit 1 - receive, bit 2 - use local if not receiving +static bool audioSyncSequence = true; // if true, the receiver will drop out-of-sequence packets +static uint8_t audioSyncPurge = 1; // 0: process each packet (don't purge); 1: auto-purge old packets; 2: only process latest received packet (always purge) +static bool audioSyncSilenceStop = true; // if true, the sender will stop transmitting UDP packets after 30 seconds of continuous silence +static bool audioSyncBroadcast = false; // if true, use subnet broadcast for sending instead of multicast; receivers on multicast sockets will still receive broadcast packets +static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync +static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start) + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! +#define MAX_PALETTES 5 // number of audioreactive palettes provided by this usermod // helper functions -static float my_mapf(float x, float in_min, float in_max, float out_min, float out_max){ // float version of map() +static float my_mapf(float x, float in_min, float in_max, float out_min, float out_max) { // float version of map() return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 -#ifndef SR_AGC // Automatic gain control mode - #ifdef SR_SQUELCH - #define SR_AGC 1 // default "squelch" was provided --> default mode = on - #else - #define SR_AGC 0 // default mode = off + #ifndef SR_AGC // Automatic gain control mode + #ifdef SR_SQUELCH + #define SR_AGC 1 // default "squelch" was provided --> default mode = on + #else + #define SR_AGC 0 // default mode = off + #endif #endif -#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; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) - enable AGC if default "squelch" was provided +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; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) - enable AGC if default "squelch" was provided #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.getMinShowDelay() -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. -volatile bool haveNewFFTResult = false; // flag to directly inform UDP sound sender when new FFT results are available (to reduce latency). Flag is reset at next UDP send - -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0}; // Our calculated freq. channel result table to be used by effects -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. (also used by dynamics limiter) -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) - -static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages used only by audio palettes 3 & 4 (EMA, ~390ms time constant, cadence-independent - see fillAudioPalettes()) -static constexpr float PALETTE_TIME_CONSTANT_MS = 390.0f; // time constant for paletteBandAvg smoothing; derived from original alpha=0.05 @ 20ms cycle (tau = -20ms / ln(1-0.05)) +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.getMinShowDelay() +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. +volatile bool haveNewFFTResult = false; // flag to directly inform UDP sound sender when new FFT results are available (to reduce latency). Flag is reset at next UDP send + +static uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; // Our calculated freq. channel result table to be used by effects +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. (also used by dynamics limiter) +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) + +static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages used only by audio palettes 3 & 4 (EMA, ~390ms time constant, cadence-independent - see fillAudioPalettes()) +static constexpr float PALETTE_TIME_CONSTANT_MS = 390.0f; // time constant for paletteBandAvg smoothing; derived from original alpha=0.05 @ 20ms cycle (tau = -20ms / ln(1-0.05)) // Aggregates derived from paletteBandAvg[], precomputed once per fillAudioPalettes() refresh // for use by getCRGBForBand() palettes 3 & 4 (see there for details). -static uint8_t paletteHue3 = 0; // case 3: spectral-centroid hue -static uint8_t paletteSat3 = 180; // case 3: loudness-derived saturation -static uint8_t paletteHue4 = 0; // case 4: bass/mid/high-weighted hue -static uint8_t paletteSat4 = 180; // case 4: dominance-derived saturation -static float paletteTotal4 = 0.0f;// case 4: total band energy (drives brightness) +static uint8_t paletteHue3 = 0; // case 3: spectral-centroid hue +static uint8_t paletteSat3 = 180; // case 3: loudness-derived saturation +static uint8_t paletteHue4 = 0; // case 4: bass/mid/high-weighted hue +static uint8_t paletteSat4 = 180; // case 4: dominance-derived saturation +static float paletteTotal4 = 0.0f; // case 4: total band energy (drives brightness) -static uint16_t zeroCrossingCount = 0; // number of zero crossings in the current batch of 512 samples +static uint16_t zeroCrossingCount = 0; // number of zero crossings in the current batch of 512 samples // 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 +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 +static bool limiterOn = false; // bool: enable / disable dynamics limiter #else static bool limiterOn = true; #endif -[[maybe_unused]] static uint8_t micQuality = 0; // input filtering; 0 normal, 1 minimal filtering, 2 no filtering - unused on 8266 +[[maybe_unused]] static uint8_t micQuality = 0; // input filtering; 0 normal, 1 minimal filtering, 2 no filtering - unused on 8266 #ifdef FFT_USE_SLIDING_WINDOW -static uint16_t attackTime = 24; // int: attack time in milliseconds. Default 0.024sec -static uint16_t decayTime = 250; // int: decay time in milliseconds. New default 250ms. +static uint16_t attackTime = 24; // int: attack time in milliseconds. Default 0.024sec +static uint16_t decayTime = 250; // int: decay time in milliseconds. New default 250ms. #else -static uint16_t attackTime = 50; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 300; // int: decay time in milliseconds. New default 300ms. Old default was 1.40sec +static uint16_t attackTime = 50; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 300; // int: decay time in milliseconds. New default 300ms. Old default was 1.40sec #endif // peak detection @@ -320,121 +321,120 @@ static uint8_t binNum = 8; // Used to select the bin for FFT based bea // use audio source class (ESP32 specific) #include "audio_source.h" -constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) +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 +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 -#ifndef SR_FREQ_PROF - static uint8_t pinkIndex = 0; // 0: default; 1: line-in; 2: IMNP441 -#else - static uint8_t pinkIndex = SR_FREQ_PROF; // 0: default; 1: line-in; 2: IMNP441 -#endif +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root + #ifndef SR_FREQ_PROF + static uint8_t pinkIndex = 0; // 0: default; 1: line-in; 2: IMNP441 + #else + static uint8_t pinkIndex = SR_FREQ_PROF; // 0: default; 1: line-in; 2: IMNP441 + #endif -// +// // 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 -#if defined(WLEDMM_FASTPATH) -const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/8.f, 1/5.f, 1/12.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) -#else -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) -#endif +// +#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 + #if defined(WLEDMM_FASTPATH) + const float agcSampleSmooth[AGC_NUM_PRESETS] = {1 / 8.f, 1 / 5.f, 1 / 12.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) + #else + 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) + #endif // AGC presets end -static AudioSource *audioSource = nullptr; -static uint8_t useInputFilter = 0; // enables low-cut filtering. Applies before FFT. +static AudioSource* audioSource = nullptr; +static uint8_t useInputFilter = 0; // enables low-cut filtering. Applies before FFT. //WLEDMM experimental settings -static uint8_t micLevelMethod = 1; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) -#if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) -static constexpr uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs. -#else -static constexpr uint8_t averageByRMS = true; // false: use mean value, true: use RMS (root mean squared). use better method on fast MCUs. -#endif -static uint8_t freqDist = 0; // 0=old 1=rightshift mode -static uint8_t fftWindow = 0; // FFT windowing function (0 = default) -#ifdef FFT_USE_SLIDING_WINDOW -static uint8_t doSlidingFFT = 1; // 1 = use sliding window FFT (faster & more accurate) -#endif +static uint8_t micLevelMethod = 1; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) + #if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) + static constexpr uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs. + #else + static constexpr uint8_t averageByRMS = true; // false: use mean value, true: use RMS (root mean squared). use better method on fast MCUs. + #endif + static uint8_t freqDist = 0; // 0=old 1=rightshift mode + static uint8_t fftWindow = 0; // FFT windowing function (0 = default) + #ifdef FFT_USE_SLIDING_WINDOW + static uint8_t doSlidingFFT = 1; // 1 = use sliding window FFT (faster & more accurate) + #endif // variables used in effects //static int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc //static float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc // shared vars for debugging -#ifdef MIC_LOGGER -static volatile float micReal_min = 0.0f; // MicIn data min from last batch of samples -static volatile float micReal_avg = 0.0f; // MicIn data average (from last batch of samples) -static volatile float micReal_max = 0.0f; // MicIn data max from last batch of samples -#if 0 -static volatile float micReal_min2 = 0.0f; // MicIn data min after filtering -static volatile float micReal_max2 = 0.0f; // MicIn data max after filtering -#endif -#endif + #ifdef MIC_LOGGER + static volatile float micReal_min = 0.0f; // MicIn data min from last batch of samples + static volatile float micReal_avg = 0.0f; // MicIn data average (from last batch of samples) + static volatile float micReal_max = 0.0f; // MicIn data max from last batch of samples + #if 0 + static volatile float micReal_min2 = 0.0f; // MicIn data min after filtering + static volatile float micReal_max2 = 0.0f; // MicIn data max after filtering + #endif + #endif //////////////////// // 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, bool i2sFastpath); // post-processing and post-amp of GEQ channels - +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, bool i2sFastpath); // 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. -#define MAX_PINK 10 // 0 = standard, 1= line-in (pink noise only), 2..4 = IMNP441, 5..6 = ICS-43434, ,7=SPM1423, 8..9 = userdef, 10= flat (no pink noise adjustment) -static const float fftResultPink[MAX_PINK+1][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 }, // 0 default from SR WLED - // { 1.30f, 1.32f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.39f, 3.09f, 4.34f }, // - Line-In Generic -> pink noise adjustment only - { 2.35f, 1.32f, 1.32f, 1.40f, 1.48f, 1.57f, 1.68f, 1.80f, 1.89f, 1.95f, 2.14f, 2.26f, 2.50f, 2.90f, 4.20f, 6.50f }, // 1 Line-In CS5343 + DC blocker + // Table of multiplication factors so that we can even out the frequency response. + #define MAX_PINK 10 // 0 = standard, 1= line-in (pink noise only), 2..4 = IMNP441, 5..6 = ICS-43434, ,7=SPM1423, 8..9 = userdef, 10= flat (no pink noise adjustment) +static const float fftResultPink[MAX_PINK + 1][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}, // 0 default from SR WLED + // { 1.30f, 1.32f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.39f, 3.09f, 4.34f }, // - Line-In Generic -> pink noise adjustment only + {2.35f, 1.32f, 1.32f, 1.40f, 1.48f, 1.57f, 1.68f, 1.80f, 1.89f, 1.95f, 2.14f, 2.26f, 2.50f, 2.90f, 4.20f, 6.50f}, // 1 Line-In CS5343 + DC blocker - { 1.82f, 1.72f, 1.70f, 1.50f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.90f, 3.86f, 6.29f}, // 2 IMNP441 datasheet response profile * pink noise - { 2.80f, 2.20f, 1.30f, 1.15f, 1.55f, 2.45f, 4.20f, 2.80f, 3.20f, 3.60f, 4.20f, 4.90f, 5.70f, 6.05f,10.50f,14.85f}, // 3 IMNP441 - big speaker, strong bass - // next one has not much visual differece compared to default IMNP441 profile - { 12.0f, 6.60f, 2.60f, 1.15f, 1.35f, 2.05f, 2.85f, 2.50f, 2.85f, 3.30f, 2.25f, 4.35f, 3.80f, 3.75f, 6.50f, 9.00f}, // 4 IMNP441 - voice, or small speaker + {1.82f, 1.72f, 1.70f, 1.50f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.90f, 3.86f, 6.29f}, // 2 IMNP441 datasheet response profile * pink noise + {2.80f, 2.20f, 1.30f, 1.15f, 1.55f, 2.45f, 4.20f, 2.80f, 3.20f, 3.60f, 4.20f, 4.90f, 5.70f, 6.05f, 10.50f, 14.85f}, // 3 IMNP441 - big speaker, strong bass + // next one has not much visual differece compared to default IMNP441 profile + {12.0f, 6.60f, 2.60f, 1.15f, 1.35f, 2.05f, 2.85f, 2.50f, 2.85f, 3.30f, 2.25f, 4.35f, 3.80f, 3.75f, 6.50f, 9.00f}, // 4 IMNP441 - voice, or small speaker - { 2.75f, 1.60f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 1.75f, 2.55f, 3.60f }, // 5 ICS-43434 datasheet response * pink noise - { 2.90f, 1.25f, 0.75f, 1.08f, 2.35f, 3.55f, 3.60f, 3.40f, 2.75f, 3.45f, 4.40f, 6.35f, 6.80f, 6.80f, 8.50f,10.64f }, // 6 ICS-43434 - big speaker, strong bass + {2.75f, 1.60f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 1.75f, 2.55f, 3.60f}, // 5 ICS-43434 datasheet response * pink noise + {2.90f, 1.25f, 0.75f, 1.08f, 2.35f, 3.55f, 3.60f, 3.40f, 2.75f, 3.45f, 4.40f, 6.35f, 6.80f, 6.80f, 8.50f, 10.64f}, // 6 ICS-43434 - big speaker, strong bass - { 1.65f, 1.00f, 1.05f, 1.30f, 1.48f, 1.30f, 1.80f, 3.00f, 1.50f, 1.65f, 2.56f, 3.00f, 2.60f, 2.30f, 5.00f, 3.00f }, // 7 SPM1423 - { 2.25f, 1.60f, 1.30f, 1.60f, 2.20f, 3.20f, 3.06f, 2.60f, 2.85f, 3.50f, 4.10f, 4.80f, 5.70f, 6.05f,10.50f,14.85f }, // 8 userdef #1 for ewowi (enhance median/high freqs) - { 4.75f, 3.60f, 2.40f, 2.46f, 3.52f, 1.60f, 1.68f, 3.20f, 2.20f, 2.00f, 2.30f, 2.41f, 2.30f, 1.25f, 4.55f, 6.50f }, // 9 userdef #2 for softhack (mic hidden inside mini-shield) + {1.65f, 1.00f, 1.05f, 1.30f, 1.48f, 1.30f, 1.80f, 3.00f, 1.50f, 1.65f, 2.56f, 3.00f, 2.60f, 2.30f, 5.00f, 3.00f}, // 7 SPM1423 + {2.25f, 1.60f, 1.30f, 1.60f, 2.20f, 3.20f, 3.06f, 2.60f, 2.85f, 3.50f, 4.10f, 4.80f, 5.70f, 6.05f, 10.50f, 14.85f}, // 8 userdef #1 for ewowi (enhance median/high freqs) + {4.75f, 3.60f, 2.40f, 2.46f, 3.52f, 1.60f, 1.68f, 3.20f, 2.20f, 2.00f, 2.30f, 2.41f, 2.30f, 1.25f, 4.55f, 6.50f}, // 9 userdef #2 for softhack (mic hidden inside mini-shield) - { 2.38f, 2.18f, 2.07f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.95f, 1.70f, 2.13f, 2.47f } // 10 almost FLAT (IMNP441 but no PINK noise adjustments) + {2.38f, 2.18f, 2.07f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.95f, 1.70f, 2.13f, 2.47f} // 10 almost FLAT (IMNP441 but no PINK noise adjustments) }; /* how to make your own profile: - * =============================== + * =============================== * preparation: make sure your microphone has direct line-of-sigh with the speaker, 1-2meter distance is best * Prepare your HiFi equipment: disable all "Sound enhancements" - like Loudness, Equalizer, Bass Boost. Bass/Treble controls set to middle. * Your HiFi equipment should receive its audio input from Line-In, SPDIF, HDMI, or another "undistorted" connection (like CDROM). @@ -463,148 +463,153 @@ static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = { */ // globals and FFT Output variables shared with animations -static float FFT_MajPeakSmth = 1.0f; // FFT: (peak) frequency, smooth -#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) -static float fftTaskCycle = 0; // avg cycle time for FFT task -static float fftTime = 0; // avg time for single FFT -static float sampleTime = 0; // avg (blocked) time for reading I2S samples -static float filterTime = 0; // avg time for filtering I2S samples -#endif +static float FFT_MajPeakSmth = 1.0f; // FFT: (peak) frequency, smooth + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + static float fftTaskCycle = 0; // avg cycle time for FFT task + static float fftTime = 0; // avg time for single FFT + static float sampleTime = 0; // avg (blocked) time for reading I2S samples + static float filterTime = 0; // avg time for filtering I2S samples + #endif // FFT Task variables (filtering and post-processing) -static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32C6) -// 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 -#ifndef WLEDMM_FASTPATH -#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling -#else - #ifdef FFT_USE_SLIDING_WINDOW - #define FFT_MIN_CYCLE 8 // we only have 12ms to take 1/2 batch of samples +static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) + + #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32C6) + // 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 + #ifndef WLEDMM_FASTPATH + #define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling + #else + #ifdef FFT_USE_SLIDING_WINDOW + #define FFT_MIN_CYCLE 8 // we only have 12ms to take 1/2 batch of samples + #else + #define FFT_MIN_CYCLE 15 // reduce min time, to allow faster catch-up when I2S is lagging + #endif + #endif + //#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 #else - #define FFT_MIN_CYCLE 15 // reduce min time, to allow faster catch-up when I2S is lagging + // slightly lower the sampling rate for -C3, to improve stability + //constexpr SRate_t SAMPLE_RATE = 20480; // 20Khz; Physical sample time -> 25ms + //#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. + constexpr SRate_t SAMPLE_RATE = 18000; // 18Khz; Physical sample time -> 28ms + #define FFT_MIN_CYCLE 25 // minimum time before FFT task is repeated. + // try 16Khz in case your device still lags and responds too slowly. + //constexpr SRate_t SAMPLE_RATE = 16000; // 16Khz -> Physical sample time -> 32ms + //#define FFT_MIN_CYCLE 30 // minimum time before FFT task is repeated. #endif -#endif -//#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 -#else -// slightly lower the sampling rate for -C3, to improve stability -//constexpr SRate_t SAMPLE_RATE = 20480; // 20Khz; Physical sample time -> 25ms -//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. -constexpr SRate_t SAMPLE_RATE = 18000; // 18Khz; Physical sample time -> 28ms -#define FFT_MIN_CYCLE 25 // minimum time before FFT task is repeated. -// try 16Khz in case your device still lags and responds too slowly. -//constexpr SRate_t SAMPLE_RATE = 16000; // 16Khz -> Physical sample time -> 32ms -//#define FFT_MIN_CYCLE 30 // minimum time before FFT task is repeated. -#endif // 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; // meaningful 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 FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging -#define LOG_256 5.54517744f // log(256) +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; // meaningful 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 FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging + #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 +static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins +static float* vImag = nullptr; // imaginary parts -#ifdef FFT_MAJORPEAK_HUMAN_EAR -static float* pinkFactors = nullptr; // "pink noise" correction factors -constexpr float pinkcenter = 23.66; // sqrt(560) - center freq for scaling is 560 hz. -constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range of each FFT result bin -#endif + #ifdef FFT_MAJORPEAK_HUMAN_EAR + static float* pinkFactors = nullptr; // "pink noise" correction factors + constexpr float pinkcenter = 23.66; // sqrt(560) - center freq for scaling is 560 hz. + constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range of each FFT result bin + #endif -// Create FFT object -// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 -#if !defined(SOC_CPU_HAS_FPU) -// these options actually cause slow-down on -S2 (-S2 doesn't have floating point hardware) -//#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and a few other speedups - WLEDMM not faster on ESP32 -//#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - WLEDMM slower on ESP32 -#endif -#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION) -#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 -#include + // Create FFT object + // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 + #if !defined(SOC_CPU_HAS_FPU) + // these options actually cause slow-down on -S2 (-S2 doesn't have floating point hardware) + //#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and a few other speedups - WLEDMM not faster on ESP32 + //#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - WLEDMM slower on ESP32 + #endif + #define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION) + #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 + #include // Helper functions // compute average of several FFT result bins // linear average static float fftAddAvgLin(int from, int to) { float result = 0.0f; - for (int i = from; i <= to; i++) { - result += vReal[i]; - } + for (int i = from; i <= to; i++) { result += vReal[i]; } return result / float(to - from + 1); } // RMS average static float fftAddAvgRMS(int from, int to) { double result = 0.0; - for (int i = from; i <= to; i++) { - result += vReal[i] * vReal[i]; - } + for (int i = from; i <= to; i++) { result += vReal[i] * vReal[i]; } return sqrtf(result / float(to - from + 1)); } static float fftAddAvg(int from, int to) { - if (from == to) return vReal[from]; // small optimization - if (averageByRMS) return fftAddAvgRMS(from, to); // use RMS - else return fftAddAvgLin(from, to); // use linear average + if (from == to) return vReal[from]; // small optimization + if (averageByRMS) return fftAddAvgRMS(from, to); // use RMS + else return fftAddAvgLin(from, to); // use linear average } -#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) constexpr bool skipSecondFFT = true; -#else + #else constexpr bool skipSecondFFT = false; -#endif + #endif // allocate FFT sample buffers from heap static bool alocateFFTBuffers(void) { #ifdef SR_DEBUG - USER_PRINT(F("\nFree heap ")); USER_PRINTLN(ESP.getFreeHeap()); + USER_PRINT(F("\nFree heap ")); + USER_PRINTLN(ESP.getFreeHeap()); #endif - if (vReal) d_free(vReal); vReal = nullptr; // should not happen - if (vImag) d_free(vImag); vImag = nullptr; // should not happen - if ((vReal = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die - if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; -#ifdef FFT_MAJORPEAK_HUMAN_EAR - if (pinkFactors) p_free(pinkFactors); - if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) return false; -#endif + if (vReal) d_free(vReal); + vReal = nullptr; // should not happen + if (vImag) d_free(vImag); + vImag = nullptr; // should not happen + if ((vReal = (float*)d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die + if ((vImag = (float*)d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; + #ifdef FFT_MAJORPEAK_HUMAN_EAR + if (pinkFactors) p_free(pinkFactors); + if ((pinkFactors = (float*)p_calloc(samplesFFT, sizeof(float))) == nullptr) return false; + #endif #ifdef SR_DEBUG USER_PRINTLN("\nalocateFFTBuffers() completed successfully."); - USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); - USER_PRINT("FFTtask free stack: "); USER_PRINTLN(uxTaskGetStackHighWaterMark(NULL)); + USER_PRINT(F("Free heap: ")); + USER_PRINTLN(ESP.getFreeHeap()); + USER_PRINT("FFTtask free stack: "); + USER_PRINTLN(uxTaskGetStackHighWaterMark(NULL)); USER_FLUSH(); #endif - return(true); // success + return (true); // success } // de-allocate FFT sample buffers from heap static void destroyFFTBuffers(bool panicOOM) { #ifdef FFT_MAJORPEAK_HUMAN_EAR - if (pinkFactors) p_free(pinkFactors); pinkFactors = nullptr; -#endif - if (vImag) d_free(vImag); vImag = nullptr; - if (vReal) d_free(vReal); vReal = nullptr; + if (pinkFactors) p_free(pinkFactors); + pinkFactors = nullptr; + #endif + if (vImag) d_free(vImag); + vImag = nullptr; + if (vReal) d_free(vReal); + vReal = nullptr; - if (panicOOM && !isOOM) { // notify user - isOOM = true; + if (panicOOM && !isOOM) { // notify user + isOOM = true; errorFlag = ERR_LOW_MEM; USER_PRINTLN("AR startup failed - out of memory!"); } #ifdef SR_DEBUG USER_PRINTLN("\ndestroyFFTBuffers() completed successfully."); - USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); + USER_PRINT(F("Free heap: ")); + USER_PRINTLN(ESP.getFreeHeap()); USER_FLUSH(); #endif } @@ -612,356 +617,374 @@ static void destroyFFTBuffers(bool panicOOM) { // High-Pass "DC blocker" filter // see https://www.dsprelated.com/freebooks/filters/DC_Blocker.html -static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) { - constexpr float filterR = 0.990f; // around 40hz - static float xm1 = 0.0f; +static void runDCBlocker(uint_fast16_t numSamples, float* sampleBuffer) { + constexpr float filterR = 0.990f; // around 40hz + static float xm1 = 0.0f; static SR_HIRES_TYPE ym1 = 0.0f; - for (unsigned i=0; i < numSamples; i++) { - float value = sampleBuffer[i]; - SR_HIRES_TYPE filtered = (SR_HIRES_TYPE)(value-xm1) + filterR*ym1; - xm1 = value; - ym1 = filtered; - sampleBuffer[i] = filtered; - } + for (unsigned i = 0; i < numSamples; i++) { + float value = sampleBuffer[i]; + SR_HIRES_TYPE filtered = (SR_HIRES_TYPE)(value - xm1) + filterR * ym1; + xm1 = value; + ym1 = filtered; + sampleBuffer[i] = filtered; + } } -// + +// // FFT runner - "return" from a task function causes crash. This wrapper keeps the task alive // -void runFFTcode(void * parameter) __attribute__((noreturn,used)); -void runFFTcode(void * parameter) { - bool firstFail = true; // prevents flood of warnings +void runFFTcode(void* parameter) __attribute__((noreturn, used)); +void runFFTcode(void* parameter) { + bool firstFail = true; // prevents flood of warnings do { if (!disableSoundProcessing) { FFTcode(parameter); - if (firstFail) {USER_PRINTLN(F("warning: unexpected exit of FFT main task."));} + if (firstFail) { USER_PRINTLN(F("warning: unexpected exit of FFT main task.")); } firstFail = false; - } else firstFail = true; // re-enable warning message - vTaskDelay(1000); // if we arrive here, FFcode has returned due to OOM. Wait a bit, then try again. - } while (true); + } else firstFail = true; // re-enable warning message + vTaskDelay(1000); // if we arrive here, FFcode has returned due to OOM. Wait a bit, then try again. + } while (true); } // // FFT main task // -void FFTcode(void * parameter) -{ +void FFTcode(void* parameter) { #ifdef SR_DEBUG USER_FLUSH(); - USER_PRINT("AR: "); USER_PRINT(pcTaskGetTaskName(NULL)); - USER_PRINT(" task started on core "); USER_PRINT(xPortGetCoreID()); // causes trouble on -S2 - USER_PRINT(" [prio="); USER_PRINT(uxTaskPriorityGet(NULL)); - USER_PRINT(", min free stack="); USER_PRINT(uxTaskGetStackHighWaterMark(NULL)); - USER_PRINTLN("]"); USER_FLUSH(); + USER_PRINT("AR: "); + USER_PRINT(pcTaskGetTaskName(NULL)); + USER_PRINT(" task started on core "); + USER_PRINT(xPortGetCoreID()); // causes trouble on -S2 + USER_PRINT(" [prio="); + USER_PRINT(uxTaskPriorityGet(NULL)); + USER_PRINT(", min free stack="); + USER_PRINT(uxTaskGetStackHighWaterMark(NULL)); + USER_PRINTLN("]"); + USER_FLUSH(); #endif // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; - const TickType_t xFrequencyDouble = FFT_MIN_CYCLE * portTICK_PERIOD_MS * 2; - static bool isFirstRun = false; + const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; + const TickType_t xFrequencyDouble = FFT_MIN_CYCLE * portTICK_PERIOD_MS * 2; + static bool isFirstRun = false; -#ifdef FFT_USE_SLIDING_WINDOW - static float* oldSamples = nullptr; // previous 50% of samples - static bool haveOldSamples = false; // for sliding window FFT - bool usingOldSamples = false; - if (!oldSamples) oldSamples = (float*) d_calloc(samplesFFT_2, sizeof(float)); // allocate on first run - if (!oldSamples) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(true); return; } // no memory -> die -#endif + #ifdef FFT_USE_SLIDING_WINDOW + static float* oldSamples = nullptr; // previous 50% of samples + static bool haveOldSamples = false; // for sliding window FFT + bool usingOldSamples = false; + if (!oldSamples) oldSamples = (float*)d_calloc(samplesFFT_2, sizeof(float)); // allocate on first run + if (!oldSamples) { + disableSoundProcessing = true; + haveOldSamples = false; + destroyFFTBuffers(true); + return; + } // no memory -> die + #endif bool success = true; - if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run + if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run if (success == false) { // no memory -> clean up heap, then suspend disableSoundProcessing = true; destroyFFTBuffers(true); - return; + return; } // create FFT object - we have to do if after allocating buffers -#if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 - // arduinoFFT 2.x has a slightly different API - static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); -#else - // recommended version optimized by @softhack007 (API version 1.9) - #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) - static float* windowWeighingFactors = nullptr; - if (!windowWeighingFactors) windowWeighingFactors = (float*) d_calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap - if (!windowWeighingFactors) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(true); return; } // alloc failed + #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + static ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE, true); #else - static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM + // recommended version optimized by @softhack007 (API version 1.9) + #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) + static float* windowWeighingFactors = nullptr; + if (!windowWeighingFactors) windowWeighingFactors = (float*)d_calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap + if (!windowWeighingFactors) { + disableSoundProcessing = true; + haveOldSamples = false; + destroyFFTBuffers(true); + return; + } // alloc failed + #else + static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM + #endif + static ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); #endif - static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); -#endif #ifdef FFT_MAJORPEAK_HUMAN_EAR - // pre-compute pink noise scaling table - for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) { - float binFreq = binInd * binWidth + binWidth/2.0f; - if (binFreq > (SAMPLE_RATE * 0.42f)) - binFreq = (SAMPLE_RATE * 0.42f) - 0.25 * (binFreq - (SAMPLE_RATE * 0.42f)); // suppress noise and aliasing - pinkFactors[binInd] = sqrtf(binFreq) / pinkcenter; - } - pinkFactors[0] *= 0.5; // suppress 0-42hz bin + // pre-compute pink noise scaling table + for (uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) { + float binFreq = binInd * binWidth + binWidth / 2.0f; + if (binFreq > (SAMPLE_RATE * 0.42f)) binFreq = (SAMPLE_RATE * 0.42f) - 0.25 * (binFreq - (SAMPLE_RATE * 0.42f)); // suppress noise and aliasing + pinkFactors[binInd] = sqrtf(binFreq) / pinkcenter; + } + pinkFactors[0] *= 0.5; // suppress 0-42hz bin #endif 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. + 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 == AUDIOSYNC_REC)) { isFirstRun = false; - #ifdef FFT_USE_SLIDING_WINDOW - haveOldSamples = false; - #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + #ifdef FFT_USE_SLIDING_WINDOW + haveOldSamples = false; + #endif + vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers continue; } -#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) // timing - uint64_t start = esp_timer_get_time(); - bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid + uint64_t start = esp_timer_get_time(); + bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid static uint64_t lastCycleStart = 0; - static uint64_t lastLastTime = 0; - if ((lastCycleStart > 0) && (lastCycleStart < start)) { // filter out overflows - uint64_t taskTimeInMillis = ((start - lastCycleStart) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTaskCycle = (((taskTimeInMillis + lastLastTime)/2) *4 + fftTaskCycle*6)/10.0; // smart smooth - lastLastTime = taskTimeInMillis; + static uint64_t lastLastTime = 0; + if ((lastCycleStart > 0) && (lastCycleStart < start)) { // filter out overflows + uint64_t taskTimeInMillis = ((start - lastCycleStart) + 5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTaskCycle = (((taskTimeInMillis + lastLastTime) / 2) * 4 + fftTaskCycle * 6) / 10.0; // smart smooth + lastLastTime = taskTimeInMillis; } lastCycleStart = start; -#endif + #endif // get a fresh batch of samples from I2S - memset(vReal, 0, sizeof(float) * samplesFFT); // start clean -#ifdef FFT_USE_SLIDING_WINDOW + memset(vReal, 0, sizeof(float) * samplesFFT); // start clean + #ifdef FFT_USE_SLIDING_WINDOW uint16_t readOffset; if (haveOldSamples && (doSlidingFFT > 0)) { - memcpy(vReal, oldSamples, sizeof(float) * samplesFFT_2); // copy first 50% from buffer + memcpy(vReal, oldSamples, sizeof(float) * samplesFFT_2); // copy first 50% from buffer usingOldSamples = true; - readOffset = samplesFFT_2; + readOffset = samplesFFT_2; } else { usingOldSamples = false; - readOffset = 0; + readOffset = 0; } // read fresh samples, in chunks of 50% do { // this looks a bit cumbersome, but it onlyworks this way - any second instance of the getSamples() call delivers junk data. - if (audioSource) audioSource->getSamples(vReal+readOffset, samplesFFT_2); + if (audioSource) audioSource->getSamples(vReal + readOffset, samplesFFT_2); readOffset += samplesFFT_2; } while (readOffset < samplesFFT); -#else + #else if (audioSource) audioSource->getSamples(vReal, samplesFFT); -#endif + #endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) // debug info in case that stack usage changes static unsigned int minStackFree = UINT32_MAX; - unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); if (minStackFree > stackFree) { minStackFree = stackFree; - DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minStackFree); //WLEDMM + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minStackFree); //WLEDMM } // timing - 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.0; // smooth + 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.0; // smooth } - start = esp_timer_get_time(); // start measuring filter time -#endif + start = esp_timer_get_time(); // start measuring filter time + #endif - xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay - isFirstRun = !isFirstRun; // toggle throttle + xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + isFirstRun = !isFirstRun; // toggle throttle -#ifdef MIC_LOGGER - float datMin = 0.0f; - float datMax = 0.0f; + #ifdef MIC_LOGGER + float datMin = 0.0f; + float datMax = 0.0f; double datAvg = 0.0f; - for (int i=0; i < samplesFFT; i++) { - if (i==0) { + for (int i = 0; i < samplesFFT; i++) { + if (i == 0) { datMin = datMax = vReal[i]; } else { - if (datMin > vReal[i]) datMin = vReal[i]; - if (datMax < vReal[i]) datMax = vReal[i]; + if (datMin > vReal[i]) datMin = vReal[i]; + if (datMax < vReal[i]) datMax = vReal[i]; } - datAvg += vReal[i]; + datAvg += vReal[i]; } -#endif + #endif -#if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) - // experimental - be nice to LED update task (trying to avoid flickering) - dual core only -#if FFTTASK_PRIORITY > 1 + #if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) + // experimental - be nice to LED update task (trying to avoid flickering) - dual core only + #if FFTTASK_PRIORITY > 1 if (strip.isServicing()) delay(1); -#endif -#endif + #endif + #endif // normal mode: filter everything - float *samplesStart = vReal; + float* samplesStart = vReal; uint16_t sampleCount = samplesFFT; - #ifdef FFT_USE_SLIDING_WINDOW + #ifdef FFT_USE_SLIDING_WINDOW if (usingOldSamples) { // sliding window mode: only latest 50% need filtering samplesStart = vReal + samplesFFT_2; - sampleCount = samplesFFT_2; + sampleCount = samplesFFT_2; } - #endif + #endif // band pass filter - can reduce noise floor by a factor of 50 // downside: frequencies below 100Hz will be ignored - bool doDCRemoval = false; // DCRemove is only necessary if we don't use any kind of low-cut filtering - if ((useInputFilter > 0) && (useInputFilter < 99)) { - switch(useInputFilter) { - case 1: runMicFilter(sampleCount, samplesStart); break; // PDM microphone bandpass - case 2: runDCBlocker(sampleCount, samplesStart); break; // generic Low-Cut + DC blocker (~40hz cut-off) + bool doDCRemoval = false; // DCRemove is only necessary if we don't use any kind of low-cut filtering + if ((useInputFilter > 0) && (useInputFilter < 99)) { + switch (useInputFilter) { + case 1: runMicFilter(sampleCount, samplesStart); break; // PDM microphone bandpass + case 2: runDCBlocker(sampleCount, samplesStart); break; // generic Low-Cut + DC blocker (~40hz cut-off) default: doDCRemoval = true; break; } } else doDCRemoval = true; -#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) // timing measurement - if (start < esp_timer_get_time()) { // filter out overflows - uint64_t filterTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding - filterTime = (filterTimeInMillis*3 + filterTime*7)/10.0; // smooth + if (start < esp_timer_get_time()) { // filter out overflows + uint64_t filterTimeInMillis = (esp_timer_get_time() - start + 5ULL) / 10ULL; // "+5" to ensure proper rounding + filterTime = (filterTimeInMillis * 3 + filterTime * 7) / 10.0; // smooth } - start = esp_timer_get_time(); // start measuring FFT time -#endif + start = esp_timer_get_time(); // start measuring FFT time + #endif // set imaginary parts to 0 memset(vImag, 0, sizeof(float) * samplesFFT); #ifdef FFT_USE_SLIDING_WINDOW - memcpy(oldSamples, vReal+samplesFFT_2, sizeof(float) * samplesFFT_2); // copy last 50% to buffer (for sliding window FFT) - haveOldSamples = true; + memcpy(oldSamples, vReal + samplesFFT_2, sizeof(float) * samplesFFT_2); // copy last 50% to buffer (for sliding window FFT) + haveOldSamples = true; #endif // find the highest sample in the batch, and count zero crossings - float maxSample = 0.0f; // max sample from FFT batch + float maxSample = 0.0f; // max sample from FFT batch uint_fast16_t newZeroCrossingCount = 0; - for (int i=0; i < samplesFFT; i++) { - // pick 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 - #ifdef FFT_USE_SLIDING_WINDOW + for (int i = 0; i < samplesFFT; i++) { + // pick 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 + #ifdef FFT_USE_SLIDING_WINDOW if (usingOldSamples) { if ((i >= samplesFFT_2) && (fabsf(vReal[i]) > maxSample)) maxSample = fabsf(vReal[i]); // only look at newest 50% } else - #endif + #endif if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); } // WLED-MM/TroyHacks: Calculate zero crossings // - if (i < (samplesFFT-1)) { - if (__builtin_signbit(vReal[i]) != __builtin_signbit(vReal[i+1])) // test sign bit: sign changed -> zero crossing - newZeroCrossingCount++; + if (i < (samplesFFT - 1)) { + if (__builtin_signbit(vReal[i]) != __builtin_signbit(vReal[i + 1])) // test sign bit: sign changed -> zero crossing + newZeroCrossingCount++; } } - newZeroCrossingCount = (newZeroCrossingCount*2)/3; // reduce value so it typically stays below 256 - zeroCrossingCount = newZeroCrossingCount; // update only once, to avoid that effects pick up an intermediate value + newZeroCrossingCount = (newZeroCrossingCount * 2) / 3; // reduce value so it typically stays below 256 + zeroCrossingCount = newZeroCrossingCount; // update only once, to avoid that effects pick up an intermediate value // 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 MIC_LOGGER - micReal_min = datMin; - micReal_max = datMax; - micReal_avg = datAvg / samplesFFT; -#if 0 - // compute min/max again after filtering - useful for filter debugging - for (int i=0; i < samplesFFT; i++) { - if (i==0) { - datMin = datMax = vReal[i]; - } else { - if (datMin > vReal[i]) datMin = vReal[i]; - if (datMax < vReal[i]) datMax = vReal[i]; + #ifdef MIC_LOGGER + micReal_min = datMin; + micReal_max = datMax; + micReal_avg = datAvg / samplesFFT; + #if 0 + // compute min/max again after filtering - useful for filter debugging + for (int i=0; i < samplesFFT; i++) { + if (i==0) { + datMin = datMax = vReal[i]; + } else { + if (datMin > vReal[i]) datMin = vReal[i]; + if (datMax < vReal[i]) datMax = vReal[i]; + } } - } - micReal_min2 = datMin; - micReal_max2 = datMax; -#endif -#endif + micReal_min2 = datMin; + micReal_max2 = datMax; + #endif + #endif - float wc = 1.0; // FFT window correction factor, relative to Blackman_Harris + float wc = 1.0; // FFT window correction factor, relative to Blackman_Harris // run FFT (takes 3-5ms on ESP32) - if (fabsf(volumeSmth) > 0.25f) { // noise gate open + if (fabsf(volumeSmth) > 0.25f) { // noise gate open if ((skipSecondFFT == false) || (isFirstRun == true)) { // run FFT (takes 2-3ms on ESP32, ~12ms on ESP32-S2, ~30ms on -C3) - if (doDCRemoval) FFT.dcRemoval(); // remove DC offset - switch(fftWindow) { // apply FFT window + if (doDCRemoval) FFT.dcRemoval(); // remove DC offset + switch (fftWindow) { // apply FFT window case 1: FFT.windowing(FFTWindow::Hann, FFTDirection::Forward); // recommended for 50% overlap - wc = 0.66415918066; // 1.8554726898 * 2.0 - break; + wc = 0.66415918066; // 1.8554726898 * 2.0 + break; case 2: - FFT.windowing( FFTWindow::Nuttall, FFTDirection::Forward); - wc = 0.9916873881f; // 2.8163172034 * 2.0 - break; + FFT.windowing(FFTWindow::Nuttall, FFTDirection::Forward); + wc = 0.9916873881f; // 2.8163172034 * 2.0 + break; case 5: - FFT.windowing( FFTWindow::Blackman, FFTDirection::Forward); - wc = 0.84762867875f; // 2.3673474360 * 2.0 - break; + FFT.windowing(FFTWindow::Blackman, FFTDirection::Forward); + wc = 0.84762867875f; // 2.3673474360 * 2.0 + break; case 3: - FFT.windowing( FFTWindow::Hamming, FFTDirection::Forward); - wc = 0.664159180663f; // 1.8549343278 * 2.0 - break; + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); + wc = 0.664159180663f; // 1.8549343278 * 2.0 + break; case 4: - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude preservation, low frequency accuracy - wc = 1.276771793156f; // 3.5659039231 * 2.0 - break; - case 0: // falls through + FFT.windowing(FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude preservation, low frequency accuracy + wc = 1.276771793156f; // 3.5659039231 * 2.0 + break; + case 0: // falls through default: FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman - Harris" window - sharp peaks due to excellent sideband rejection - wc = 1.0f; // 2.7929062517 * 2.0 + wc = 1.0f; // 2.7929062517 * 2.0 } #ifdef FFT_USE_SLIDING_WINDOW - if (usingOldSamples) wc = wc * 1.10f; // compensate for loss caused by averaging + if (usingOldSamples) wc = wc * 1.10f; // compensate for loss caused by averaging #endif - 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.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. float last_majorpeak = FFT_MajorPeak; float last_magnitude = FFT_Magnitude; #ifdef FFT_MAJORPEAK_HUMAN_EAR - // scale FFT results - for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) - vReal[binInd] *= pinkFactors[binInd]; + // scale FFT results + for (uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) vReal[binInd] *= pinkFactors[binInd]; #endif #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 // arduinoFFT 2.x has a slightly different API FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); #else - FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant + FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant #endif FFT_Magnitude *= wc; // apply correction factor - if (FFT_MajorPeak < (SAMPLE_RATE / samplesFFT)) {FFT_MajorPeak = 1.0f; FFT_Magnitude = 0;} // too low - use zero - if (FFT_MajorPeak > (0.42f * SAMPLE_RATE)) {FFT_MajorPeak = last_majorpeak; FFT_Magnitude = last_magnitude;} // too high - keep last peak + if (FFT_MajorPeak < (SAMPLE_RATE / samplesFFT)) { + FFT_MajorPeak = 1.0f; + FFT_Magnitude = 0; + } // too low - use zero + if (FFT_MajorPeak > (0.42f * SAMPLE_RATE)) { + FFT_MajorPeak = last_majorpeak; + FFT_Magnitude = last_magnitude; + } // too high - keep last peak #ifdef FFT_MAJORPEAK_HUMAN_EAR - // undo scaling - we want unmodified values for FFTResult[] computations - for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) - vReal[binInd] *= 1.0f/pinkFactors[binInd]; - //fix peak magnitude - if ((FFT_MajorPeak > (binWidth/1.25f)) && (FFT_MajorPeak < (SAMPLE_RATE/2.2f)) && (FFT_Magnitude > 4.0f)) { - unsigned peakBin = constrain((int)((FFT_MajorPeak + binWidth/2.0f) / binWidth), 0, samplesFFT -1); - FFT_Magnitude *= fmaxf(1.0f/pinkFactors[peakBin], 1.0f); - } + // undo scaling - we want unmodified values for FFTResult[] computations + for (uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) vReal[binInd] *= 1.0f / pinkFactors[binInd]; + //fix peak magnitude + if ((FFT_MajorPeak > (binWidth / 1.25f)) && (FFT_MajorPeak < (SAMPLE_RATE / 2.2f)) && (FFT_Magnitude > 4.0f)) { + unsigned peakBin = constrain((int)((FFT_MajorPeak + binWidth / 2.0f) / binWidth), 0, samplesFFT - 1); + FFT_Magnitude *= fmaxf(1.0f / pinkFactors[peakBin], 1.0f); + } #endif - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42 * (FFT_MajorPeak - FFT_MajPeakSmth); // I like this "swooping peak" look + FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42 * (FFT_MajorPeak - FFT_MajPeakSmth); // I like this "swooping peak" look - } else { // skip second run --> clear fft results, keep peaks - memset(vReal, 0, sizeof(float) * samplesFFT); + } else { // skip second run --> clear fft results, keep peaks + memset(vReal, 0, sizeof(float) * samplesFFT); } -#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) - haveDoneFFT = true; -#endif + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + haveDoneFFT = true; + #endif - } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. + } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. memset(vReal, 0, sizeof(float) * samplesFFT); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; @@ -969,258 +992,259 @@ void FFTcode(void * parameter) if ((skipSecondFFT == false) || (isFirstRun == true)) { 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() + 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.25f) { // noise gate open - if (fabsf(volumeSmth) > 0.25f) { // noise gate open + if (fabsf(volumeSmth) > 0.25f) { // noise gate open //WLEDMM: different distributions if (freqDist == 0) { /* new mapping, optimized for 22050 Hz by softhack007 --- update: removed overlap */ - // bins frequency range - if (useInputFilter==1) { + // bins frequency range + if (useInputFilter == 1) { // skip frequencies below 100hz - fftCalc[ 0] = wc * 0.8f * fftAddAvg(3,3); - fftCalc[ 1] = wc * 0.9f * fftAddAvg(4,4); - fftCalc[ 2] = wc * fftAddAvg(5,5); - fftCalc[ 3] = wc * fftAddAvg(6,6); - // don't use the last bins from 206 to 255. - fftCalc[15] = wc * fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + fftCalc[0] = wc * 0.8f * fftAddAvg(3, 3); + fftCalc[1] = wc * 0.9f * fftAddAvg(4, 4); + fftCalc[2] = wc * fftAddAvg(5, 5); + fftCalc[3] = wc * fftAddAvg(6, 6); + // don't use the last bins from 206 to 255. + fftCalc[15] = wc * fftAddAvg(165, 205) * 0.75f; // 40 7106 - 8828 high -- with some damping } else { - fftCalc[ 0] = wc * fftAddAvg(1,1); // 1 43 - 86 sub-bass - fftCalc[ 1] = wc * fftAddAvg(2,2); // 1 86 - 129 bass - fftCalc[ 2] = wc * fftAddAvg(3,4); // 2 129 - 216 bass - fftCalc[ 3] = wc * fftAddAvg(5,6); // 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] = wc * fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + fftCalc[0] = wc * fftAddAvg(1, 1); // 1 43 - 86 sub-bass + fftCalc[1] = wc * fftAddAvg(2, 2); // 1 86 - 129 bass + fftCalc[2] = wc * fftAddAvg(3, 4); // 2 129 - 216 bass + fftCalc[3] = wc * fftAddAvg(5, 6); // 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] = wc * fftAddAvg(165, 215) * 0.70f; // 50 7106 - 9259 high -- with some damping } - fftCalc[ 4] = wc * fftAddAvg(7,9); // 3 301 - 430 midrange - fftCalc[ 5] = wc * fftAddAvg(10,12); // 3 430 - 560 midrange - fftCalc[ 6] = wc * fftAddAvg(13,18); // 5 560 - 818 midrange - fftCalc[ 7] = wc * fftAddAvg(19,25); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = wc * fftAddAvg(26,32); // 7 1120 - 1421 midrange - fftCalc[ 9] = wc * fftAddAvg(33,43); // 9 1421 - 1895 midrange - fftCalc[10] = wc * fftAddAvg(44,55); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = wc * fftAddAvg(56,69); // 14 2412 - 3015 high mid - fftCalc[12] = wc * fftAddAvg(70,85); // 16 3015 - 3704 high mid - fftCalc[13] = wc * fftAddAvg(86,103); // 18 3704 - 4479 high mid - fftCalc[14] = wc * fftAddAvg(104,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping - } else if (freqDist == 1) { //WLEDMM: Rightshift: note ewowi: frequencies in comments are not correct - if (useInputFilter==1) { - // skip frequencies below 100hz - fftCalc[ 0] = wc * 0.8f * fftAddAvg(1,1); - fftCalc[ 1] = wc * 0.9f * fftAddAvg(2,2); - fftCalc[ 2] = wc * fftAddAvg(3,3); - fftCalc[ 3] = wc * fftAddAvg(4,4); - // don't use the last bins from 206 to 255. - fftCalc[15] = wc * fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = wc * fftAddAvg(1,1); // 1 43 - 86 sub-bass - fftCalc[ 1] = wc * fftAddAvg(2,2); // 1 86 - 129 bass - fftCalc[ 2] = wc * fftAddAvg(3,3); // 2 129 - 216 bass - fftCalc[ 3] = wc * fftAddAvg(4,4); // 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] = wc * fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + fftCalc[4] = wc * fftAddAvg(7, 9); // 3 301 - 430 midrange + fftCalc[5] = wc * fftAddAvg(10, 12); // 3 430 - 560 midrange + fftCalc[6] = wc * fftAddAvg(13, 18); // 5 560 - 818 midrange + fftCalc[7] = wc * fftAddAvg(19, 25); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[8] = wc * fftAddAvg(26, 32); // 7 1120 - 1421 midrange + fftCalc[9] = wc * fftAddAvg(33, 43); // 9 1421 - 1895 midrange + fftCalc[10] = wc * fftAddAvg(44, 55); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = wc * fftAddAvg(56, 69); // 14 2412 - 3015 high mid + fftCalc[12] = wc * fftAddAvg(70, 85); // 16 3015 - 3704 high mid + fftCalc[13] = wc * fftAddAvg(86, 103); // 18 3704 - 4479 high mid + fftCalc[14] = wc * fftAddAvg(104, 164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping + } else if (freqDist == 1) { //WLEDMM: Rightshift: note ewowi: frequencies in comments are not correct + if (useInputFilter == 1) { + // skip frequencies below 100hz + fftCalc[0] = wc * 0.8f * fftAddAvg(1, 1); + fftCalc[1] = wc * 0.9f * fftAddAvg(2, 2); + fftCalc[2] = wc * fftAddAvg(3, 3); + fftCalc[3] = wc * fftAddAvg(4, 4); + // don't use the last bins from 206 to 255. + fftCalc[15] = wc * fftAddAvg(165, 205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[0] = wc * fftAddAvg(1, 1); // 1 43 - 86 sub-bass + fftCalc[1] = wc * fftAddAvg(2, 2); // 1 86 - 129 bass + fftCalc[2] = wc * fftAddAvg(3, 3); // 2 129 - 216 bass + fftCalc[3] = wc * fftAddAvg(4, 4); // 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] = wc * fftAddAvg(165, 215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[4] = wc * fftAddAvg(5, 6); // 3 301 - 430 midrange + fftCalc[5] = wc * fftAddAvg(7, 8); // 3 430 - 560 midrange + fftCalc[6] = wc * fftAddAvg(9, 10); // 5 560 - 818 midrange + fftCalc[7] = wc * fftAddAvg(11, 13); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[8] = wc * fftAddAvg(14, 18); // 7 1120 - 1421 midrange + fftCalc[9] = wc * fftAddAvg(19, 25); // 9 1421 - 1895 midrange + fftCalc[10] = wc * fftAddAvg(26, 36); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = wc * fftAddAvg(37, 45); // 14 2412 - 3015 high mid + fftCalc[12] = wc * fftAddAvg(46, 66); // 16 3015 - 3704 high mid + fftCalc[13] = wc * fftAddAvg(67, 97); // 18 3704 - 4479 high mid + fftCalc[14] = wc * fftAddAvg(98, 164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping + } + } else { // noise gate closed - just decay old values + isFirstRun = false; + 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; } - fftCalc[ 4] = wc * fftAddAvg(5,6); // 3 301 - 430 midrange - fftCalc[ 5] = wc * fftAddAvg(7,8); // 3 430 - 560 midrange - fftCalc[ 6] = wc * fftAddAvg(9,10); // 5 560 - 818 midrange - fftCalc[ 7] = wc * fftAddAvg(11,13); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = wc * fftAddAvg(14,18); // 7 1120 - 1421 midrange - fftCalc[ 9] = wc * fftAddAvg(19,25); // 9 1421 - 1895 midrange - fftCalc[10] = wc * fftAddAvg(26,36); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = wc * fftAddAvg(37,45); // 14 2412 - 3015 high mid - fftCalc[12] = wc * fftAddAvg(46,66); // 16 3015 - 3704 high mid - fftCalc[13] = wc * fftAddAvg(67,97); // 18 3704 - 4479 high mid - fftCalc[14] = wc * fftAddAvg(98,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping } - } else { // noise gate closed - just decay old values - isFirstRun = false; - 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; - } } - memcpy(lastFftCalc, fftCalc, sizeof(lastFftCalc)); // make a backup of last "good" channels + memcpy(lastFftCalc, fftCalc, sizeof(lastFftCalc)); // make a backup of last "good" channels - } else { // if second run skipped - memcpy(fftCalc, lastFftCalc, sizeof(fftCalc)); // restore last "good" channels + } else { // if second run skipped + memcpy(fftCalc, lastFftCalc, sizeof(fftCalc)); // restore last "good" channels } // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) if (pinkIndex > MAX_PINK) pinkIndex = MAX_PINK; -#ifdef FFT_USE_SLIDING_WINDOW - postProcessFFTResults((fabsf(volumeSmth) > 0.25f)? true : false, NUM_GEQ_CHANNELS, usingOldSamples); // this function modifies fftCalc, fftAvg and fftResult -#else - postProcessFFTResults((fabsf(volumeSmth) > 0.25f)? true : false, NUM_GEQ_CHANNELS, false); // this function modifies fftCalc, fftAvg and fftResult -#endif + #ifdef FFT_USE_SLIDING_WINDOW + postProcessFFTResults((fabsf(volumeSmth) > 0.25f) ? true : false, NUM_GEQ_CHANNELS, usingOldSamples); // this function modifies fftCalc, fftAvg and fftResult + #else + postProcessFFTResults((fabsf(volumeSmth) > 0.25f) ? true : false, NUM_GEQ_CHANNELS, false); // this function modifies fftCalc, fftAvg and fftResult + #endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) - // timing - static uint64_t lastLastFFT = 0; - 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 + lastLastFFT)/2) *3 + fftTime*7)/10.0; // smart smooth - lastLastFFT = fftTimeInMillis; - } -#endif + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + // timing + static uint64_t lastLastFFT = 0; + 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 + lastLastFFT) / 2) * 3 + fftTime * 7) / 10.0; // smart smooth + lastLastFFT = fftTimeInMillis; + } + #endif // run peak detection autoResetPeak(); detectSamplePeak(); haveNewFFTResult = true; - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC #endif { - #ifdef FFT_USE_SLIDING_WINDOW + #ifdef FFT_USE_SLIDING_WINDOW if (!usingOldSamples) { - vTaskDelayUntil( &xLastWakeTime, xFrequencyDouble); // we need a double wait when no old data was used + vTaskDelayUntil(&xLastWakeTime, xFrequencyDouble); // we need a double wait when no old data was used } else - #endif + #endif if ((skipSecondFFT == false) || (fabsf(volumeSmth) < 0.25f)) { - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers } else if (isFirstRun == true) { - vTaskDelayUntil( &xLastWakeTime, xFrequencyDouble); // release CPU after performing FFT in "skip second run" mode + vTaskDelayUntil(&xLastWakeTime, xFrequencyDouble); // release CPU after performing FFT in "skip second run" mode } } - } // for(;;)ever -} // FFTcode() task end + } // for(;;)ever +} // FFTcode() task end /////////////////////////// // Pre / Postprocessing // /////////////////////////// -static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) +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.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.0; - 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; + constexpr float beta1 = 0.85f; // 20Khz + + constexpr float beta2 = (1.0f - beta1) / 2.0; + 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, bool i2sFastpath) // post-processing and post-amp of GEQ channels +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool i2sFastpath) // 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[pinkIndex][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; - } + for (int i = 0; i < numberOfChannels; i++) { + + if (noiseGateOpen) { // noise gate open + // Adjustment for frequency curves. + fftCalc[i] *= fftResultPink[pinkIndex][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; + } - float speed = 1.0f; // filter correction for sampling speed -> 1.0 in normal mode (43hz) - if (i2sFastpath) speed = 0.6931471805599453094f * 1.1f; // -> ln(2) from math, *1.1 from my gut feeling ;-) in fast mode (86hz) - - if(limiterOn == true) { - // Limiter ON -> smooth results - if(fftCalc[i] > fftAvg[i]) { // rise fast - fftAvg[i] += speed * 0.78f * (fftCalc[i] - fftAvg[i]); // will need approx 1-2 cycles (50ms) for converging against fftCalc[i] - } else { // fall slow - if (decayTime < 150) fftAvg[i] += speed * 0.50f * (fftCalc[i] - fftAvg[i]); - else if (decayTime < 250) fftAvg[i] += speed * 0.40f * (fftCalc[i] - fftAvg[i]); - else if (decayTime < 500) fftAvg[i] += speed * 0.33f * (fftCalc[i] - fftAvg[i]); - else if (decayTime < 1000) fftAvg[i] += speed * 0.22f * (fftCalc[i] - fftAvg[i]); // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] += speed * 0.17f * (fftCalc[i] - fftAvg[i]); // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] += speed * 0.14f * (fftCalc[i] - fftAvg[i]); // approx 14 cycles (350ms) for falling to zero - else if (decayTime < 4000) fftAvg[i] += speed * 0.10f * (fftCalc[i] - fftAvg[i]); - else fftAvg[i] += speed * 0.05f * (fftCalc[i] - fftAvg[i]); - } + float speed = 1.0f; // filter correction for sampling speed -> 1.0 in normal mode (43hz) + if (i2sFastpath) speed = 0.6931471805599453094f * 1.1f; // -> ln(2) from math, *1.1 from my gut feeling ;-) in fast mode (86hz) + + if (limiterOn == true) { + // Limiter ON -> smooth results + if (fftCalc[i] > fftAvg[i]) { // rise fast + fftAvg[i] += speed * 0.78f * (fftCalc[i] - fftAvg[i]); // will need approx 1-2 cycles (50ms) for converging against fftCalc[i] + } else { // fall slow + if (decayTime < 150) fftAvg[i] += speed * 0.50f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 250) fftAvg[i] += speed * 0.40f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 500) fftAvg[i] += speed * 0.33f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 1000) fftAvg[i] += speed * 0.22f * (fftCalc[i] - fftAvg[i]); // approx 5 cycles (225ms) for falling to zero + else if (decayTime < 2000) fftAvg[i] += speed * 0.17f * (fftCalc[i] - fftAvg[i]); // default - approx 9 cycles (225ms) for falling to zero + else if (decayTime < 3000) fftAvg[i] += speed * 0.14f * (fftCalc[i] - fftAvg[i]); // approx 14 cycles (350ms) for falling to zero + else if (decayTime < 4000) fftAvg[i] += speed * 0.10f * (fftCalc[i] - fftAvg[i]); + else fftAvg[i] += speed * 0.05f * (fftCalc[i] - fftAvg[i]); + } + } else { + // Limiter OFF + if (i2sFastpath) { + // fast mode -> average last two results + float tmp = fftCalc[i]; + fftCalc[i] = 0.7f * tmp + 0.3f * fftAvg[i]; + fftAvg[i] = tmp; // store current sample for next run } else { - // Limiter OFF - if (i2sFastpath) { - // fast mode -> average last two results - float tmp = fftCalc[i]; - fftCalc[i] = 0.7f * tmp + 0.3f * fftAvg[i]; - fftAvg[i] = tmp; // store current sample for next run - } else { - // normal mode -> no adjustments - fftAvg[i] = fftCalc[i]; // keep filters up-to-date - } + // normal mode -> no adjustments + fftAvg[i] = fftCalc[i]; // keep filters up-to-date } + } - // 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 = limiterOn ? fftAvg[i] : fftCalc[i]; // continue with filtered result (limiter on) or unfiltered result (limiter off) - - switch (FFTScalingMode) { - case 1: - // Logarithmic scaling - currentResult *= 0.42; // 42 is the answer ;-) - currentResult -= 8.0; // this skips the lowest row, giving some room for peaks - if (currentResult > 1.0) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function - else currentResult = 0.0; // special handling, because log(1) = 0; log(0) = undefined - currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies - currentResult = my_mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] + // 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 = limiterOn ? fftAvg[i] : fftCalc[i]; // continue with filtered result (limiter on) or unfiltered result (limiter off) + + switch (FFTScalingMode) { + case 1: + // Logarithmic scaling + currentResult *= 0.42; // 42 is the answer ;-) + currentResult -= 8.0; // this skips the lowest row, giving some room for peaks + if (currentResult > 1.0) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function + else currentResult = 0.0; // special handling, because log(1) = 0; log(0) = undefined + currentResult *= 0.85f + (float(i) / 18.0f); // extra up-scaling for high frequencies + currentResult = my_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 -= 2.0; // 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 + case 2: + // Linear scaling + currentResult *= 0.30f; // needs a bit more damping, get stay below 255 + currentResult -= 2.0; // 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 *= 0.34f; //experiment - currentResult -= 6.0f; - if (currentResult > 1.0) currentResult = sqrtf(currentResult); - else currentResult = 0.0; // special handling, because sqrt(0) = undefined - currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies - //currentResult *= 0.80f + (float(i)/5.6f); //experiment - currentResult = my_mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] + case 3: + // square root scaling + currentResult *= 0.38f; + //currentResult *= 0.34f; //experiment + currentResult -= 6.0f; + if (currentResult > 1.0) currentResult = sqrtf(currentResult); + else currentResult = 0.0; // special handling, because sqrt(0) = undefined + currentResult *= 0.85f + (float(i) / 4.5f); // extra up-scaling for high frequencies + //currentResult *= 0.80f + (float(i)/5.6f); //experiment + currentResult = my_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 -= 2; // just a bit more room for peaks + case 0: + default: + // no scaling - leave freq bins as-is + currentResult -= 2; // 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] = max(min((int)(currentResult+0.5f), 255), 0); // +0.5 for proper rounding + // 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] = max(min((int)(currentResult + 0.5f), 255), 0); // +0.5 for proper rounding + } } //////////////////// // Peak detection // @@ -1229,24 +1253,22 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool // peak detection is called from FFT task when vReal[] contains valid FFT results static void detectSamplePeak(void) { bool havePeak = false; -#if 1 - // softhack007: this code continuously triggers while volume in the selected bin is above a certain threshold. So it does not detect peaks - it detects volume 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; - } -#endif + #if 1 + // softhack007: this code continuously triggers while volume in the selected bin is above a certain threshold. So it does not detect peaks - it detects volume 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; } + #endif -#if 0 - // alternate detection, based on FFT_MajorPeak and FFT_Magnitude. Not much better... - if ((binNum > 1) && (maxVol > 8) && (binNum < 10) && (sampleAgc > 127) && - (FFT_MajorPeak > 50) && (FFT_MajorPeak < 250) && (FFT_Magnitude > (16.0f * (maxVol+42.0)) /*my_magnitude > 136.0f*16.0f*/) && - (millis() - timeOfPeak > 80)) { - havePeak = true; - } -#endif + #if 0 + // alternate detection, based on FFT_MajorPeak and FFT_Magnitude. Not much better... + if ((binNum > 1) && (maxVol > 8) && (binNum < 10) && (sampleAgc > 127) && + (FFT_MajorPeak > 50) && (FFT_MajorPeak < 250) && (FFT_Magnitude > (16.0f * (maxVol+42.0)) /*my_magnitude > 136.0f*16.0f*/) && + (millis() - timeOfPeak > 80)) { + havePeak = true; + } + #endif if (havePeak) { samplePeak = true; @@ -1258,8 +1280,8 @@ static void detectSamplePeak(void) { #endif static void autoResetPeak(void) { - uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC - if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. + uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC + if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. samplePeak = false; if (audioSyncEnabled == AUDIOSYNC_NONE) udpSamplePeak = false; // this is normally reset by transmitAudioData } @@ -1272,181 +1294,190 @@ static void autoResetPeak(void) { //class name. Use something descriptive and leave the ": public Usermod" part :) class AudioReactive : public Usermod { - private: + private: #ifdef ARDUINO_ARCH_ESP32 -// HUB75 workaround - audio receive only -#if defined(WLED_ENABLE_HUB75MATRIX) && (defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)) - #ifdef SR_DMTYPE - #undef SR_DMTYPE - #warning "HUB75 classic ESP32 builds do not work reliably with I2S microphones. Changing default MIC type to network receive only" + // HUB75 workaround - audio receive only + #if defined(WLED_ENABLE_HUB75MATRIX) && (defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)) + #ifdef SR_DMTYPE + #undef SR_DMTYPE + #warning "HUB75 classic ESP32 builds do not work reliably with I2S microphones. Changing default MIC type to network receive only" + #endif + #define SR_DMTYPE 254 // "network receive only" #endif - #define SR_DMTYPE 254 // "network receive only" -#endif - #ifndef AUDIOPIN + #ifndef AUDIOPIN int8_t audioPin = -1; - #else + #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 + #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 + #endif + #ifndef I2S_SDPIN // aka DOUT int8_t i2ssdPin = 32; - #else + #else int8_t i2ssdPin = I2S_SDPIN; - #endif - #ifndef I2S_WSPIN // aka LRCL + #endif + #ifndef I2S_WSPIN // aka LRCL int8_t i2swsPin = 15; - #else + #else int8_t i2swsPin = I2S_WSPIN; - #endif - #ifndef I2S_CKPIN // aka BCLK + #endif + #ifndef I2S_CKPIN // aka BCLK int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ - #else + #else int8_t i2sckPin = I2S_CKPIN; - #endif - #ifndef ES7243_SDAPIN + #endif + #ifndef ES7243_SDAPIN int8_t sdaPin = -1; - #else + #else int8_t sdaPin = ES7243_SDAPIN; - #endif - #ifndef ES7243_SCLPIN + #endif + #ifndef ES7243_SCLPIN int8_t sclPin = -1; - #else + #else int8_t sclPin = ES7243_SCLPIN; - #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 { // WLEDMM "packed" ensures that there are no additional gaps - char header[6]; // 06 Bytes offset 0 - "00002" for protocol version 2 ( includes \0 for c-style string termination) - uint8_t pressure[2]; // 02 Bytes, offset 6 - sound pressure as fixed point (8bit integer, 8bit fraction) - 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 frameCounter; // 01 Bytes offset 17 - rolling counter to track duplicate/out of order packets - uint8_t fftResult[16]; // 16 Bytes offset 18 - 16 GEQ channels, each channel has one byte (uint8_t) - uint16_t zeroCrossingCount; // 02 Bytes, offset 34 - number of zero crossings seen in 23ms - float FFT_Magnitude; // 04 Bytes offset 36 - largest FFT result from a single run (raw value, can go up to 4096) - float FFT_MajorPeak; // 04 Bytes offset 40 - frequency (Hz) of largest FFT result - }; - - // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total - for backwards compatibility - struct audioSyncPacket_v1 { - char header[6]; // 06 Bytes - uint8_t myVals[32]; // 32 Bytes - int32_t sampleAgc; // 04 Bytes - int32_t 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 96 // max packet size for audiosync, with a bit of "headroom" - #define AR_UDP_READ_INTERVAL_MS 18 // 23ms = time for reading one new batch of samples @ 22kHz; minus 5ms margin for network overhead - #define AR_UDP_FLUSH_ALL 255 // tells receiveAudioData to purge the network input queue - - // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) - #if defined(SR_ENABLE_DEFAULT) || defined(UM_AUDIOREACTIVE_ENABLE) - bool enabled = true; // WLEDMM + #endif + #ifndef MCLK_PIN + int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ #else - bool enabled = false; + int8_t mclkPin = MCLK_PIN; #endif - bool initDone = false; - bool addPalettes = false; - int8_t palettes = 0; - bool processingPalettes = false; // poor man's interlock - to prevent that palettes get removed while they are processed - - // 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 -#if defined(WLEDMM_FASTPATH) - const uint16_t delayMs = 5; // I don't want to sample too often and overload WLED +#endif + // new "V2" audiosync struct - 44 Bytes + struct __attribute__((packed)) audioSyncPacket { // WLEDMM "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 - "00002" for protocol version 2 ( includes \0 for c-style string termination) + uint8_t pressure[2]; // 02 Bytes, offset 6 - sound pressure as fixed point (8bit integer, 8bit fraction) + 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 frameCounter; // 01 Bytes offset 17 - rolling counter to track duplicate/out of order packets + uint8_t fftResult[16]; // 16 Bytes offset 18 - 16 GEQ channels, each channel has one byte (uint8_t) + uint16_t zeroCrossingCount; // 02 Bytes, offset 34 - number of zero crossings seen in 23ms + float FFT_Magnitude; // 04 Bytes offset 36 - largest FFT result from a single run (raw value, can go up to 4096) + float FFT_MajorPeak; // 04 Bytes offset 40 - frequency (Hz) of largest FFT result + }; + + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total - for backwards compatibility + struct audioSyncPacket_v1 { + char header[6]; // 06 Bytes + uint8_t myVals[32]; // 32 Bytes + int32_t sampleAgc; // 04 Bytes + int32_t 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 96 // max packet size for audiosync, with a bit of "headroom" +#define AR_UDP_READ_INTERVAL_MS 18 // 23ms = time for reading one new batch of samples @ 22kHz; minus 5ms margin for network overhead +#define AR_UDP_FLUSH_ALL 255 // tells receiveAudioData to purge the network input queue + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) +#if defined(SR_ENABLE_DEFAULT) || defined(UM_AUDIOREACTIVE_ENABLE) + bool enabled = true; // WLEDMM #else + bool enabled = false; +#endif + bool initDone = false; + bool addPalettes = false; + int8_t palettes = 0; + bool processingPalettes = false; // poor man's interlock - to prevent that palettes get removed while they are processed + + // 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 + #if defined(WLEDMM_FASTPATH) + const uint16_t delayMs = 5; // I don't want to sample too often and overload WLED + #else const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED -#endif - uint16_t audioSyncPort= 11988;// default port for UDP sound sync + #endif + uint16_t audioSyncPort = 11988; // default port for UDP sound sync - bool updateIsRunning = false; // true during OTA. + 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() - 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 - int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc - float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc - float soundPressure = 0; // Sound Pressure estimation, based on microphone raw readings. 0 ->5db, 255 ->105db - - // used to feed "Info" Page - unsigned long last_UDPTime = 0; // time of last valid UDP sound sync data packet - int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) - unsigned long syncSilenceStart = 0; // millis() when continuous silence began for the sender; 0 = sound is (or was recently) present - 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 measuring - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _inputLvl[]; + // 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() + 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 + int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc + float my_magnitude = 0.0f; // FFT_Magnitude, scaled by multAgc + float soundPressure = 0; // Sound Pressure estimation, based on microphone raw readings. 0 ->5db, 255 ->105db + + // used to feed "Info" Page + unsigned long last_UDPTime = 0; // time of last valid UDP sound sync data packet + int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) + unsigned long syncSilenceStart = 0; // millis() when continuous silence began for the sender; 0 = sound is (or was recently) present + 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 measuring + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _inputLvl[]; #if defined(ARDUINO_ARCH_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && !defined(__riscv) - static const char _analogmic[]; -#endif - static const char _digitalmic[]; - static const char UDP_SYNC_HEADER[]; - static const char UDP_SYNC_HEADER_v1[]; - static const char _addPalettes[]; - static const char _palName0[]; - static const char _palName1[]; - static const char _palName2[]; - static const char _palName3[]; - static const char _palName4[]; - - // 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 & AUDIOSYNC_REC) == 0))) return; // no audio available - #ifdef MIC_LOGGER - // Debugging functions for audio input and sound processing. Comment out the values you want to see - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); - #ifdef ARDUINO_ARCH_ESP32 - PLOT_PRINT("micMin:"); PLOT_PRINT(0.5f * micReal_min); PLOT_PRINT("\t"); // scaled down to 50%, for better readability - PLOT_PRINT("micMax:"); PLOT_PRINT(0.5f * micReal_max); PLOT_PRINT("\t"); // scaled down to 50% - //PLOT_PRINT("micAvg:"); PLOT_PRINT(0.5f * micReal_avg); PLOT_PRINT("\t"); // scaled down to 50% - //PLOT_PRINT("micDC:"); PLOT_PRINT(0.5f * (micReal_min + micReal_max)/2.0f);PLOT_PRINT("\t"); // scaled down to 50% - PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + static const char _analogmic[]; +#endif + static const char _digitalmic[]; + static const char UDP_SYNC_HEADER[]; + static const char UDP_SYNC_HEADER_v1[]; + static const char _addPalettes[]; + static const char _palName0[]; + static const char _palName1[]; + static const char _palName2[]; + static const char _palName3[]; + static const char _palName4[]; + + // 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 & AUDIOSYNC_REC) == 0))) return; // no audio available +#ifdef MIC_LOGGER + // Debugging functions for audio input and sound processing. Comment out the values you want to see + PLOT_PRINT("volumeSmth:"); + PLOT_PRINT(volumeSmth + 256.0f); + PLOT_PRINT("\t"); // +256 to move above other lines + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 + PLOT_PRINT("micMin:"); + PLOT_PRINT(0.5f * micReal_min); + PLOT_PRINT("\t"); // scaled down to 50%, for better readability + PLOT_PRINT("micMax:"); + PLOT_PRINT(0.5f * micReal_max); + PLOT_PRINT("\t"); // scaled down to 50% + //PLOT_PRINT("micAvg:"); PLOT_PRINT(0.5f * micReal_avg); PLOT_PRINT("\t"); // scaled down to 50% + //PLOT_PRINT("micDC:"); PLOT_PRINT(0.5f * (micReal_min + micReal_max)/2.0f);PLOT_PRINT("\t"); // scaled down to 50% + PLOT_PRINT("micReal:"); + PLOT_PRINT(micDataReal + 256.0f); + PLOT_PRINT("\t"); // +256 to move above other lines + PLOT_PRINT("DC_Level:"); + PLOT_PRINT(micLev + 256.0f); + PLOT_PRINT("\t"); // +256 to move above other lines // //PLOT_PRINT("filtmicMin:"); PLOT_PRINT(0.5f * micReal_min2); PLOT_PRINT("\t"); // scaled down to 50% // //PLOT_PRINT("filtmicMax:"); PLOT_PRINT(0.5f * micReal_max2); PLOT_PRINT("\t"); // scaled down to 50% //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); @@ -1455,69 +1486,66 @@ class AudioReactive : public Usermod { //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); - #endif - PLOT_PRINTLN(); - PLOT_FLUSH(); - #endif + #endif + PLOT_PRINTLN(); + PLOT_FLUSH(); +#endif - #ifdef FFT_SAMPLING_LOG - #if 0 +#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() + // OPTIONS are in the following format: Description \n Option + // + // Set true if wanting to see all the bands in their own vertical space on the Serial Plotter, false if wanting to see values in Serial Monitor + const bool mapValuesToPlotterSpace = false; + // Set true to apply an auto-gain like setting to the data (this hasn't been tested recently) + const bool scaleValuesFromCurrentMaxVal = false; + // prints the max value seen in the current data + const bool printMaxVal = false; + // prints the min value seen in the current data + const bool printMinVal = false; + // if !scaleValuesFromCurrentMaxVal, we scale values from [0..defaultScalingFromHighValue] to [0..scalingToHighValue], lower this if you want to see smaller values easier + const int defaultScalingFromHighValue = 256; + // Print values to terminal in range of [0..scalingToHighValue] if !mapValuesToPlotterSpace, or [(i)*scalingToHighValue..(i+1)*scalingToHighValue] if mapValuesToPlotterSpace + const int scalingToHighValue = 256; + // set higher if using scaleValuesFromCurrentMaxVal and you want a small value that's also the current maxVal to look small on the plotter (can't be 0 to avoid divide by zero error) + const int minimumMaxVal = 1; + + int maxVal = minimumMaxVal; + int minVal = 0; + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + if (fftResult[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 // - ////////////////////// + ////////////////////// + // Audio Processing // + ////////////////////// - /* + /* * A "PI controller" multiplier to automatically adjust sound sensitivity. * * A few tricks are implemented so that sampleAgc doesn't only utilize 0% and 100%: @@ -1528,877 +1556,857 @@ class AudioReactive : public Usermod { * 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 + 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 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 + float control_error; // "control error" input for PI control - if (last_soundAgc != soundAgc) - control_integrated = 0.0; // new preset - reset integrator + 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 + // 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 (time_now - last_time > 2) { + last_time = time_now; - if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { - // 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 integrator beast - - // 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 emergency 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; - } + if ((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { + // 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 integrator beast + + // 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 emergency 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; - if (micQuality > 0) { - if (micQuality > 1) { - rawSampleAgc = 0.95f * tmpAgc + 0.05f * (float)rawSampleAgc; // raw path - sampleAgc += 0.95f * (tmpAgc - sampleAgc); // smooth path - } else { - rawSampleAgc = 0.70f * tmpAgc + 0.30f * (float)rawSampleAgc; // min filtering path - sampleAgc += 0.70f * (tmpAgc - sampleAgc); - } + // 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; + if (micQuality > 0) { + if (micQuality > 1) { + rawSampleAgc = 0.95f * tmpAgc + 0.05f * (float)rawSampleAgc; // raw path + sampleAgc += 0.95f * (tmpAgc - sampleAgc); // smooth path } else { -#if defined(WLEDMM_FASTPATH) - rawSampleAgc = 0.65f * tmpAgc + 0.35f * (float)rawSampleAgc; -#else - rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; -#endif - // 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 + rawSampleAgc = 0.70f * tmpAgc + 0.30f * (float)rawSampleAgc; // min filtering path + sampleAgc += 0.70f * (tmpAgc - sampleAgc); } - sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value - last_soundAgc = soundAgc; - } // agcAvg() + } else { + #if defined(WLEDMM_FASTPATH) + rawSampleAgc = 0.65f * tmpAgc + 0.35f * (float)rawSampleAgc; + #else + rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; + #endif + // 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.18f; // Exponential filter weighting. Will be adjustable in a future release. + const float weighting2 = 0.073f; // Exponential filter weighting, for rising signal (a bit more robust against spikes) + 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 + static bool isFrozen = false; + static bool haveSilence = true; + static unsigned long lastSoundTime = 0; // for delaying un-freeze + static unsigned long startuptime = 0; // "fast freeze" mode: do not interfere during first 12 seconds (filter startup time) + + if (startuptime == 0) startuptime = millis(); // fast freeze mode - remember filter startup time + if ((micLevelMethod < 1) || !isFrozen) { // following the input level, UNLESS mic Level was frozen + micLev += (micDataReal - micLev) / 12288.0f; + } - // 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.18f; // Exponential filter weighting. Will be adjustable in a future release. - const float weighting2 = 0.073f; // Exponential filter weighting, for rising signal (a bit more robust against spikes) - 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 - static bool isFrozen = false; - static bool haveSilence = true; - static unsigned long lastSoundTime = 0; // for delaying un-freeze - static unsigned long startuptime = 0; // "fast freeze" mode: do not interfere during first 12 seconds (filter startup time) - - if (startuptime == 0) startuptime = millis(); // fast freeze mode - remember filter startup time - if ((micLevelMethod < 1) || !isFrozen) { // following the input level, UNLESS mic Level was frozen - micLev += (micDataReal-micLev) / 12288.0f; - } + if (micDataReal < (micLev - 0.24)) { // MicLev above input signal: + micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // always align MicLev to lowest input signal + if (!haveSilence) isFrozen = true; // freeze mode: freeze micLevel so it cannot rise again + } - if(micDataReal < (micLev-0.24)) { // MicLev above input signal: - micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // always align MicLev to lowest input signal - if (!haveSilence) isFrozen = true; // freeze mode: freeze micLevel so it cannot rise again - } + // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. + float micInNoDC = fabsf(micDataReal - micLev); - // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. - float micInNoDC = fabsf(micDataReal - micLev); + if ((micInNoDC > expAdjF) && (expAdjF > soundSquelch)) // MicIn rising, and above squelch threshold? + expAdjF = (weighting2 * micInNoDC + (1.0f - weighting2) * expAdjF); // rise slower + else expAdjF = (weighting * micInNoDC + (1.0f - weighting) * expAdjF); // fall faster - if ((micInNoDC > expAdjF) && (expAdjF > soundSquelch)) // MicIn rising, and above squelch threshold? - expAdjF = (weighting2 * micInNoDC + (1.0f-weighting2) * expAdjF); // rise slower - else - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); // fall faster + expAdjF = fabsf(expAdjF); // Now (!) take the absolute value - expAdjF = fabsf(expAdjF); // Now (!) take the absolute value + if ((micLevelMethod == 2) && !haveSilence && (expAdjF >= (1.5f * float(soundSquelch)))) isFrozen = true; // fast freeze mode: freeze micLevel once the volume rises 50% above squelch - if ((micLevelMethod == 2) && !haveSilence && (expAdjF >= (1.5f * float(soundSquelch)))) - isFrozen = true; // fast freeze mode: freeze micLevel once the volume rises 50% above squelch + // simple noise gate + if ((expAdjF <= soundSquelch) || ((soundSquelch == 0) && (expAdjF < 0.25f))) { + expAdjF = 0.0f; + micInNoDC = 0.0f; + } - // simple noise gate - if ((expAdjF <= soundSquelch) || ((soundSquelch == 0) && (expAdjF < 0.25f))) { - expAdjF = 0.0f; - micInNoDC = 0.0f; - } + if (expAdjF <= 0.5f) haveSilence = true; + else { + lastSoundTime = millis(); + haveSilence = false; + } - if (expAdjF <= 0.5f) - haveSilence = true; - else { - lastSoundTime = millis(); - haveSilence = false; - } + // un-freeze micLev + if (micLevelMethod == 0) isFrozen = false; + if ((micLevelMethod == 1) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 4000)) isFrozen = false; // normal freeze: 4 seconds silence needed + if ((micLevelMethod == 2) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 6000)) isFrozen = false; // fast freeze: 6 seconds silence needed + if ((micLevelMethod == 2) && (millis() - startuptime < 12000)) isFrozen = false; // fast freeze: no freeze in first 12 seconds (filter startup phase) - // un-freeze micLev - if (micLevelMethod == 0) isFrozen = false; - if ((micLevelMethod == 1) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 4000)) isFrozen = false; // normal freeze: 4 seconds silence needed - if ((micLevelMethod == 2) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 6000)) isFrozen = false; // fast freeze: 6 seconds silence needed - if ((micLevelMethod == 2) && (millis() - startuptime < 12000)) isFrozen = false; // fast freeze: no freeze in first 12 seconds (filter startup phase) + tmpSample = expAdjF; - tmpSample = expAdjF; + // Adjust the gain. with inputLevel adjustment. + if (micQuality > 0) { + sampleAdj = micInNoDC * sampleGain / 40.0f * inputLevel / 128.0f + micInNoDC / 16.0f; // ... using unfiltered sample + sampleReal = micInNoDC; + } else { + sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel / 128.0f + tmpSample / 16.0f; // ... using pre-filtered sample + sampleReal = tmpSample; + } - // Adjust the gain. with inputLevel adjustment. - if (micQuality > 0) { - sampleAdj = micInNoDC * sampleGain / 40.0f * inputLevel/128.0f + micInNoDC / 16.0f; // ... using unfiltered sample - sampleReal = micInNoDC; - } else { - sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // ... using pre-filtered sample - sampleReal = tmpSample; - } + sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? + sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! - sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // 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 -#if 1 - // 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; - } -#endif - } 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; - - if (micQuality > 0) { - if (micQuality > 1) sampleAvg += 0.95f * (sampleAdj - sampleAvg); - else sampleAvg += 0.70f * (sampleAdj - sampleAvg); - } else { -#if defined(WLEDMM_FASTPATH) - sampleAvg = ((sampleAvg * 11.0f) + sampleAdj) / 12.0f; // make reactions a bit more "crisp" in fastpath mode -#else - sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. -#endif + // 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 + #if 1 + // 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; } - sampleAvg = fabsf(sampleAvg); // make sure we have a positive value - } // getSample() - + #endif + } 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; - // current sensitivity, based on AGC gain (multAgc) - float getSensitivity() - { - // start with AGC gain factor - float tmpSound = multAgc; - // experimental: this gives you a calculated "real gain" - // if ((sampleAvg> 1.0) && (sampleReal > 0.05)) tmpSound = (float)sampleRaw / sampleReal; // calculate gain from sampleReal - // else tmpSound = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // silence --> use values from user settings - - if (soundAgc == 0) - tmpSound = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // AGC off -> use non-AGC gain from presets - else - tmpSound /= (float)sampleGain/40.0f + 1.0f/16.0f; // AGC ON -> scale value so 1 = middle value - - // scale to 0..255. Actually I'm not absolutely happy with this, but it works - if (tmpSound > 1.0) tmpSound = sqrtf(tmpSound); - if (tmpSound > 1.25) tmpSound = ((tmpSound-1.25f)/3.42f) +1.25f; - // we have a value now that should be between 0 and 4 (representing gain 1/16 ... 16.0) - return fminf(fmaxf(128.0*tmpSound -6.0f, 0), 255.0); // return scaled non-inverted value // "-6" to ignore values below 1/24 - } - - // estimate sound pressure, based on some assumptions : - // * sample max = 32676 -> Acoustic overload point --> 105db ==> 255 - // * sample < squelch -> just above hearing level --> 5db ==> 0 - // see https://en.wikipedia.org/wiki/Sound_pressure#Examples_of_sound_pressure - // use with I2S digital microphones. Expect stupid values for analog in, and with Line-In !! - float estimatePressure() const { - // some constants - constexpr float logMinSample = 0.8329091229351f; // ln(2.3) - constexpr float sampleRangeMin = 2.3f; - constexpr float logMaxSample = 10.1895683436f; // ln(32767 - 6144) - constexpr float sampleRangeMax = 32767.0f - 6144.0f; - - // take the max sample from last I2S batch. - float micSampleMax = fabsf(sampleReal); // from getSample() - nice results, however a bit distorted by MicLev processing - //float micSampleMax = fabsf(micDataReal); // from FFTCode() - better source, but more flickering - if (dmType == 0) micSampleMax *= 2.0f; // correction for ADC analog - //if (dmType == 4) micSampleMax *= 16.0f; // correction for I2S Line-In - if (dmType == 5) micSampleMax *= 2.0f; // correction for PDM - if (dmType == 4) { // I2S Line-In. This is a dirty trick to make sound pressure look interesting for line-in (which doesn't have "sound pressure" as it is not a microphone) - micSampleMax /= 11.0f; // reduce to max 128 - micSampleMax *= micSampleMax; // blow up --> max 16000 - } - // make sure we are in expected ranges - if(micSampleMax <= sampleRangeMin) return 0.0f; - if(micSampleMax >= sampleRangeMax) return 255.0f; + if (micQuality > 0) { + if (micQuality > 1) sampleAvg += 0.95f * (sampleAdj - sampleAvg); + else sampleAvg += 0.70f * (sampleAdj - sampleAvg); + } else { + #if defined(WLEDMM_FASTPATH) + sampleAvg = ((sampleAvg * 11.0f) + sampleAdj) / 12.0f; // make reactions a bit more "crisp" in fastpath mode + #else + sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. + #endif + } + sampleAvg = fabsf(sampleAvg); // make sure we have a positive value + } // getSample() + + + // current sensitivity, based on AGC gain (multAgc) + float getSensitivity() { + // start with AGC gain factor + float tmpSound = multAgc; + // experimental: this gives you a calculated "real gain" + // if ((sampleAvg> 1.0) && (sampleReal > 0.05)) tmpSound = (float)sampleRaw / sampleReal; // calculate gain from sampleReal + // else tmpSound = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // silence --> use values from user settings + + if (soundAgc == 0) tmpSound = ((float)sampleGain / 40.0f * (float)inputLevel / 128.0f) + 1.0f / 16.0f; // AGC off -> use non-AGC gain from presets + else tmpSound /= (float)sampleGain / 40.0f + 1.0f / 16.0f; // AGC ON -> scale value so 1 = middle value + + // scale to 0..255. Actually I'm not absolutely happy with this, but it works + if (tmpSound > 1.0) tmpSound = sqrtf(tmpSound); + if (tmpSound > 1.25) tmpSound = ((tmpSound - 1.25f) / 3.42f) + 1.25f; + // we have a value now that should be between 0 and 4 (representing gain 1/16 ... 16.0) + return fminf(fmaxf(128.0 * tmpSound - 6.0f, 0), 255.0); // return scaled non-inverted value // "-6" to ignore values below 1/24 + } - // apply logarithmic scaling - float scaledvalue = logf(micSampleMax); - scaledvalue = (scaledvalue - logMinSample) / (logMaxSample - logMinSample); // 0...1 - return fminf(fmaxf(256.0f*scaledvalue, 0.0f), 255.0f); // scaled value + // estimate sound pressure, based on some assumptions : + // * sample max = 32676 -> Acoustic overload point --> 105db ==> 255 + // * sample < squelch -> just above hearing level --> 5db ==> 0 + // see https://en.wikipedia.org/wiki/Sound_pressure#Examples_of_sound_pressure + // use with I2S digital microphones. Expect stupid values for analog in, and with Line-In !! + float estimatePressure() const { + // some constants + constexpr float logMinSample = 0.8329091229351f; // ln(2.3) + constexpr float sampleRangeMin = 2.3f; + constexpr float logMaxSample = 10.1895683436f; // ln(32767 - 6144) + constexpr float sampleRangeMax = 32767.0f - 6144.0f; + + // take the max sample from last I2S batch. + float micSampleMax = fabsf(sampleReal); // from getSample() - nice results, however a bit distorted by MicLev processing + //float micSampleMax = fabsf(micDataReal); // from FFTCode() - better source, but more flickering + if (dmType == 0) micSampleMax *= 2.0f; // correction for ADC analog + //if (dmType == 4) micSampleMax *= 16.0f; // correction for I2S Line-In + if (dmType == 5) micSampleMax *= 2.0f; // correction for PDM + if (dmType == 4) { // I2S Line-In. This is a dirty trick to make sound pressure look interesting for line-in (which doesn't have "sound pressure" as it is not a microphone) + micSampleMax /= 11.0f; // reduce to max 128 + micSampleMax *= micSampleMax; // blow up --> max 16000 } + // make sure we are in expected ranges + if (micSampleMax <= sampleRangeMin) return 0.0f; + if (micSampleMax >= sampleRangeMax) return 255.0f; + + // apply logarithmic scaling + float scaledvalue = logf(micSampleMax); + scaledvalue = (scaledvalue - logMinSample) / (logMaxSample - logMinSample); // 0...1 + return fminf(fmaxf(256.0f * scaledvalue, 0.0f), 255.0f); // scaled value + } #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 -> silly lil hick-up - float deltaSample = volumeSmth - last_volumeSmth; + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). + * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) + */ + 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 (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; - } + if (limiterOn == false) return; - volumeSmth = last_volumeSmth + deltaSample; + long delta_time = millis() - last_time; + delta_time = constrain(delta_time, 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up + float deltaSample = volumeSmth - last_volumeSmth; - last_volumeSmth = volumeSmth; - last_time = millis(); + 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; } - // MM experimental: limiter to smooth GEQ samples (only for UDP sound receiver mode) - // target value (if gotNewSample) : fftCalc - // last filtered value: fftAvg - void limitGEQDynamics(bool gotNewSample) { - constexpr float bigChange = 202; // just a representative number - a large, expected sample value - constexpr float smooth = 0.8f; // a bit of filtering - static unsigned long last_time = 0; + volumeSmth = last_volumeSmth + deltaSample; - if (limiterOn == false) return; + last_volumeSmth = volumeSmth; + last_time = millis(); + } - if (gotNewSample) { // take new FFT samples as target values - for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { - fftCalc[i] = fftResult[i]; - fftResult[i] = fftAvg[i]; - } - } + // MM experimental: limiter to smooth GEQ samples (only for UDP sound receiver mode) + // target value (if gotNewSample) : fftCalc + // last filtered value: fftAvg + void limitGEQDynamics(bool gotNewSample) { + constexpr float bigChange = 202; // just a representative number - a large, expected sample value + constexpr float smooth = 0.8f; // a bit of filtering + static unsigned long last_time = 0; + + if (limiterOn == false) return; - long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up - float maxAttack = (attackTime <= 0) ? 255.0f : (bigChange * float(delta_time) / float(attackTime)); - float maxDecay = (decayTime <= 0) ? -255.0f : (-bigChange * float(delta_time) / float(decayTime)); - - for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { - float deltaSample = fftCalc[i] - fftAvg[i]; - if (deltaSample > maxAttack) deltaSample = maxAttack; - if (deltaSample < maxDecay) deltaSample = maxDecay; - deltaSample = deltaSample * smooth; - fftAvg[i] = fmaxf(0.0f, fminf(255.0f, fftAvg[i] + deltaSample)); + if (gotNewSample) { // take new FFT samples as target values + for (unsigned i = 0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] = fftResult[i]; fftResult[i] = fftAvg[i]; } - last_time = millis(); } - ////////////////////// - // UDP Sound Sync // - ////////////////////// + long delta_time = millis() - last_time; + delta_time = constrain(delta_time, 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up + float maxAttack = (attackTime <= 0) ? 255.0f : (bigChange * float(delta_time) / float(attackTime)); + float maxDecay = (decayTime <= 0) ? -255.0f : (-bigChange * float(delta_time) / float(decayTime)); + + for (unsigned i = 0; i < NUM_GEQ_CHANNELS; i++) { + float deltaSample = fftCalc[i] - fftAvg[i]; + if (deltaSample > maxAttack) deltaSample = maxAttack; + if (deltaSample < maxDecay) deltaSample = maxDecay; + deltaSample = deltaSample * smooth; + fftAvg[i] = fmaxf(0.0f, fminf(255.0f, fftAvg[i] + deltaSample)); + fftResult[i] = fftAvg[i]; + } + last_time = millis(); + } - // 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; + ////////////////////// + // UDP Sound Sync // + ////////////////////// - if ((audioSyncPort <= 0) || (audioSyncEnabled == AUDIOSYNC_NONE)) return; // Sound Sync not enabled - if (!(apActive || WLED_CONNECTED || interfacesInited)) { - if (udpSyncConnected) { - udpSyncConnected = false; - fftUdp.stop(); - receivedFormat = 0; - DEBUGSR_PRINTLN(F("AR connectUDPSoundSync(): connection lost, UDP closed.")); - } - return; // neither AP nor other connections available - } - if (udpSyncConnected) return; // already connected - if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; // don't reconnect during OTA + // 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 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; - static uint8_t frameCounter = 0; - //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 - transmitData.frameCounter = frameCounter; - transmitData.zeroCrossingCount = zeroCrossingCount; - - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - transmitData.fftResult[i] = fftResult[i]; + if ((audioSyncPort <= 0) || (audioSyncEnabled == AUDIOSYNC_NONE)) return; // Sound Sync not enabled + if (!(apActive || WLED_CONNECTED || interfacesInited)) { + if (udpSyncConnected) { + udpSyncConnected = false; + fftUdp.stop(); + receivedFormat = 0; + DEBUGSR_PRINTLN(F("AR connectUDPSoundSync(): connection lost, UDP closed.")); } + return; // neither AP nor other connections available + } + if (udpSyncConnected) return; // already connected + if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; // don't reconnect during OTA - // WLEDMM transmit soundPressure as 16 bit fixed point - uint32_t pressure16bit = max(0.0f, soundPressure) * 256.0f; // convert to fixed point, remove negative values - uint16_t pressInt = pressure16bit / 256; // integer part - uint16_t pressFract = pressure16bit % 256; // faction part - if (pressInt > 255) pressInt = 255; // saturation at 255 - transmitData.pressure[0] = (uint8_t)pressInt; - transmitData.pressure[1] = (uint8_t)pressFract; - - transmitData.FFT_Magnitude = my_magnitude; - transmitData.FFT_MajorPeak = FFT_MajorPeak; - - if (audioSyncBroadcast) { - // Compute subnet broadcast address (e.g. 192.168.1.255 for a /24 network) - IPAddress broadcastAddr((uint32_t)WiFi.localIP() | ~(uint32_t)WiFi.subnetMask()); - if (fftUdp.beginPacket(broadcastAddr, audioSyncPort) != 0) { - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - } - } else { - if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - } - } - - frameCounter++; - } // 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; - } - - bool decodeAudioData(int packetSize, uint8_t *fftBuff) { - if((0 == packetSize) || (nullptr == fftBuff)) return false; // sanity check - //audioSyncPacket *receivedPacket = reinterpret_cast(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 - - // validate sequence, discard out-of-sequence packets - static uint8_t lastFrameCounter = 0; - int lastReceivedFormat = receivedFormat; - // add info for UI - if ((receivedPacket.frameCounter > 0) && (lastFrameCounter > 0)) receivedFormat = 3; // v2+ - else receivedFormat = 2; // v2 - - // check sequence - bool sequenceOK = false; - if ((int8_t)(receivedPacket.frameCounter - lastFrameCounter) > 0) sequenceOK = true; // 8-bit rollover-safe sequence check - if (millis()- last_UDPTime >= AUDIOSYNC_IDLE_MS) sequenceOK = true; // receiver timed out - resync needed - if (lastReceivedFormat < 2) sequenceOK = true; // first or second V2 packet - accept anything (prevents delay when re-enabling AR) - if(audioSyncSequence == false) sequenceOK = true; // sequence checking disabled by user - if((sequenceOK == false) && (receivedPacket.frameCounter != 0)) { // always accept "0" as the legacy value - DEBUGSR_PRINTF("Skipping audio frame out of order or duplicated - %u vs %u\n", lastFrameCounter, receivedPacket.frameCounter); - return false; // reject out-of sequence frame + // 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; + static uint8_t frameCounter = 0; + //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 + transmitData.frameCounter = frameCounter; + transmitData.zeroCrossingCount = zeroCrossingCount; + + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = fftResult[i]; } + + // WLEDMM transmit soundPressure as 16 bit fixed point + uint32_t pressure16bit = max(0.0f, soundPressure) * 256.0f; // convert to fixed point, remove negative values + uint16_t pressInt = pressure16bit / 256; // integer part + uint16_t pressFract = pressure16bit % 256; // faction part + if (pressInt > 255) pressInt = 255; // saturation at 255 + transmitData.pressure[0] = (uint8_t)pressInt; + transmitData.pressure[1] = (uint8_t)pressFract; + + transmitData.FFT_Magnitude = my_magnitude; + transmitData.FFT_MajorPeak = FFT_MajorPeak; + + if (audioSyncBroadcast) { + // Compute subnet broadcast address (e.g. 192.168.1.255 for a /24 network) + IPAddress broadcastAddr((uint32_t)WiFi.localIP() | ~(uint32_t)WiFi.subnetMask()); + if (fftUdp.beginPacket(broadcastAddr, audioSyncPort) != 0) { + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); } - else { - lastFrameCounter = receivedPacket.frameCounter; + } else { + if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); } + } - // update samples for effects - volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); -#ifdef ARDUINO_ARCH_ESP32 + frameCounter++; + } // 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; } + + bool decodeAudioData(int packetSize, uint8_t* fftBuff) { + if ((0 == packetSize) || (nullptr == fftBuff)) return false; // sanity check + //audioSyncPacket *receivedPacket = reinterpret_cast(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 + + // validate sequence, discard out-of-sequence packets + static uint8_t lastFrameCounter = 0; + int lastReceivedFormat = receivedFormat; + // add info for UI + if ((receivedPacket.frameCounter > 0) && (lastFrameCounter > 0)) receivedFormat = 3; // v2+ + else receivedFormat = 2; // v2 + + // check sequence + bool sequenceOK = false; + if ((int8_t)(receivedPacket.frameCounter - lastFrameCounter) > 0) sequenceOK = true; // 8-bit rollover-safe sequence check + if (millis() - last_UDPTime >= AUDIOSYNC_IDLE_MS) sequenceOK = true; // receiver timed out - resync needed + if (lastReceivedFormat < 2) sequenceOK = true; // first or second V2 packet - accept anything (prevents delay when re-enabling AR) + if (audioSyncSequence == false) sequenceOK = true; // sequence checking disabled by user + if ((sequenceOK == false) && (receivedPacket.frameCounter != 0)) { // always accept "0" as the legacy value + DEBUGSR_PRINTF("Skipping audio frame out of order or duplicated - %u vs %u\n", lastFrameCounter, receivedPacket.frameCounter); + return false; // reject out-of sequence frame + } else { + lastFrameCounter = receivedPacket.frameCounter; + } + + // 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 -#ifdef ARDUINO_ARCH_ESP32 - FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42f * (FFT_MajorPeak - FFT_MajPeakSmth); // simulate smooth value -#endif - agcSensitivity = 128.0f; // substitute - V2 format does not include this value - zeroCrossingCount = receivedPacket.zeroCrossingCount; - - // WLEDMM extract soundPressure - if ((receivedPacket.pressure[0] != 0) || (receivedPacket.pressure[1] != 0)) { - // found something in gap "reserved2" - soundPressure = float(receivedPacket.pressure[1]) / 256.0f; // fractional part - soundPressure += float(receivedPacket.pressure[0]); // integer part - } else { - soundPressure = volumeSmth; // fallback - } - - return true; + #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 + #ifdef ARDUINO_ARCH_ESP32 + FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42f * (FFT_MajorPeak - FFT_MajPeakSmth); // simulate smooth value + #endif + agcSensitivity = 128.0f; // substitute - V2 format does not include this value + zeroCrossingCount = receivedPacket.zeroCrossingCount; + + // WLEDMM extract soundPressure + if ((receivedPacket.pressure[0] != 0) || (receivedPacket.pressure[1] != 0)) { + // found something in gap "reserved2" + soundPressure = float(receivedPacket.pressure[1]) / 256.0f; // fractional part + soundPressure += float(receivedPacket.pressure[0]); // integer part + } else { + soundPressure = volumeSmth; // fallback } - 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 + return true; + } + + 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);; + 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 - soundPressure = volumeSmth; // substitute - V1 format does not include this value - agcSensitivity = 128.0f; // substitute - V1 format does not include this value - } - - bool receiveAudioData( unsigned maxSamples) { // maxSamples = AR_UDP_FLUSH_ALL (255) means "purge complete input queue" - if (!udpSyncConnected) return false; - bool haveFreshData = false; - size_t packetSize = 0; - static uint8_t fftUdpBuffer[UDPSOUND_MAX_PACKET + 1] = {0}; - - // Loop to read available packets - unsigned packetsReceived = 0; - do { - #if __cpp_exceptions - try { - packetSize = fftUdp.parsePacket(); - } catch (...) { - packetSize = 0; - #ifdef ARDUINO_ARCH_ESP32 + 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 + soundPressure = volumeSmth; // substitute - V1 format does not include this value + agcSensitivity = 128.0f; // substitute - V1 format does not include this value + } + + bool receiveAudioData(unsigned maxSamples) { // maxSamples = AR_UDP_FLUSH_ALL (255) means "purge complete input queue" + if (!udpSyncConnected) return false; + bool haveFreshData = false; + size_t packetSize = 0; + static uint8_t fftUdpBuffer[UDPSOUND_MAX_PACKET + 1] = {0}; + + // Loop to read available packets + unsigned packetsReceived = 0; + do { + #if __cpp_exceptions + try { + packetSize = fftUdp.parsePacket(); + } catch (...) { + packetSize = 0; + #ifdef ARDUINO_ARCH_ESP32 #if ESP_IDF_VERSION_MAJOR < 5 - fftUdp.flush(); + fftUdp.flush(); #else - fftUdp.clear(); - #endif + fftUdp.clear(); #endif - DEBUG_PRINTLN(F("receiveAudioData: parsePacket out of memory exception caught!")); - USER_FLUSH(); - //continue; // don't skip to next iteration -> we are OOM - } - #else - packetSize = fftUdp.parsePacket(); #endif + DEBUG_PRINTLN(F("receiveAudioData: parsePacket out of memory exception caught!")); + USER_FLUSH(); + //continue; // don't skip to next iteration -> we are OOM + } + #else + packetSize = fftUdp.parsePacket(); + #endif - #ifdef ARDUINO_ARCH_ESP32 - if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) { - #if ESP_IDF_VERSION_MAJOR < 5 + #ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) { + #if ESP_IDF_VERSION_MAJOR < 5 fftUdp.flush(); - #else + #else fftUdp.clear(); - #endif - continue; // Skip invalid packets -> next iteration - } #endif + continue; // Skip invalid packets -> next iteration + } + #endif - if (packetSize == 0) break; // No more packets available --> exit loop + if (packetSize == 0) break; // No more packets available --> exit loop - if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { - fftUdp.read(fftUdpBuffer, packetSize); - } + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { fftUdp.read(fftUdpBuffer, packetSize); } - // Process each received packet: last value will persist, intermediate ones needed to update sequence counters - if (packetSize > 0) { - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftUdpBuffer))) { - //receivedFormat = max(receivedFormat, 2); // format V2 or V2+ - will be set in decodeAudioData() - haveFreshData |= decodeAudioData(packetSize, fftUdpBuffer); - } else if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftUdpBuffer))) { - decodeAudioData_v1(packetSize, fftUdpBuffer); - receivedFormat = 1; - haveFreshData = true; - } else { - receivedFormat = 0; // unknown format - } + // Process each received packet: last value will persist, intermediate ones needed to update sequence counters + if (packetSize > 0) { + if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char*)fftUdpBuffer))) { + //receivedFormat = max(receivedFormat, 2); // format V2 or V2+ - will be set in decodeAudioData() + haveFreshData |= decodeAudioData(packetSize, fftUdpBuffer); + } else if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char*)fftUdpBuffer))) { + decodeAudioData_v1(packetSize, fftUdpBuffer); + receivedFormat = 1; + haveFreshData = true; + } else { + receivedFormat = 0; // unknown format } + } - packetsReceived++; - } while ((packetSize > 0) && ((packetsReceived < maxSamples) || (maxSamples == AR_UDP_FLUSH_ALL))); // repeat until we have read enough packets, or no more packets available + packetsReceived++; + } while ((packetSize > 0) && ((packetsReceived < maxSamples) || (maxSamples == AR_UDP_FLUSH_ALL))); // repeat until we have read enough packets, or no more packets available - #if defined(WLED_DEBUG) || defined(SR_DEBUG) - if ((packetsReceived > 1) && haveFreshData) {DEBUGSR_PRINTF("AR UDP: dropped %d packets [%lums]\t%d maxDrop.\n", packetsReceived-1, millis() - last_UDPTime, maxSamples-1);} // for debugging - #endif - return haveFreshData; - } + #if defined(WLED_DEBUG) || defined(SR_DEBUG) + if ((packetsReceived > 1) && haveFreshData) { DEBUGSR_PRINTF("AR UDP: dropped %d packets [%lums]\t%d maxDrop.\n", packetsReceived - 1, millis() - last_UDPTime, maxSamples - 1); } // for debugging + #endif + return haveFreshData; + } - ////////////////////// - // usermod functions// - ////////////////////// + ////////////////////// + // usermod functions// + ////////////////////// - public: - //Functions called by WLED or other usermods + 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 - isOOM = false; - 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 = 12; - 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; + void setup() override { + disableSoundProcessing = true; // just to be sure + isOOM = false; + 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 = 12; + 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 - um_data->u_data[8] = &FFT_MajPeakSmth; // new - um_data->u_type[8] = UMT_FLOAT; + um_data->u_data[8] = &FFT_MajPeakSmth; // new + um_data->u_type[8] = UMT_FLOAT; #else - um_data->u_data[8] = &FFT_MajorPeak; // substitute for 8266 - um_data->u_type[8] = UMT_FLOAT; -#endif - um_data->u_data[9] = &soundPressure; // used (New) - um_data->u_type[9] = UMT_FLOAT; - um_data->u_data[10] = &agcSensitivity; // used (New) - dummy value on 8266 - um_data->u_type[10] = UMT_FLOAT; - um_data->u_data[11] = &zeroCrossingCount; // for auto playlist usermod - um_data->u_type[11] = UMT_UINT16; - } + um_data->u_data[8] = &FFT_MajorPeak; // substitute for 8266 + um_data->u_type[8] = UMT_FLOAT; +#endif + um_data->u_data[9] = &soundPressure; // used (New) + um_data->u_type[9] = UMT_FLOAT; + um_data->u_data[10] = &agcSensitivity; // used (New) - dummy value on 8266 + um_data->u_type[10] = UMT_FLOAT; + um_data->u_data[11] = &zeroCrossingCount; // for auto playlist usermod + um_data->u_type[11] = UMT_UINT16; + } #ifdef ARDUINO_ARCH_ESP32 - // Reset I2S peripheral for good measure - not needed in esp-idf v4.4.x and later. + // Reset I2S peripheral for good measure - not needed in esp-idf v4.4.x and later. #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) - i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed + i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed #if defined(ARDUINO_ARCH_ESP32) && !defined(__riscv) delay(100); - periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 + periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 #endif #endif - delay(100); // Give that poor microphone some time to setup. + delay(100); // Give that poor microphone some time to setup. + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4))) dmType = 51; // dummy user support: SCK == -1 --means--> PDM microphone + #endif - #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) - if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) - && ((dmType == 1) || (dmType == 4)) ) dmType = 51; // dummy user support: SCK == -1 --means--> PDM microphone - #endif + useInputFilter = 2; // default: DC blocker + switch (dmType) { + // stub cases for not-yet-supported I2S modes on other ESP32 chips + #if !defined(CONFIG_IDF_TARGET_ESP32) || !defined(SOC_I2S_SUPPORTS_ADC) || (ESP_IDF_VERSION_MAJOR > 4) + case 0: //ADC analog + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) || defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2) + case 5: //PDM Microphone + case 51: //legacy PDM Microphone + #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).")); + //useInputFilter = 0; // in case you need to disable low-cut software filtering + audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; - useInputFilter = 2; // default: DC blocker - switch (dmType) { - // stub cases for not-yet-supported I2S modes on other ESP32 chips - #if !defined(CONFIG_IDF_TARGET_ESP32) || !defined(SOC_I2S_SUPPORTS_ADC) || (ESP_IDF_VERSION_MAJOR > 4) - case 0: //ADC analog - #endif - #if !defined(SOC_I2S_SUPPORTS_PDM_RX) || defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2) - case 5: //PDM Microphone - case 51: //legacy PDM Microphone - #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).")); - //useInputFilter = 0; // in case you need to disable low-cut software filtering - audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - // WLEDMM align global pins - if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) - if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; - - 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); - //audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f, false); // I2S SLAVE mode - does not work, unfortunately - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) - case 5: - DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useInputFilter = 1; // PDM bandpass filter - 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; - case 51: - DEBUGSR_PRINT(F("AR: Legacy PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f); - useInputFilter = 1; // PDM bandpass filter - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); - break; - #endif - case 6: + 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); + //audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f, false); // I2S SLAVE mode - does not work, unfortunately + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) + case 5: + DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); + DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f / 4.0f); + useInputFilter = 1; // PDM bandpass filter - 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; + case 51: + DEBUGSR_PRINT(F("AR: Legacy PDM Microphone - ")); + DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + useInputFilter = 1; // PDM bandpass filter + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); + break; + #endif + case 6: #ifdef use_es8388_mic - DEBUGSR_PRINTLN(F("AR: ES8388 Source (Mic)")); + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Mic)")); #else - DEBUGSR_PRINTLN(F("AR: ES8388 Source (Line-In)")); + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Line-In)")); #endif - audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); - //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour - delay(100); - // WLEDMM align global pins - if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) - if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; - - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 7: + audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 7: #ifdef use_wm8978_mic - DEBUGSR_PRINTLN(F("AR: WM8978 Source (Mic)")); + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Mic)")); #else - DEBUGSR_PRINTLN(F("AR: WM8978 Source (Line-In)")); - #endif - audioSource = new WM8978Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); - //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour - delay(100); - // WLEDMM align global pins - if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) - if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; - - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 8: - DEBUGSR_PRINTLN(F("AR: AC101 Source (Line-In)")); - audioSource = new AC101Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); - //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour - delay(100); - // WLEDMM align global pins - if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) - if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 10: // reserved for ES7210 (without ES83xx) - USER_PRINTLN(F("AR: ES7220 Source (Mic) is not supported yet - trying ES8311.")); - // falls through - case 9: - DEBUGSR_PRINTLN(F("AR: ES8311 Source (Mic)")); - audioSource = new ES8311Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); - //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour - delay(100); - // WLEDMM align global pins - if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) - if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - - case 255: // falls through - case 254: // dummy "network receive only" driver - if (audioSource) delete audioSource; - audioSource = nullptr; - disableSoundProcessing = true; - audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode - break; - - #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) - // ADC over I2S is only possible on "classic" ESP32 - case 0: - default: - DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); - useInputFilter = 1; // PDM bandpass filter seems to work well for analog, too - audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(audioPin); - break; + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Line-In)")); #endif - } - delay(250); // give microphone enough time to initialise + audioSource = new WM8978Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; - if (!audioSource && (dmType < 254)) enabled = false; // audio failed to initialise -#endif - if (enabled) onUpdateBegin(false); // create FFT task, and initialize network + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 8: + DEBUGSR_PRINTLN(F("AR: AC101 Source (Line-In)")); + audioSource = new AC101Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 10: // reserved for ES7210 (without ES83xx) + USER_PRINTLN(F("AR: ES7220 Source (Mic) is not supported yet - trying ES8311.")); + // falls through + case 9: + DEBUGSR_PRINTLN(F("AR: ES8311 Source (Mic)")); + audioSource = new ES8311Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + + case 255: // falls through + case 254: // dummy "network receive only" driver + if (audioSource) delete audioSource; + audioSource = nullptr; + disableSoundProcessing = true; + audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode + break; + + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) + // ADC over I2S is only possible on "classic" ESP32 + case 0: + default: + DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); + useInputFilter = 1; // PDM bandpass filter seems to work well for analog, too + audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(audioPin); + break; + #endif + } + delay(250); // give microphone enough time to initialise + + if (!audioSource && (dmType < 254)) enabled = false; // audio failed to initialise +#endif // ARDUINO_ARCH_ESP32 + + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 - if (audioSource && 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 + if (audioSource && 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 - if (dmType < 254) { USER_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));} - else { USER_PRINTLN(F("AR: No sound input driver configured - network receive only."));} - disableSoundProcessing = true; + if (dmType < 254) { + USER_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); } else { - USER_PRINTLN(F("AR: sound input driver initialized successfully.")); - } -#endif - if (enabled) disableSoundProcessing = false; // all good - enable audio processing - // try to start UDP - last_UDPTime = 0; - receivedFormat = 0; - delay(100); - if (enabled) connectUDPSoundSync(); - if (enabled && addPalettes) createAudioPalettes(); - initDone = true; - DEBUGSR_PRINT(F("AR: init done, enabled = ")); - DEBUGSR_PRINTLN(enabled ? F("true.") : F("false.")); - USER_FLUSH(); - - // dump audiosync data layout - #if defined(SR_DEBUG) - { - audioSyncPacket data; - USER_PRINTF("\naudioSyncPacket_v1 size = %d\n", sizeof(audioSyncPacket_v1)); // size 88 - USER_PRINTF("audioSyncPacket size = %d\n", sizeof(audioSyncPacket)); // size 44 - USER_PRINTF("| char header[6] offset = %2d size = %2d\n", offsetof(audioSyncPacket, header[0]), sizeof(data.header)); // offset 0 size 6 - USER_PRINTF("| uint8_t pressure[2] offset = %2d size = %2d\n", offsetof(audioSyncPacket, pressure[0]), sizeof(data.pressure)); // offset 6 size 2 - USER_PRINTF("| float sampleRaw offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleRaw), sizeof(data.sampleRaw)); // offset 8 size 4 - USER_PRINTF("| float sampleSmth offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleSmth), sizeof(data.sampleSmth)); // offset 12 size 4 - USER_PRINTF("| uint8_t samplePeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, samplePeak), sizeof(data.samplePeak)); // offset 16 size 1 - USER_PRINTF("| uint8_t frameCounter offset = %2d size = %2d\n", offsetof(audioSyncPacket, frameCounter), sizeof(data.frameCounter)); // offset 17 size 1 - USER_PRINTF("| uint8_t fftResult[16] offset = %2d size = %2d\n", offsetof(audioSyncPacket, fftResult[0]), sizeof(data.fftResult)); // offset 18 size 16 - USER_PRINTF("| uint16_t zeroCrossingCount offset = %2d size = %2d\n", offsetof(audioSyncPacket, zeroCrossingCount), sizeof(data.zeroCrossingCount)); // offset 34 size 2 - USER_PRINTF("| float FFT_Magnitude offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_Magnitude), sizeof(data.FFT_Magnitude));// offset 36 size 4 - USER_PRINTF("| float FFT_MajorPeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_MajorPeak), sizeof(data.FFT_MajorPeak));// offset 40 size 4 - USER_PRINTLN(); USER_FLUSH(); + USER_PRINTLN(F("AR: No sound input driver configured - network receive only.")); } - #endif + disableSoundProcessing = true; + } else { + USER_PRINTLN(F("AR: sound input driver initialized successfully.")); + } +#endif // ARDUINO_ARCH_ESP32 + + if (enabled) disableSoundProcessing = false; // all good - enable audio processing + // try to start UDP + last_UDPTime = 0; + receivedFormat = 0; + delay(100); + if (enabled) connectUDPSoundSync(); + if (enabled && addPalettes) createAudioPalettes(); + initDone = true; + DEBUGSR_PRINT(F("AR: init done, enabled = ")); + DEBUGSR_PRINTLN(enabled ? F("true.") : F("false.")); + USER_FLUSH(); - #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) - DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM - #endif + // dump audiosync data layout + #if defined(SR_DEBUG) + { + audioSyncPacket data; + USER_PRINTF("\naudioSyncPacket_v1 size = %d\n", sizeof(audioSyncPacket_v1)); // size 88 + USER_PRINTF("audioSyncPacket size = %d\n", sizeof(audioSyncPacket)); // size 44 + USER_PRINTF("| char header[6] offset = %2d size = %2d\n", offsetof(audioSyncPacket, header[0]), sizeof(data.header)); // offset 0 size 6 + USER_PRINTF("| uint8_t pressure[2] offset = %2d size = %2d\n", offsetof(audioSyncPacket, pressure[0]), sizeof(data.pressure)); // offset 6 size 2 + USER_PRINTF("| float sampleRaw offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleRaw), sizeof(data.sampleRaw)); // offset 8 size 4 + USER_PRINTF("| float sampleSmth offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleSmth), sizeof(data.sampleSmth)); // offset 12 size 4 + USER_PRINTF("| uint8_t samplePeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, samplePeak), sizeof(data.samplePeak)); // offset 16 size 1 + USER_PRINTF("| uint8_t frameCounter offset = %2d size = %2d\n", offsetof(audioSyncPacket, frameCounter), sizeof(data.frameCounter)); // offset 17 size 1 + USER_PRINTF("| uint8_t fftResult[16] offset = %2d size = %2d\n", offsetof(audioSyncPacket, fftResult[0]), sizeof(data.fftResult)); // offset 18 size 16 + USER_PRINTF("| uint16_t zeroCrossingCount offset = %2d size = %2d\n", offsetof(audioSyncPacket, zeroCrossingCount), sizeof(data.zeroCrossingCount)); // offset 34 size 2 + USER_PRINTF("| float FFT_Magnitude offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_Magnitude), sizeof(data.FFT_Magnitude)); // offset 36 size 4 + USER_PRINTF("| float FFT_MajorPeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_MajorPeak), sizeof(data.FFT_MajorPeak)); // offset 40 size 4 + USER_PRINTLN(); + USER_FLUSH(); } + #endif + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif + } /* * 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(); - receivedFormat = 0; - DEBUGSR_PRINTLN(F("AR connected(): old UDP connection closed.")); - } - - if ((audioSyncPort > 0) && (audioSyncEnabled > AUDIOSYNC_NONE)) { + void connected() override { + if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection + udpSyncConnected = false; + fftUdp.stop(); + receivedFormat = 0; + DEBUGSR_PRINTLN(F("AR connected(): old UDP connection closed.")); + } + + if ((audioSyncPort > 0) && (audioSyncEnabled > AUDIOSYNC_NONE)) { #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 - receivedFormat = 0; - if (udpSyncConnected) last_UDPTime = millis(); - if (apActive && !(WLED_CONNECTED)) { - DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected using AP.") : F("AR connected(): UDP is disconnected (AP).")); - } else { - DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected to WIFI.") : F("AR connected(): UDP is disconnected (Wifi).")); - } + receivedFormat = 0; + if (udpSyncConnected) last_UDPTime = millis(); + if (apActive && !(WLED_CONNECTED)) { + DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected using AP.") : F("AR connected(): UDP is disconnected (AP).")); + } else { + DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected to WIFI.") : F("AR connected(): UDP is disconnected (Wifi).")); } - - #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) - DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM - #endif } + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif + } /* * loop() is called continuously. Here you can check for events, read sensors, etc. @@ -2410,646 +2418,624 @@ class AudioReactive : public Usermod { * 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(); + 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.isServicing() && (millis() - lastUMRun < 2)) return; // WLEDMM isServicing() is the critical part (be nice, but not too nice) + + // sound sync "receive or local" + bool useNetworkAudio = false; + if (audioSyncEnabled > AUDIOSYNC_SEND) { // we are in "receive" or "receive+local" mode + if (udpSyncConnected && (receivedFormat > 0) && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) // receivedFormat == 0 means we haven't received valid data yet + useNetworkAudio = true; + else useNetworkAudio = false; + if (audioSyncEnabled == AUDIOSYNC_REC) useNetworkAudio = true; // don't fall back to local audio in standard "receive mode" + } - if (!enabled) { - disableSoundProcessing = true; // keep processing suspended (FFT task) - lastUMRun = millis(); // update time keeping - return; + // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX) + // exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input. + if ((realtimeMode != REALTIME_MODE_INACTIVE) && (realtimeOverride == REALTIME_OVERRIDE_NONE) && !useMainSegmentOnly) { + #ifdef WLED_DEBUG + if ((disableSoundProcessing == false) && (audioSyncEnabled < AUDIOSYNC_REC)) { // we just switched to "disabled" + DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); + DEBUG_PRINTF(" RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } - // We cannot wait indefinitely before processing audio data - if (strip.isServicing() && (millis() - lastUMRun < 2)) return; // WLEDMM isServicing() is the critical part (be nice, but not too nice) - - // sound sync "receive or local" - bool useNetworkAudio = false; - if (audioSyncEnabled > AUDIOSYNC_SEND) { // we are in "receive" or "receive+local" mode - if (udpSyncConnected && (receivedFormat > 0) && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) // receivedFormat == 0 means we haven't received valid data yet - useNetworkAudio = true; - else - useNetworkAudio = false; - if (audioSyncEnabled == AUDIOSYNC_REC) - useNetworkAudio = true; // don't fall back to local audio in standard "receive mode" + #endif + disableSoundProcessing = true; + useNetworkAudio = false; + } else { + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == true) && (audioSyncEnabled < AUDIOSYNC_REC) && audioSource->isInitialized()) { // we just switched to "enabled" + DEBUG_PRINTLN("[AR userLoop] realtime mode ended - audio processing resumed."); + DEBUG_PRINTF(" RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } + #endif + if ((disableSoundProcessing == true) && (audioSyncEnabled != AUDIOSYNC_REC)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + disableSoundProcessing = false; + } - // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX) - // exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input. - if ((realtimeMode != REALTIME_MODE_INACTIVE) && (realtimeOverride == REALTIME_OVERRIDE_NONE) && !useMainSegmentOnly) { - #ifdef WLED_DEBUG - if ((disableSoundProcessing == false) && (audioSyncEnabled < AUDIOSYNC_REC)) { // we just switched to "disabled" - DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); - DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); - } - #endif - disableSoundProcessing = true; - useNetworkAudio = false; - } else { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled < AUDIOSYNC_REC) && audioSource->isInitialized()) { // we just switched to "enabled" - DEBUG_PRINTLN("[AR userLoop] realtime mode ended - audio processing resumed."); - DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); - } - #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled != AUDIOSYNC_REC)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping - disableSoundProcessing = false; - } + if (audioSyncEnabled == AUDIOSYNC_REC) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode + if (audioSyncEnabled == AUDIOSYNC_SEND) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode - if (audioSyncEnabled == AUDIOSYNC_REC) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled == AUDIOSYNC_SEND) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode #ifdef ARDUINO_ARCH_ESP32 - if (!audioSource || !audioSource->isInitialized()) { // no audio source - disableSoundProcessing = true; - if (audioSyncEnabled > AUDIOSYNC_SEND) useNetworkAudio = true; - } - if ((audioSyncEnabled == AUDIOSYNC_REC_PLUS) && useNetworkAudio) disableSoundProcessing = true; // UDP sound receiving - disable local audio - - #ifdef SR_DEBUG - // debug info in case that task stack usage changes - static unsigned int minLoopStackFree = UINT32_MAX; - unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); - if (minLoopStackFree > stackFree) { - minLoopStackFree = stackFree; - DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minLoopStackFree); //WLEDMM - } - #endif + if (!audioSource || !audioSource->isInitialized()) { // no audio source + disableSoundProcessing = true; + if (audioSyncEnabled > AUDIOSYNC_SEND) useNetworkAudio = true; + } + if ((audioSyncEnabled == AUDIOSYNC_REC_PLUS) && useNetworkAudio) disableSoundProcessing = true; // UDP sound receiving - disable local audio + + #ifdef SR_DEBUG + // debug info in case that task stack usage changes + static unsigned int minLoopStackFree = UINT32_MAX; + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + if (minLoopStackFree > stackFree) { + minLoopStackFree = stackFree; + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minLoopStackFree); //WLEDMM + } + #endif - // Only run the sampling code IF we're not in Receive mode or realtime mode - if ((audioSyncEnabled != AUDIOSYNC_REC) && !disableSoundProcessing && !useNetworkAudio) { - if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) + // Only run the sampling code IF we're not in Receive mode or realtime mode + if ((audioSyncEnabled != AUDIOSYNC_REC) && !disableSoundProcessing && !useNetworkAudio) { + 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. + 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. - #if defined(SR_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 > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == AUDIOSYNC_NONE)) { - //DEBUG_PRINTF("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n", userloopDelay); - //} - #endif + #if defined(SR_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 > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == AUDIOSYNC_NONE)) { + //DEBUG_PRINTF("[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 - - // get AGC sensitivity and sound pressure - static unsigned long lastEstimate = 0; -#ifdef WLEDMM_FASTPATH + // 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 + + // get AGC sensitivity and sound pressure + static unsigned long lastEstimate = 0; + #ifdef WLEDMM_FASTPATH if (millis() - lastEstimate > 7) { -#else + #else if (millis() - lastEstimate > 12) { -#endif - lastEstimate = millis(); - agcSensitivity = getSensitivity(); - if (limiterOn) - soundPressure = soundPressure + 0.38f * (estimatePressure() - soundPressure); // dynamics limiter on -> some smoothing - else - soundPressure = soundPressure + 0.95f * (estimatePressure() - soundPressure); // dynamics limiter on -> raw value + #endif + lastEstimate = millis(); + agcSensitivity = getSensitivity(); + if (limiterOn) soundPressure = soundPressure + 0.38f * (estimatePressure() - soundPressure); // dynamics limiter on -> some smoothing + else soundPressure = soundPressure + 0.95f * (estimatePressure() - soundPressure); // dynamics limiter on -> raw value + } + limitSampleDynamics(); + } // if (!disableSoundProcessing) +#endif // ARDUINO_ARCH_ESP32 + + 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 & AUDIOSYNC_REC) && 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) { + // DEBUG_PRINTF(F("AR reading at %d compared to %d max\n"), millis() - lastTime, delayMs); // TroyHacks + + unsigned timeElapsed = (millis() - last_UDPTime); + unsigned maxReadSamples = timeElapsed / AR_UDP_READ_INTERVAL_MS; // estimate how many packets arrived since last receive + maxReadSamples = max(1U, min(maxReadSamples, 20U)); // constrain to [1...20] = max 380ms drop + // check if we should purge the receiving queue + switch (audioSyncPurge) { + case 0: maxReadSamples = 1; break; // never drop packets, unless new connection or timed out + case 2: maxReadSamples = AR_UDP_FLUSH_ALL; break; // always drop - process latest packet only + default: + // falls through + case 1: // auto drop when silence detected, or when receiver loop is slower than sender + if (fabsf(volumeSmth) < 0.25f) maxReadSamples = AR_UDP_FLUSH_ALL; + break; } - 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 & AUDIOSYNC_REC) && 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) { - // DEBUG_PRINTF(F("AR reading at %d compared to %d max\n"), millis() - lastTime, delayMs); // TroyHacks - - unsigned timeElapsed = (millis() - last_UDPTime); - unsigned maxReadSamples = timeElapsed / AR_UDP_READ_INTERVAL_MS; // estimate how many packets arrived since last receive - maxReadSamples = max(1U, min(maxReadSamples, 20U)); // constrain to [1...20] = max 380ms drop - // check if we should purge the receiving queue - switch (audioSyncPurge) { - case 0: maxReadSamples = 1; break; // never drop packets, unless new connection or timed out - case 2: maxReadSamples = AR_UDP_FLUSH_ALL; break; // always drop - process latest packet only - default: - // falls through - case 1: // auto drop when silence detected, or when receiver loop is slower than sender - if (fabsf(volumeSmth) < 0.25f) maxReadSamples = AR_UDP_FLUSH_ALL; - break; - } - if (receivedFormat == 0) maxReadSamples = AR_UDP_FLUSH_ALL; // new connection -> always flush queue - if (timeElapsed >= AUDIOSYNC_IDLE_MS) maxReadSamples = AR_UDP_FLUSH_ALL; // too long since last run - always flush queue - - // try to get fresh data - have_new_sample = receiveAudioData(maxReadSamples); - if (have_new_sample) { - last_UDPTime = millis(); - useNetworkAudio = true; // UDP input arrived - use it - } - lastTime = millis(); - } else { -#ifdef ARDUINO_ARCH_ESP32 + if (receivedFormat == 0) maxReadSamples = AR_UDP_FLUSH_ALL; // new connection -> always flush queue + if (timeElapsed >= AUDIOSYNC_IDLE_MS) maxReadSamples = AR_UDP_FLUSH_ALL; // too long since last run - always flush queue + + // try to get fresh data + have_new_sample = receiveAudioData(maxReadSamples); + if (have_new_sample) { + last_UDPTime = millis(); + useNetworkAudio = true; // UDP input arrived - use it + } + lastTime = millis(); + } else { + #ifdef ARDUINO_ARCH_ESP32 #if ESP_IDF_VERSION_MAJOR < 5 - fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. + fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. #else - fftUdp.clear(); + fftUdp.clear(); #endif -#endif - } - if (useNetworkAudio) { - 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 - limitGEQDynamics(have_new_sample); // WLEDMM experimental: smooth FFT (GEQ) samples - } - } else { - receivedFormat = 0; + #endif } - - if ( (audioSyncEnabled & AUDIOSYNC_REC) // receive mode - && udpSyncConnected // connected - && (receivedFormat > 0) // we actually received something in the past - && ((millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS)) { // close connection after 25sec idle - udpSyncConnected = false; - receivedFormat = 0; - fftUdp.stop(); - volumeSmth =0.0f; - volumeRaw =0; - my_magnitude = 0.1; FFT_Magnitude = 0.01; FFT_MajorPeak = 2; - soundPressure = 1.0f; - agcSensitivity = 64.0f; -#ifdef ARDUINO_ARCH_ESP32 - multAgc = 1; -#endif - DEBUGSR_PRINTLN(F("AR loop(): UDP closed due to inactivity.")); + if (useNetworkAudio) { + 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 + limitGEQDynamics(have_new_sample); // WLEDMM experimental: smooth FFT (GEQ) samples } + } else { + receivedFormat = 0; + } - #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) - static unsigned long lastMicLoggerTime = 0; - if (millis()-lastMicLoggerTime > 20) { - lastMicLoggerTime = millis(); - logAudio(); - } + if ((audioSyncEnabled & AUDIOSYNC_REC) // receive mode + && udpSyncConnected // connected + && (receivedFormat > 0) // we actually received something in the past + && ((millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS)) { // close connection after 25sec idle + udpSyncConnected = false; + receivedFormat = 0; + fftUdp.stop(); + volumeSmth = 0.0f; + volumeRaw = 0; + my_magnitude = 0.1; + FFT_Magnitude = 0.01; + FFT_MajorPeak = 2; + soundPressure = 1.0f; + agcSensitivity = 64.0f; + #ifdef ARDUINO_ARCH_ESP32 + multAgc = 1; #endif + DEBUGSR_PRINTLN(F("AR loop(): UDP closed due to inactivity.")); + } - // Info Page: keep max sample from last 5 seconds + #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.15 * maxSample5sec) + 0.85 *((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 - } + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * ((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 - // Track sender silence: reset timer when sound is present, start it when silence begins - if (audioSyncEnabled & AUDIOSYNC_SEND) { - if (volumeSmth >= 1.0f) syncSilenceStart = 0; // sound detected - clear silence timer - else if (syncSilenceStart == 0) syncSilenceStart = millis(); // just went silent - start timer - } else syncSilenceStart = 0; // not in send mode - no silence + //UDP Microphone Sync - transmit mode + // Track sender silence: reset timer when sound is present, start it when silence begins + if (audioSyncEnabled & AUDIOSYNC_SEND) { + if (volumeSmth >= 1.0f) syncSilenceStart = 0; // sound detected - clear silence timer + else if (syncSilenceStart == 0) syncSilenceStart = millis(); // just went silent - start timer + } else syncSilenceStart = 0; // not in send mode - no silence #if defined(WLEDMM_FASTPATH) if ((audioSyncEnabled & AUDIOSYNC_SEND) && (haveNewFFTResult || (millis() - lastTime > 24))) { // fastpath: send data once results are ready, or each 25ms as fallback (max sampling time is 23ms) #else - if ((audioSyncEnabled & AUDIOSYNC_SEND) && (millis() - lastTime > 20)) { // standard: send data each 20ms + if ((audioSyncEnabled & AUDIOSYNC_SEND) && (millis() - lastTime > 20)) { // standard: send data each 20ms #endif - haveNewFFTResult = false; // reset notification - // Only run the transmit code IF we're in Transmit mode - // Sender silence gate: skip transmission when silence has persisted for more than 30 seconds - if (!audioSyncSilenceStop || (syncSilenceStart == 0) || (millis() - syncSilenceStart < AUDIOSYNC_SEND_STOP_MS)) { - transmitAudioData(); - } - lastTime = millis(); - } -#endif - - fillAudioPalettes(); + haveNewFFTResult = false; // reset notification + // Only run the transmit code IF we're in Transmit mode + // Sender silence gate: skip transmission when silence has persisted for more than 30 seconds + if (!audioSyncSilenceStop || (syncSilenceStart == 0) || (millis() - syncSilenceStart < AUDIOSYNC_SEND_STOP_MS)) { transmitAudioData(); } + lastTime = millis(); } +#endif // ARDUINO_ARCH_ESP32 + + fillAudioPalettes(); + } #if defined(_MoonModules_WLED_) && defined(WLEDMM_FASTPATH) - void loop2(void) override { - loop(); - } + void loop2(void) override { loop(); } #endif - 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; - } + 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 = filterTime = 0; -#endif - // gracefully suspend FFT task (if running) - disableSoundProcessing = true; + void onUpdateBegin(bool init) override { + #ifdef WLED_DEBUG + fftTime = sampleTime = filterTime = 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)); - memset(paletteBandAvg, 0, sizeof(paletteBandAvg)); - for(int i=(init?0:1); i don't process audio - updateIsRunning = init; } -#endif + if (enabled) disableSoundProcessing = init; // init = true means that OTA is just starting --> don't process audio + updateIsRunning = init; + } +#endif // 8266 #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 -#ifndef WLED_USE_PINMANAGER_V14 - && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) -#else - && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) -#endif - ) { - return true; - } - return false; + **/ + 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 + #ifndef WLED_USE_PINMANAGER_V14 + && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) + #else + && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) + #endif + ) { + return true; } -#endif + return false; + } +#endif // ARDUINO_ARCH_ESP32 - //////////////////////////// - // Settings and Info Page // - //////////////////////////// + //////////////////////////// + // 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"); + 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)); + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - String uiDomString = F(""); - infoArr.add(uiDomString); + String uiDomString = F(""); + infoArr.add(uiDomString); - if (enabled) { - bool audioSyncIDLE = false; // true if sound sync is not receiving + if (enabled) { + bool audioSyncIDLE = false; // true if sound sync is not receiving -#ifdef ARDUINO_ARCH_ESP32 - // audio sync status - if ((audioSyncEnabled & AUDIOSYNC_REC) && (!udpSyncConnected || (millis() - last_UDPTime > AUDIOSYNC_IDLE_MS))) // connected and nothing received in 2.5sec - audioSyncIDLE = true; - if ((audioSource == nullptr) || (!audioSource->isInitialized())) // local audio not configured - audioSyncIDLE = false; - - // 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 - // show slider value as a number - float post_gain = (float)inputLevel/128.0f; - if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; - post_gain = roundf(post_gain * 100.0f); - snprintf_P(myStringBuffer, 15, PSTR("%3.0f %%"), post_gain); - infoArr.add(myStringBuffer); - } 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 == AUDIOSYNC_REC) || (!audioSyncIDLE && (audioSyncEnabled == AUDIOSYNC_REC_PLUS))){ - // UDP sound sync - receive mode - infoArr.add(F("UDP sound sync")); - if (udpSyncConnected) { - if (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS) - infoArr.add(F(" - receiving")); - else { - if((last_UDPTime > 0) && (millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS) infoArr.add(F(" - reconnect")); - else infoArr.add(F(" - idle")); - } - } else { - infoArr.add(F(" - no connection")); + #ifdef ARDUINO_ARCH_ESP32 + // audio sync status + if ((audioSyncEnabled & AUDIOSYNC_REC) && (!udpSyncConnected || (millis() - last_UDPTime > AUDIOSYNC_IDLE_MS))) // connected and nothing received in 2.5sec + audioSyncIDLE = true; + if ((audioSource == nullptr) || (!audioSource->isInitialized())) // local audio not configured + audioSyncIDLE = false; + + // 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 + // show slider value as a number + float post_gain = (float)inputLevel / 128.0f; + if (post_gain < 1.0f) post_gain = ((post_gain - 1.0f) * 0.8f) + 1.0f; + post_gain = roundf(post_gain * 100.0f); + snprintf_P(myStringBuffer, 15, PSTR("%3.0f %%"), post_gain); + infoArr.add(myStringBuffer); + } else { + infoArr = user.createNestedArray(F("Audio Input Level")); + } + uiDomString = F("
"); // + infoArr.add(uiDomString); + } + #endif // ARDUINO_ARCH_ESP32 + + // 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 == AUDIOSYNC_REC) || (!audioSyncIDLE && (audioSyncEnabled == AUDIOSYNC_REC_PLUS))) { + // UDP sound sync - receive mode + infoArr.add(F("UDP sound sync")); + if (udpSyncConnected) { + if (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS) infoArr.add(F(" - receiving")); + else { + if ((last_UDPTime > 0) && (millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS) infoArr.add(F(" - reconnect")); + else infoArr.add(F(" - idle")); } -#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 } else { - infoArr.add(F("sound sync Off")); + 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()) && !isOOM) { - // audio source successfully configured - if (audioSource->getType() == AudioSource::Type_I2SAdc) { - infoArr.add(F("ADC analog")); - } else { - if (dmType != 51) { - if (dmType == 5) infoArr.add(F("PDM digital")); - else infoArr.add(F("I2S digital")); - } - else - infoArr.add(F("legacy PDM")); - } - // input level or "silence" - if (maxSample5sec > 1.0) { - 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 { + // Analog or I2S digital input + if (audioSource && (audioSource->isInitialized()) && !isOOM) { + // audio source successfully configured + if (audioSource->getType() == AudioSource::Type_I2SAdc) { + infoArr.add(F("ADC analog")); } else { - // error during audio source setup - infoArr.add(F("not initialized")); - if (isOOM) infoArr.add(F(" - out of memory")); - else if (dmType < 254) infoArr.add(F(" - check pin settings")); + if (dmType != 51) { + if (dmType == 5) infoArr.add(F("PDM digital")); + else infoArr.add(F("I2S digital")); + } else infoArr.add(F("legacy PDM")); + } + // input level or "silence" + if (maxSample5sec > 1.0) { + 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")); } - } - - // 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")); + // error during audio source setup + infoArr.add(F("not initialized")); + if (isOOM) infoArr.add(F(" - out of memory")); + else if (dmType < 254) infoArr.add(F(" - check pin settings")); } + } - // AGC or manual Gain - if ((soundAgc == 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { - 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 > 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { - 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 & AUDIOSYNC_SEND) { - infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < AUDIOSYNC_IDLE_MS)) { - if (audioSyncSilenceStop && (syncSilenceStart > 0) && unsigned(millis() - syncSilenceStart) > AUDIOSYNC_SEND_STOP_MS) - infoArr.add(F(" (silence)")); - else - infoArr.add(F(" v2+")); - } - } else if (audioSyncEnabled == AUDIOSYNC_REC) { - infoArr.add(F("receive mode")); - } else if (audioSyncEnabled == AUDIOSYNC_REC_PLUS) { - infoArr.add(F("receive+local mode")); + // 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 == AUDIOSYNC_REC)) { + 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 > 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { + infoArr = user.createNestedArray(F("AGC Gain")); + infoArr.add(roundf(multAgc * 100.0f) / 100.0f); + infoArr.add("x"); + } +#endif // ARDUINO_ARCH_ESP32 + + // UDP Sound Sync status + infoArr = user.createNestedArray(F("UDP Sound Sync")); + if (audioSyncEnabled) { + if (audioSyncEnabled & AUDIOSYNC_SEND) { + infoArr.add(F("send mode")); + if ((udpSyncConnected) && (millis() - lastTime < AUDIOSYNC_IDLE_MS)) { + if (audioSyncSilenceStop && (syncSilenceStart > 0) && unsigned(millis() - syncSilenceStart) > AUDIOSYNC_SEND_STOP_MS) infoArr.add(F(" (silence)")); + else infoArr.add(F(" v2+")); } - } else - infoArr.add("off"); - - if (audioSyncEnabled && udpSyncConnected && unsigned(millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS) infoArr.add(" (reconnecting)"); - else if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS)) { - if (receivedFormat == 1) infoArr.add(F(" v1")); - if (receivedFormat == 2) infoArr.add(F(" v2")); - if (receivedFormat == 3) { - if (audioSyncSequence) infoArr.add(F(" v2+")); // Sequence checking enabled - else infoArr.add(F(" v2")); - } + } else if (audioSyncEnabled == AUDIOSYNC_REC) { + infoArr.add(F("receive mode")); + } else if (audioSyncEnabled == AUDIOSYNC_REC_PLUS) { + infoArr.add(F("receive+local mode")); } - - #ifdef ARDUINO_ARCH_ESP32 - - if (!disableSoundProcessing && (audioSyncEnabled & AUDIOSYNC_SEND)) { // send mode idle time - unsigned silencetime = 0; - if (syncSilenceStart > 0) silencetime = millis() - syncSilenceStart; - infoArr = user.createNestedArray(F("Sound silence")); - infoArr.add(silencetime/1000); - infoArr.add(" sec"); - } else if (audioSyncEnabled) { // receive mode idle time - unsigned noUDPtime = 0; - if (last_UDPTime > 0) noUDPtime = millis() - last_UDPTime; - infoArr = user.createNestedArray(F("Sound receive idle")); - infoArr.add(noUDPtime/1000); - infoArr.add(" sec"); + } else infoArr.add("off"); + + if (audioSyncEnabled && udpSyncConnected && unsigned(millis() - last_UDPTime) > AUDIOSYNC_REC_DEAD_MS) infoArr.add(" (reconnecting)"); + else if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS)) { + if (receivedFormat == 1) infoArr.add(F(" v1")); + if (receivedFormat == 2) infoArr.add(F(" v2")); + if (receivedFormat == 3) { + if (audioSyncSequence) infoArr.add(F(" v2+")); // Sequence checking enabled + else infoArr.add(F(" v2")); } + } - #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) - - if (!disableSoundProcessing) { // FFTTask timing - infoArr = user.createNestedArray(F("I2S cycle time")); - infoArr.add(roundf(fftTaskCycle)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(roundf(sampleTime)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("Filtering time")); - infoArr.add(roundf(filterTime)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("FFT time")); - infoArr.add(roundf(fftTime)/100.0f); +#ifdef ARDUINO_ARCH_ESP32 + if (!disableSoundProcessing && (audioSyncEnabled & AUDIOSYNC_SEND)) { // send mode idle time + unsigned silencetime = 0; + if (syncSilenceStart > 0) silencetime = millis() - syncSilenceStart; + infoArr = user.createNestedArray(F("Sound silence")); + infoArr.add(silencetime / 1000); + infoArr.add(" sec"); + } else if (audioSyncEnabled) { // receive mode idle time + unsigned noUDPtime = 0; + if (last_UDPTime > 0) noUDPtime = millis() - last_UDPTime; + infoArr = user.createNestedArray(F("Sound receive idle")); + infoArr.add(noUDPtime / 1000); + infoArr.add(" sec"); + } + + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + if (!disableSoundProcessing) { // FFTTask timing + infoArr = user.createNestedArray(F("I2S cycle time")); + infoArr.add(roundf(fftTaskCycle) / 100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("Sampling time")); + infoArr.add(roundf(sampleTime) / 100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("Filtering time")); + infoArr.add(roundf(filterTime) / 100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("FFT time")); + infoArr.add(roundf(fftTime) / 100.0f); -#ifdef FFT_USE_SLIDING_WINDOW + #ifdef FFT_USE_SLIDING_WINDOW unsigned timeBudget = doSlidingFFT ? (FFT_MIN_CYCLE) : fftTaskCycle / 115; -#else + #else unsigned timeBudget = (FFT_MIN_CYCLE); -#endif - if ((fftTime/100) >= timeBudget) // FFT time over budget -> I2S buffer will overflow - infoArr.add("! ms"); - else if ((fftTime/85 + filterTime/85 + sampleTime/85) >= timeBudget) // FFT time >75% of budget -> risk of instability - infoArr.add(" ms!"); - else - infoArr.add(" ms"); - } - - DEBUGSR_PRINTF("AR I2S cycle time: %5.2f ms\n", roundf(fftTaskCycle)/100.0f); - DEBUGSR_PRINTF("AR Sampling time : %5.2f ms\n", roundf(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR filter time : %5.2f ms\n", roundf(filterTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", roundf(fftTime)/100.0f); #endif - #endif - } + if ((fftTime / 100) >= timeBudget) // FFT time over budget -> I2S buffer will overflow + infoArr.add("! ms"); + else if ((fftTime / 85 + filterTime / 85 + sampleTime / 85) >= timeBudget) // FFT time >75% of budget -> risk of instability + infoArr.add(" ms!"); + else infoArr.add(" ms"); + } + + DEBUGSR_PRINTF("AR I2S cycle time: %5.2f ms\n", roundf(fftTaskCycle) / 100.0f); + DEBUGSR_PRINTF("AR Sampling time : %5.2f ms\n", roundf(sampleTime) / 100.0f); + DEBUGSR_PRINTF("AR filter time : %5.2f ms\n", roundf(filterTime) / 100.0f); + DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", roundf(fftTime) / 100.0f); + #endif // WLED_DEBUG +#endif // ARDUINO_ARCH_ESP32 } - + } /* * 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; - } - + 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())); + 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(); } -#endif } + #ifdef ARDUINO_ARCH_ESP32 + if (usermod[FPSTR(_inputLvl)].is()) { inputLevel = min(255, max(0, usermod[FPSTR(_inputLvl)].as())); } + #endif } + } - void onStateChange(uint8_t callMode) { - if (initDone && enabled && addPalettes && palettes==0) { - // if palettes were removed during JSON call re-add them - createAudioPalettes(); - } + void onStateChange(uint8_t callMode) { + if (initDone && enabled && addPalettes && palettes == 0) { + // 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. @@ -3086,65 +3072,64 @@ class AudioReactive : public Usermod { * * 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; + 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_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); - amic["pin"] = audioPin; + amic["pin"] = audioPin; #endif - JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); - dmic[F("type")] = dmType; - // WLEDMM: align with globals I2C pins - if ((dmType == 2) || (dmType == 6)) { // only for ES7243 and ES8388 - if (i2c_sda >= 0) sdaPin = -1; // -1 = use global - if (i2c_scl >= 0) sclPin = -1; // -1 = use global - } - JsonArray pinArray = dmic.createNestedArray("pin"); - pinArray.add(i2ssdPin); - pinArray.add(i2swsPin); - pinArray.add(i2sckPin); - pinArray.add(mclkPin); - pinArray.add(sdaPin); - pinArray.add(sclPin); - - JsonObject cfg = top.createNestedObject("config"); - cfg[F("squelch")] = soundSquelch; - cfg[F("gain")] = sampleGain; - cfg[F("AGC")] = soundAgc; - - //WLEDMM: experimental settings - JsonObject poweruser = top.createNestedObject("experiments"); - poweruser[F("micLev")] = micLevelMethod; - poweruser[F("Mic_Quality")] = micQuality; - poweruser[F("freqDist")] = freqDist; - //poweruser[F("freqRMS")] = averageByRMS; - poweruser[F("FFT_Window")] = fftWindow; - #ifdef FFT_USE_SLIDING_WINDOW - poweruser[F("I2S_FastPath")] = doSlidingFFT; - #endif - JsonObject freqScale = top.createNestedObject("frequency"); - freqScale[F("scale")] = FFTScalingMode; - freqScale[F("profile")] = pinkIndex; //WLEDMM -#endif - - JsonObject dynLim = top.createNestedObject("dynamics"); - dynLim[F("limiter")] = limiterOn; - dynLim[F("rise")] = attackTime; - dynLim[F("fall")] = decayTime; - - JsonObject sync = top.createNestedObject("sync"); - sync[F("port")] = audioSyncPort; - sync[F("mode")] = audioSyncEnabled; - sync[F("skip_old_data")] = audioSyncPurge; - sync[F("check_sequence")] = audioSyncSequence; - sync[F("silence_stop")] = audioSyncSilenceStop; - sync[F("broadcast")] = audioSyncBroadcast; + JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); + dmic[F("type")] = dmType; + // WLEDMM: align with globals I2C pins + if ((dmType == 2) || (dmType == 6)) { // only for ES7243 and ES8388 + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; // -1 = use global } - + JsonArray pinArray = dmic.createNestedArray("pin"); + pinArray.add(i2ssdPin); + pinArray.add(i2swsPin); + pinArray.add(i2sckPin); + pinArray.add(mclkPin); + pinArray.add(sdaPin); + pinArray.add(sclPin); + + JsonObject cfg = top.createNestedObject("config"); + cfg[F("squelch")] = soundSquelch; + cfg[F("gain")] = sampleGain; + cfg[F("AGC")] = soundAgc; + + //WLEDMM: experimental settings + JsonObject poweruser = top.createNestedObject("experiments"); + poweruser[F("micLev")] = micLevelMethod; + poweruser[F("Mic_Quality")] = micQuality; + poweruser[F("freqDist")] = freqDist; + //poweruser[F("freqRMS")] = averageByRMS; + poweruser[F("FFT_Window")] = fftWindow; + #ifdef FFT_USE_SLIDING_WINDOW + poweruser[F("I2S_FastPath")] = doSlidingFFT; + #endif + JsonObject freqScale = top.createNestedObject("frequency"); + freqScale[F("scale")] = FFTScalingMode; + freqScale[F("profile")] = pinkIndex; //WLEDMM +#endif // ARDUINO_ARCH_ESP32 + + JsonObject dynLim = top.createNestedObject("dynamics"); + dynLim[F("limiter")] = limiterOn; + dynLim[F("rise")] = attackTime; + dynLim[F("fall")] = decayTime; + + JsonObject sync = top.createNestedObject("sync"); + sync[F("port")] = audioSyncPort; + sync[F("mode")] = audioSyncEnabled; + sync[F("skip_old_data")] = audioSyncPurge; + sync[F("check_sequence")] = audioSyncSequence; + sync[F("silence_stop")] = audioSyncSilenceStop; + sync[F("broadcast")] = audioSyncBroadcast; + } /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). @@ -3161,374 +3146,402 @@ class AudioReactive : public Usermod { * * 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 oldAddPalettes = addPalettes; + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + bool oldAddPalettes = addPalettes; #ifdef ARDUINO_ARCH_ESP32 - // remember previous values - auto oldEnabled = enabled; - auto oldDMType = dmType; - auto oldI2SsdPin = i2ssdPin; - auto oldI2SwsPin = i2swsPin; - auto oldI2SckPin = i2sckPin; - auto oldI2SmclkPin = mclkPin; -#endif - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); - configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); + // remember previous values + auto oldEnabled = enabled; + auto oldDMType = dmType; + auto oldI2SsdPin = i2ssdPin; + auto oldI2SwsPin = i2swsPin; + auto oldI2SckPin = i2sckPin; + auto oldI2SmclkPin = mclkPin; +#endif // ARDUINO_ARCH_ESP32 + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); #ifdef ARDUINO_ARCH_ESP32 #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); #else - audioPin = -1; // MCU does not support analog mic + audioPin = -1; // MCU does not support analog mic #endif - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); #if !defined(CONFIG_IDF_TARGET_ESP32) if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog #if !defined(SOC_I2S_SUPPORTS_PDM_RX) || defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2) - if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM - if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM + if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM #else - if (dmType == 5) useInputFilter = 1; // enable filter for PDM - if (dmType == 51) useInputFilter = 1; // switch on filter for legacy 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(_digitalmic)]["pin"][4], sdaPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][5], sclPin); - - configComplete &= getJsonValue(top["config"][F("squelch")], soundSquelch); - configComplete &= getJsonValue(top["config"][F("gain")], sampleGain); - configComplete &= getJsonValue(top["config"][F("AGC")], soundAgc); - - //WLEDMM: experimental settings - configComplete &= getJsonValue(top["experiments"][F("micLev")], micLevelMethod); - configComplete &= getJsonValue(top["experiments"][F("Mic_Quality")], micQuality); - configComplete &= getJsonValue(top["experiments"][F("freqDist")], freqDist); - //configComplete &= getJsonValue(top["experiments"][F("freqRMS")], averageByRMS); - configComplete &= getJsonValue(top["experiments"][F("FFT_Window")], fftWindow); -#ifdef FFT_USE_SLIDING_WINDOW + if (dmType == 5) useInputFilter = 1; // enable filter for PDM + if (dmType == 51) useInputFilter = 1; // switch on filter for legacy 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(_digitalmic)]["pin"][4], sdaPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][5], sclPin); + + configComplete &= getJsonValue(top["config"][F("squelch")], soundSquelch); + configComplete &= getJsonValue(top["config"][F("gain")], sampleGain); + configComplete &= getJsonValue(top["config"][F("AGC")], soundAgc); + + //WLEDMM: experimental settings + configComplete &= getJsonValue(top["experiments"][F("micLev")], micLevelMethod); + configComplete &= getJsonValue(top["experiments"][F("Mic_Quality")], micQuality); + configComplete &= getJsonValue(top["experiments"][F("freqDist")], freqDist); + //configComplete &= getJsonValue(top["experiments"][F("freqRMS")], averageByRMS); + configComplete &= getJsonValue(top["experiments"][F("FFT_Window")], fftWindow); + #ifdef FFT_USE_SLIDING_WINDOW configComplete &= getJsonValue(top["experiments"][F("I2S_FastPath")], doSlidingFFT); -#endif + #endif - configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); - configComplete &= getJsonValue(top["frequency"][F("profile")], pinkIndex); //WLEDMM -#endif - configComplete &= getJsonValue(top["dynamics"][F("limiter")], limiterOn); - configComplete &= getJsonValue(top["dynamics"][F("rise")], attackTime); - configComplete &= getJsonValue(top["dynamics"][F("fall")], decayTime); + configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); + configComplete &= getJsonValue(top["frequency"][F("profile")], pinkIndex); //WLEDMM +#endif // ARDUINO_ARCH_ESP32 + + configComplete &= getJsonValue(top["dynamics"][F("limiter")], limiterOn); + configComplete &= getJsonValue(top["dynamics"][F("rise")], attackTime); + configComplete &= getJsonValue(top["dynamics"][F("fall")], decayTime); - configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); - configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); - configComplete &= getJsonValue(top["sync"][F("skip_old_data")], audioSyncPurge); - configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); - configComplete &= getJsonValue(top["sync"][F("silence_stop")], audioSyncSilenceStop); - configComplete &= getJsonValue(top["sync"][F("broadcast")], audioSyncBroadcast); + configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); + configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); + configComplete &= getJsonValue(top["sync"][F("skip_old_data")], audioSyncPurge); + configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); + configComplete &= getJsonValue(top["sync"][F("silence_stop")], audioSyncSilenceStop); + configComplete &= getJsonValue(top["sync"][F("broadcast")], audioSyncBroadcast); #ifdef ARDUINO_ARCH_ESP32 - if (initDone) { - // add/remove custom/audioreactive palettes - if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); - if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); - } -#endif + if (initDone) { + // add/remove custom/audioreactive palettes + if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); + if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); + } +#endif // ARDUINO_ARCH_ESP32 - // WLEDMM notify user when a reboot is necessary - #ifdef ARDUINO_ARCH_ESP32 - if (initDone) { - if ((audioSource != nullptr) && (oldDMType != dmType)) errorFlag = ERR_REBOOT_NEEDED; // changing mic type requires reboot - if ( (audioSource != nullptr) && (enabled==true) - && ((oldI2SsdPin != i2ssdPin) || (oldI2SwsPin != i2swsPin) || (oldI2SckPin != i2sckPin)) ) errorFlag = ERR_REBOOT_NEEDED; // changing mic pins requires reboot - if ((audioSource != nullptr) && (oldI2SmclkPin != mclkPin)) errorFlag = ERR_REBOOT_NEEDED; // changing MCLK pin requires reboot - if ((oldDMType != dmType) && (oldDMType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing from analog mic requires power cycle - if ((oldDMType != dmType) && (dmType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing to analog mic requires power cycle - } - #endif - return configComplete; +// WLEDMM notify user when a reboot is necessary +#ifdef ARDUINO_ARCH_ESP32 + if (initDone) { + if ((audioSource != nullptr) && (oldDMType != dmType)) errorFlag = ERR_REBOOT_NEEDED; // changing mic type requires reboot + if ((audioSource != nullptr) && (enabled == true) && ((oldI2SsdPin != i2ssdPin) || (oldI2SwsPin != i2swsPin) || (oldI2SckPin != i2sckPin))) errorFlag = ERR_REBOOT_NEEDED; // changing mic pins requires reboot + if ((audioSource != nullptr) && (oldI2SmclkPin != mclkPin)) errorFlag = ERR_REBOOT_NEEDED; // changing MCLK pin requires reboot + if ((oldDMType != dmType) && (oldDMType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing from analog mic requires power cycle + if ((oldDMType != dmType) && (dmType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing to analog mic requires power cycle } +#endif // ARDUINO_ARCH_ESP32 + return configComplete; + } - void appendConfigData() override { - oappend(SET_F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style - oappend(SET_F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] - oappend(SET_F("addInfo(ux+':help',0,'');")); -#ifdef ARDUINO_ARCH_ESP32 - //WLEDMM: add defaults - #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // -S3/-S2/-C3 don't support analog audio + void appendConfigData() override { + oappend(SET_F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style + oappend(SET_F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] + oappend(SET_F("addInfo(ux+':help',0,'');")); +#ifdef ARDUINO_ARCH_ESP32 + //WLEDMM: add defaults + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // -S3/-S2/-C3 don't support analog audio #ifdef AUDIOPIN - oappend(SET_F("xOpt(ux+':analogmic:pin',1,' ⎌',")); oappendi(AUDIOPIN); oappend(");"); - #endif - oappend(SET_F("aOpt(ux+':analogmic:pin',1);")); //only analog options + oappend(SET_F("xOpt(ux+':analogmic:pin',1,' ⎌',")); + oappendi(AUDIOPIN); + oappend(");"); #endif + oappend(SET_F("aOpt(ux+':analogmic:pin',1);")); //only analog options + #endif - oappend(SET_F("dd=addDropdown(ux,'digitalmic:type');")); - #if SR_DMTYPE==254 - oappend(SET_F("addOption(dd,'None - network receive only (⎌)',254);")); - #else - oappend(SET_F("addOption(dd,'None - network receive only',254);")); - #endif - #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) - #if SR_DMTYPE==0 - oappend(SET_F("addOption(dd,'Generic Analog (⎌)',0);")); - #else - oappend(SET_F("addOption(dd,'Generic Analog',0);")); - #endif - #endif - #if SR_DMTYPE==1 - oappend(SET_F("addOption(dd,'Generic I2S (⎌)',1);")); - #else - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - #endif - #if SR_DMTYPE==2 - oappend(SET_F("addOption(dd,'ES7243 (⎌)',2);")); - #else - oappend(SET_F("addOption(dd,'ES7243',2);")); - #endif - #if SR_DMTYPE==3 - oappend(SET_F("addOption(dd,'SPH0654 (⎌)',3);")); - #else - oappend(SET_F("addOption(dd,'SPH0654',3);")); - #endif - #if SR_DMTYPE==4 - oappend(SET_F("addOption(dd,'Generic I2S with Mclk (⎌)',4);")); - #else - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); - #endif - #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) - #if SR_DMTYPE==5 - oappend(SET_F("addOption(dd,'Generic PDM (⎌)',5);")); - #else - oappend(SET_F("addOption(dd,'Generic PDM',5);")); - #endif - #if SR_DMTYPE==51 - oappend(SET_F("addOption(dd,'.Legacy PDM ☾ (⎌)',51);")); - #else - oappend(SET_F("addOption(dd,'.Legacy PDM ☾',51);")); - #endif - #endif - #if SR_DMTYPE==6 - oappend(SET_F("addOption(dd,'ES8388 ☾ (⎌)',6);")); - #else - oappend(SET_F("addOption(dd,'ES8388 ☾',6);")); - #endif - #if SR_DMTYPE==7 - oappend(SET_F("addOption(dd,'WM8978 ☾ (⎌)',7);")); + oappend(SET_F("dd=addDropdown(ux,'digitalmic:type');")); + #if SR_DMTYPE == 254 + oappend(SET_F("addOption(dd,'None - network receive only (⎌)',254);")); + #else + oappend(SET_F("addOption(dd,'None - network receive only',254);")); + #endif + + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) + #if SR_DMTYPE == 0 + oappend(SET_F("addOption(dd,'Generic Analog (⎌)',0);")); #else - oappend(SET_F("addOption(dd,'WM8978 ☾',7);")); + oappend(SET_F("addOption(dd,'Generic Analog',0);")); #endif - #if SR_DMTYPE==8 - oappend(SET_F("addOption(dd,'AC101 ☾ (⎌)',8);")); + #endif + + #if SR_DMTYPE == 1 + oappend(SET_F("addOption(dd,'Generic I2S (⎌)',1);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S',1);")); + #endif + #if SR_DMTYPE == 2 + oappend(SET_F("addOption(dd,'ES7243 (⎌)',2);")); + #else + oappend(SET_F("addOption(dd,'ES7243',2);")); + #endif + #if SR_DMTYPE == 3 + oappend(SET_F("addOption(dd,'SPH0654 (⎌)',3);")); + #else + oappend(SET_F("addOption(dd,'SPH0654',3);")); + #endif + #if SR_DMTYPE == 4 + oappend(SET_F("addOption(dd,'Generic I2S with Mclk (⎌)',4);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + #endif + + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) + #if SR_DMTYPE == 5 + oappend(SET_F("addOption(dd,'Generic PDM (⎌)',5);")); #else - oappend(SET_F("addOption(dd,'AC101 ☾',8);")); + oappend(SET_F("addOption(dd,'Generic PDM',5);")); #endif - #if SR_DMTYPE==9 - oappend(SET_F("addOption(dd,'ES8311 ☾ (⎌)',9);")); + #if SR_DMTYPE == 51 + oappend(SET_F("addOption(dd,'.Legacy PDM ☾ (⎌)',51);")); #else - oappend(SET_F("addOption(dd,'ES8311 ☾',9);")); - #endif - #ifdef SR_SQUELCH - oappend(SET_F("addInfo(ux+':config:squelch',1,'⎌ ")); oappendi(SR_SQUELCH); oappend("');"); // 0 is field type, 1 is actual field - #endif - #ifdef SR_GAIN - oappend(SET_F("addInfo(ux+':config:gain',1,'⎌ ")); oappendi(SR_GAIN); oappend("');"); // 0 is field type, 1 is actual field + oappend(SET_F("addOption(dd,'.Legacy PDM ☾',51);")); #endif + #endif + + #if SR_DMTYPE == 6 + oappend(SET_F("addOption(dd,'ES8388 ☾ (⎌)',6);")); + #else + oappend(SET_F("addOption(dd,'ES8388 ☾',6);")); + #endif + #if SR_DMTYPE == 7 + oappend(SET_F("addOption(dd,'WM8978 ☾ (⎌)',7);")); + #else + oappend(SET_F("addOption(dd,'WM8978 ☾',7);")); + #endif + #if SR_DMTYPE == 8 + oappend(SET_F("addOption(dd,'AC101 ☾ (⎌)',8);")); + #else + oappend(SET_F("addOption(dd,'AC101 ☾',8);")); + #endif + #if SR_DMTYPE == 9 + oappend(SET_F("addOption(dd,'ES8311 ☾ (⎌)',9);")); + #else + oappend(SET_F("addOption(dd,'ES8311 ☾',9);")); + #endif + #ifdef SR_SQUELCH + oappend(SET_F("addInfo(ux+':config:squelch',1,'⎌ ")); + oappendi(SR_SQUELCH); + oappend("');"); // 0 is field type, 1 is actual field + #endif + #ifdef SR_GAIN + oappend(SET_F("addInfo(ux+':config:gain',1,'⎌ ")); + oappendi(SR_GAIN); + oappend("');"); // 0 is field type, 1 is actual field + #endif - oappend(SET_F("dd=addDropdown(ux,'config:AGC');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Normal',1);")); - oappend(SET_F("addOption(dd,'Vivid',2);")); - oappend(SET_F("addOption(dd,'Lazy',3);")); - - //WLEDMM: experimental settings - oappend(SET_F("xx='experiments';")); // shortcut - oappend(SET_F("dd=addDropdown(ux,xx+':micLev');")); - oappend(SET_F("addOption(dd,'Floating',0);")); - oappend(SET_F("addOption(dd,'Freeze (⎌)',1);")); - oappend(SET_F("addOption(dd,'Fast Freeze',2);")); - oappend(SET_F("addInfo(ux+':'+xx+':micLev',1,'☾');")); - - oappend(SET_F("dd=addDropdown(ux,xx+':Mic_Quality');")); - oappend(SET_F("addOption(dd,'average (standard)',0);")); - oappend(SET_F("addOption(dd,'low noise',1);")); - oappend(SET_F("addOption(dd,'perfect',2);")); - - oappend(SET_F("dd=addDropdown(ux,xx+':freqDist');")); - oappend(SET_F("addOption(dd,'Normal (⎌)',0);")); - oappend(SET_F("addOption(dd,'RightShift',1);")); - oappend(SET_F("addInfo(ux+':'+xx+':freqDist',1,'☾');")); - - //oappend(SET_F("dd=addDropdown(ux,xx+':freqRMS');")); - //oappend(SET_F("addOption(dd,'Off (⎌)',0);")); - //oappend(SET_F("addOption(dd,'On',1);")); - //oappend(SET_F("addInfo(ux+':experiments:freqRMS',1,'☾');")); - - oappend(SET_F("dd=addDropdown(ux,xx+':FFT_Window');")); - oappend(SET_F("addOption(dd,'Blackman-Harris (MM standard)',0);")); - oappend(SET_F("addOption(dd,'Hann (balanced)',1);")); - oappend(SET_F("addOption(dd,'Nuttall (more accurate)',2);")); - oappend(SET_F("addOption(dd,'Blackman',5);")); - oappend(SET_F("addOption(dd,'Hamming',3);")); - oappend(SET_F("addOption(dd,'Flat-Top (AC WLED, inaccurate)',4);")); + oappend(SET_F("dd=addDropdown(ux,'config:AGC');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Normal',1);")); + oappend(SET_F("addOption(dd,'Vivid',2);")); + oappend(SET_F("addOption(dd,'Lazy',3);")); + + //WLEDMM: experimental settings + oappend(SET_F("xx='experiments';")); // shortcut + oappend(SET_F("dd=addDropdown(ux,xx+':micLev');")); + oappend(SET_F("addOption(dd,'Floating',0);")); + oappend(SET_F("addOption(dd,'Freeze (⎌)',1);")); + oappend(SET_F("addOption(dd,'Fast Freeze',2);")); + oappend(SET_F("addInfo(ux+':'+xx+':micLev',1,'☾');")); + + oappend(SET_F("dd=addDropdown(ux,xx+':Mic_Quality');")); + oappend(SET_F("addOption(dd,'average (standard)',0);")); + oappend(SET_F("addOption(dd,'low noise',1);")); + oappend(SET_F("addOption(dd,'perfect',2);")); + + oappend(SET_F("dd=addDropdown(ux,xx+':freqDist');")); + oappend(SET_F("addOption(dd,'Normal (⎌)',0);")); + oappend(SET_F("addOption(dd,'RightShift',1);")); + oappend(SET_F("addInfo(ux+':'+xx+':freqDist',1,'☾');")); + + //oappend(SET_F("dd=addDropdown(ux,xx+':freqRMS');")); + //oappend(SET_F("addOption(dd,'Off (⎌)',0);")); + //oappend(SET_F("addOption(dd,'On',1);")); + //oappend(SET_F("addInfo(ux+':experiments:freqRMS',1,'☾');")); + + oappend(SET_F("dd=addDropdown(ux,xx+':FFT_Window');")); + oappend(SET_F("addOption(dd,'Blackman-Harris (MM standard)',0);")); + oappend(SET_F("addOption(dd,'Hann (balanced)',1);")); + oappend(SET_F("addOption(dd,'Nuttall (more accurate)',2);")); + oappend(SET_F("addOption(dd,'Blackman',5);")); + oappend(SET_F("addOption(dd,'Hamming',3);")); + oappend(SET_F("addOption(dd,'Flat-Top (AC WLED, inaccurate)',4);")); -#ifdef FFT_USE_SLIDING_WINDOW + #ifdef FFT_USE_SLIDING_WINDOW oappend(SET_F("dd=addDropdown(ux,xx+':I2S_FastPath');")); oappend(SET_F("addOption(dd,'Off',0);")); oappend(SET_F("addOption(dd,'On (⎌)',1);")); oappend(SET_F("addInfo(ux+':'+xx+':I2S_FastPath',1,'☾');")); -#endif + #endif - oappend(SET_F("dd=addDropdown(ux,'dynamics:limiter');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); - - oappend(SET_F("dd=addDropdown(ux,'frequency:scale');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); - oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); - oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); - - //WLEDMM add defaults - oappend(SET_F("dd=addDropdown(ux,'frequency:profile');")); - #if SR_FREQ_PROF==0 - oappend(SET_F("addOption(dd,'Generic Microphone (⎌)',0);")); - #else - oappend(SET_F("addOption(dd,'Generic Microphone',0);")); - #endif - #if SR_FREQ_PROF==1 - oappend(SET_F("addOption(dd,'Generic Line-In (⎌)',1);")); - #else - oappend(SET_F("addOption(dd,'Generic Line-In',1);")); - #endif - #if SR_FREQ_PROF==5 - oappend(SET_F("addOption(dd,'ICS-43434 (⎌)',5);")); - #else - oappend(SET_F("addOption(dd,'ICS-43434',5);")); - #endif - #if SR_FREQ_PROF==6 - oappend(SET_F("addOption(dd,'ICS-43434 - big speakers (⎌)',6);")); - #else - oappend(SET_F("addOption(dd,'ICS-43434 - big speakers',6);")); - #endif - #if SR_FREQ_PROF==7 - oappend(SET_F("addOption(dd,'SPM1423 (⎌)',7);")); - #else - oappend(SET_F("addOption(dd,'SPM1423',7);")); - #endif - #if SR_FREQ_PROF==2 - oappend(SET_F("addOption(dd,'IMNP441 (⎌)',2);")); - #else - oappend(SET_F("addOption(dd,'IMNP441',2);")); - #endif - #if SR_FREQ_PROF==3 - oappend(SET_F("addOption(dd,'IMNP441 - big speakers (⎌)',3);")); - #else - oappend(SET_F("addOption(dd,'IMNP441 - big speakers',3);")); - #endif - #if SR_FREQ_PROF==4 - oappend(SET_F("addOption(dd,'IMNP441 - small speakers (⎌)',4);")); - #else - oappend(SET_F("addOption(dd,'IMNP441 - small speakers',4);")); - #endif - #if SR_FREQ_PROF==10 - oappend(SET_F("addOption(dd,'flat - no adjustments (⎌)',10);")); - #else - oappend(SET_F("addOption(dd,'flat - no adjustments',10);")); - #endif - #if SR_FREQ_PROF==8 - oappend(SET_F("addOption(dd,'userdefined #1 (⎌)',8);")); - #else - oappend(SET_F("addOption(dd,'userdefined #1',8);")); - #endif - #if SR_FREQ_PROF==9 - oappend(SET_F("addOption(dd,'userdefined #2 (⎌)',9);")); - #else - oappend(SET_F("addOption(dd,'userdefined #2',9);")); - #endif - oappend(SET_F("addInfo(ux+':frequency:profile',1,'☾');")); -#endif - oappend(SET_F("dd=addDropdown(ux,'sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); // AUDIOSYNC_NONE -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Send',1);")); // AUDIOSYNC_SEND -#endif - oappend(SET_F("addOption(dd,'Receive',2);")); // AUDIOSYNC_REC -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Receive or Local',6);")); // AUDIOSYNC_REC_PLUS -#endif - // Receiver drops old packets and processes the latest packet only - oappend(SET_F("dd=addDropdown(ux,'sync:skip_old_data');")); - oappend(SET_F("addOption(dd,'Never',0);")); - oappend(SET_F("addOption(dd,'Auto (recommended)',1);")); // auto = drop during silence, or when last receive happened too long ago - oappend(SET_F("addOption(dd,'Always',2);")); - // check_sequence: Receiver skips out-of-sequence packets when enabled - oappend(SET_F("dd=addDropdown(ux,'sync:check_sequence');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo(ux+':sync:check_sequence',1,'when receiving');")); + oappend(SET_F("dd=addDropdown(ux,'dynamics:limiter');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + oappend(SET_F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); + + oappend(SET_F("dd=addDropdown(ux,'frequency:scale');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); + oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); + oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); + + //WLEDMM add defaults + oappend(SET_F("dd=addDropdown(ux,'frequency:profile');")); + #if SR_FREQ_PROF == 0 + oappend(SET_F("addOption(dd,'Generic Microphone (⎌)',0);")); + #else + oappend(SET_F("addOption(dd,'Generic Microphone',0);")); + #endif + #if SR_FREQ_PROF == 1 + oappend(SET_F("addOption(dd,'Generic Line-In (⎌)',1);")); + #else + oappend(SET_F("addOption(dd,'Generic Line-In',1);")); + #endif + #if SR_FREQ_PROF == 5 + oappend(SET_F("addOption(dd,'ICS-43434 (⎌)',5);")); + #else + oappend(SET_F("addOption(dd,'ICS-43434',5);")); + #endif + #if SR_FREQ_PROF == 6 + oappend(SET_F("addOption(dd,'ICS-43434 - big speakers (⎌)',6);")); + #else + oappend(SET_F("addOption(dd,'ICS-43434 - big speakers',6);")); + #endif + #if SR_FREQ_PROF == 7 + oappend(SET_F("addOption(dd,'SPM1423 (⎌)',7);")); + #else + oappend(SET_F("addOption(dd,'SPM1423',7);")); + #endif + #if SR_FREQ_PROF == 2 + oappend(SET_F("addOption(dd,'IMNP441 (⎌)',2);")); + #else + oappend(SET_F("addOption(dd,'IMNP441',2);")); + #endif + #if SR_FREQ_PROF == 3 + oappend(SET_F("addOption(dd,'IMNP441 - big speakers (⎌)',3);")); + #else + oappend(SET_F("addOption(dd,'IMNP441 - big speakers',3);")); + #endif + #if SR_FREQ_PROF == 4 + oappend(SET_F("addOption(dd,'IMNP441 - small speakers (⎌)',4);")); + #else + oappend(SET_F("addOption(dd,'IMNP441 - small speakers',4);")); + #endif + #if SR_FREQ_PROF == 10 + oappend(SET_F("addOption(dd,'flat - no adjustments (⎌)',10);")); + #else + oappend(SET_F("addOption(dd,'flat - no adjustments',10);")); + #endif + #if SR_FREQ_PROF == 8 + oappend(SET_F("addOption(dd,'userdefined #1 (⎌)',8);")); + #else + oappend(SET_F("addOption(dd,'userdefined #1',8);")); + #endif + #if SR_FREQ_PROF == 9 + oappend(SET_F("addOption(dd,'userdefined #2 (⎌)',9);")); + #else + oappend(SET_F("addOption(dd,'userdefined #2',9);")); + #endif + oappend(SET_F("addInfo(ux+':frequency:profile',1,'☾');")); +#endif // ARDUINO_ARCH_ESP32 + + oappend(SET_F("dd=addDropdown(ux,'sync:mode');")); + oappend(SET_F("addOption(dd,'Off',0);")); // AUDIOSYNC_NONE #ifdef ARDUINO_ARCH_ESP32 - // silence_stop: Sender stops transmitting UDP packets after 30s of continuous silence - oappend(SET_F("dd=addDropdown(ux,'sync:silence_stop');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); -#endif + oappend(SET_F("addOption(dd,'Send',1);")); // AUDIOSYNC_SEND +#endif // ARDUINO_ARCH_ESP32 - // broadcast: send to subnet broadcast address instead of multicast - oappend(SET_F("addInfo(ux+':sync:broadcast',1,'use broadcast instead of multicast ☾
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' + oappend(SET_F("addOption(dd,'Receive',2);")); // AUDIOSYNC_REC - oappend(SET_F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field #ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); - #ifdef I2S_SDPIN - oappend(SET_F("xOpt(uxp,0,' ⎌',")); oappendi(I2S_SDPIN); oappend(");"); - #endif + oappend(SET_F("addOption(dd,'Receive or Local',6);")); // AUDIOSYNC_REC_PLUS +#endif // ARDUINO_ARCH_ESP32 + + // Receiver drops old packets and processes the latest packet only + oappend(SET_F("dd=addDropdown(ux,'sync:skip_old_data');")); + oappend(SET_F("addOption(dd,'Never',0);")); + oappend(SET_F("addOption(dd,'Auto (recommended)',1);")); // auto = drop during silence, or when last receive happened too long ago + oappend(SET_F("addOption(dd,'Always',2);")); + // check_sequence: Receiver skips out-of-sequence packets when enabled + oappend(SET_F("dd=addDropdown(ux,'sync:check_sequence');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + oappend(SET_F("addInfo(ux+':sync:check_sequence',1,'when receiving');")); - oappend(SET_F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); - oappend(SET_F("dRO(uxp,1);")); // disable read only pins - #ifdef I2S_WSPIN - oappend(SET_F("xOpt(uxp,1,' ⎌',")); oappendi(I2S_WSPIN); oappend(");"); - #endif +#ifdef ARDUINO_ARCH_ESP32 + // silence_stop: Sender stops transmitting UDP packets after 30s of continuous silence + oappend(SET_F("dd=addDropdown(ux,'sync:silence_stop');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); +#endif // ARDUINO_ARCH_ESP32 - oappend(SET_F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); - oappend(SET_F("dRO(uxp,2);")); // disable read only pins - #ifdef I2S_CKPIN - oappend(SET_F("xOpt(uxp,2,' ⎌',")); oappendi(I2S_CKPIN); oappend(");"); - #endif + // broadcast: send to subnet broadcast address instead of multicast + oappend(SET_F("addInfo(ux+':sync:broadcast',1,'use broadcast instead of multicast ☾
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' - oappend(SET_F("addInfo(uxp,3,'master clock','I2S MCLK');")); - oappend(SET_F("dRO(uxp,3);")); // disable read only pins - #if defined(CONFIG_IDF_TARGET_ESP32) - oappend(SET_F("dOpt(uxp,3,2,2);")); //only use -1, 0, 1 or 3 - oappend(SET_F("dOpt(uxp,3,4,39);")); //only use -1, 0, 1 or 3 - #endif - #ifdef MCLK_PIN - oappend(SET_F("xOpt(uxp,3,' ⎌',")); oappendi(MCLK_PIN); oappend(");"); - #endif + oappend(SET_F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo(uxp,4,'','I2C SDA');")); - oappend(SET_F("rOpt(uxp,4,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); - #ifdef ES7243_SDAPIN - oappend(SET_F("xOpt(uxp,4,' ⎌',")); oappendi(ES7243_SDAPIN); oappend(");"); - #endif +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); + #ifdef I2S_SDPIN + oappend(SET_F("xOpt(uxp,0,' ⎌',")); + oappendi(I2S_SDPIN); + oappend(");"); + #endif + oappend(SET_F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); + oappend(SET_F("dRO(uxp,1);")); // disable read only pins + #ifdef I2S_WSPIN + oappend(SET_F("xOpt(uxp,1,' ⎌',")); + oappendi(I2S_WSPIN); + oappend(");"); + #endif + oappend(SET_F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); + oappend(SET_F("dRO(uxp,2);")); // disable read only pins + #ifdef I2S_CKPIN + oappend(SET_F("xOpt(uxp,2,' ⎌',")); + oappendi(I2S_CKPIN); + oappend(");"); + #endif + oappend(SET_F("addInfo(uxp,3,'master clock','I2S MCLK');")); + oappend(SET_F("dRO(uxp,3);")); // disable read only pins + #if defined(CONFIG_IDF_TARGET_ESP32) + oappend(SET_F("dOpt(uxp,3,2,2);")); //only use -1, 0, 1 or 3 + oappend(SET_F("dOpt(uxp,3,4,39);")); //only use -1, 0, 1 or 3 + #endif + #ifdef MCLK_PIN + oappend(SET_F("xOpt(uxp,3,' ⎌',")); + oappendi(MCLK_PIN); + oappend(");"); + #endif + oappend(SET_F("addInfo(uxp,4,'','I2C SDA');")); + oappend(SET_F("rOpt(uxp,4,'use global (")); + oappendi(i2c_sda); + oappend(")',-1);"); + #ifdef ES7243_SDAPIN + oappend(SET_F("xOpt(uxp,4,' ⎌',")); + oappendi(ES7243_SDAPIN); + oappend(");"); + #endif - oappend(SET_F("addInfo(uxp,5,'','I2C SCL');")); - oappend(SET_F("rOpt(uxp,5,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); - #ifdef ES7243_SCLPIN - oappend(SET_F("xOpt(uxp,5,' ⎌',")); oappendi(ES7243_SCLPIN); oappend(");"); - #endif - oappend(SET_F("dRO(uxp,5);")); // disable read only pins -#endif - } + oappend(SET_F("addInfo(uxp,5,'','I2C SCL');")); + oappend(SET_F("rOpt(uxp,5,'use global (")); + oappendi(i2c_scl); + oappend(")',-1);"); + #ifdef ES7243_SCLPIN + oappend(SET_F("xOpt(uxp,5,' ⎌',")); + oappendi(ES7243_SCLPIN); + oappend(");"); + #endif + oappend(SET_F("dRO(uxp,5);")); // disable read only pins +#endif // ARDUINO_ARCH_ESP32 + + } /* @@ -3536,20 +3549,17 @@ class AudioReactive : public Usermod { * 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() - //{ - //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black - //} + //void handleOverlayDraw() + //{ + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + //} + - /* * getId() allows you to optionally give your V2 usermod a 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; - } + uint16_t getId() override { return USERMOD_ID_AUDIOREACTIVE; } }; void AudioReactive::removeAudioPalettes(void) { @@ -3561,7 +3571,7 @@ void AudioReactive::removeAudioPalettes(void) { } DEBUG_PRINTLN(F("Removing audio palettes.")); palettes -= (int8_t)removeUsermodPalettes(_name); - if (palettes < 0) palettes = 0; // safeguard + if (palettes < 0) palettes = 0; // safeguard #endif } @@ -3569,10 +3579,10 @@ void AudioReactive::createAudioPalettes(void) { #ifndef _MoonModules_WLED_ if (palettes || processingPalettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); - static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2, _palName3, _palName4}; - for (int i=0; i= NUM_GEQ_CHANNELS) return 0.0f; // out of range + if (band >= NUM_GEQ_CHANNELS) return 0.0f; // out of range -#if 1 // experimental - only works for local audio - #ifdef ARDUINO_ARCH_ESP32 // no local audio on 8266 + #if 1 // experimental - only works for local audio + #ifdef ARDUINO_ARCH_ESP32 // no local audio on 8266 if (audioSource && !disableSoundProcessing) { // use high quality data if we can // start with filtered or unfiltered raw data, depending on limiter settings float currentResult = limiterOn ? fftAvg[band] : fftCalc[band]; // Apply linear scaling - currentResult *= 0.27f; // some damping to get values below 255 + currentResult *= 0.27f; // some damping to get values below 255 if (currentResult < 1.0f) currentResult = 0.0f; // clamp to target range - return min(currentResult, 278.0f); // allow slight overshooting beyond 255 - } else - #endif -#endif - { // fall back to UDP sound data - float currentResult = float(fftResult[band]); - float scalingEstimate = 0.85f + (float(band)/3.8f); // we don't know the sender's scaling mode - this de-amp step just brings back the original shape, close to the default sqrt() scaling. - return min(currentResult / scalingEstimate, 278.0f); // allow slight overshooting beyond 255 + return min(currentResult, 278.0f); // allow slight overshooting beyond 255 + } else + #endif + #endif + { // fall back to UDP sound data + float currentResult = float(fftResult[band]); + float scalingEstimate = 0.85f + (float(band) / 3.8f); // we don't know the sender's scaling mode - this de-amp step just brings back the original shape, close to the default sqrt() scaling. + return min(currentResult / scalingEstimate, 278.0f); // allow slight overshooting beyond 255 } } @@ -3611,10 +3620,10 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { #ifndef _MoonModules_WLED_ CHSV hsv; int b; - x = constrain(x, 0, 255); // just to be safe + x = constrain(x, 0, 255); // just to be safe switch (pal) { case 2: { - b = map(x, 0, 255, 0, NUM_GEQ_CHANNELS/2); // convert palette position to lower half of freq band + b = map(x, 0, 255, 0, NUM_GEQ_CHANNELS / 2); // convert palette position to lower half of freq band hsv = CHSV(fftResult[b], 255, x); // float linBand = getRawBand(b); // hsv = CHSV(uint8_t(roundf(linBand)), 255, x); @@ -3622,7 +3631,7 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { break; } case 1: - b = map(x, 1, 255, 0, 10); // convert palette position to lower half of freq band + b = map(x, 1, 255, 0, 10); // convert palette position to lower half of freq band hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255)); // pick hue // float linBand = getRawBand(b); // hsv = CHSV(uint8_t(roundf(linBand)), 255, map(fftResult[b], 0, 255, 30, 255)); @@ -3641,16 +3650,17 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { // (they don't depend on x, so there's no need to redo the 16-channel scan here // on every one of the 3 calls per refresh). // TODO: use CHSV32 with 16-bit hue for finer resolution - int8_t hueSpread = map(x, 0, 255, -30, 30); // spread palette positions ±30 hue units + int8_t hueSpread = map(x, 0, 255, -30, 30); // spread palette positions ±30 hue units // paletteHue3 ∈ [0,200] and hueSpread ∈ [-30,30]; the sum can go negative, so clamp // through a signed int before narrowing to uint8_t instead of letting it silently // wrap around (which would turn the warm/bass end of the gradient into purple). uint8_t hue = (uint8_t)constrain((int)paletteHue3 + hueSpread, 0, 255); - hsv = CHSV(hue, paletteSat3, (uint8_t)constrain(x, 30, 255)); - value = hsv; + hsv = CHSV(hue, paletteSat3, (uint8_t)constrain(x, 30, 255)); + value = hsv; break; } // AI: end + // AI: below section was generated by an AI case 4: { // "Spectral Balance" palette (palette index 4) @@ -3664,27 +3674,27 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { // paletteHue4/paletteSat4/paletteTotal4 are precomputed once per refresh in // fillAudioPalettes() (they don't depend on x). // TODO: use CHSV32 with 16-bit hue for finer resolution - int8_t hueOffset = map(x, 0, 255, -25, 25); // spread palette positions ±25 hue units + int8_t hueOffset = map(x, 0, 255, -25, 25); // spread palette positions ±25 hue units // paletteHue4 ∈ [20,190] and hueOffset ∈ [-25,25]; clamp through a signed int // before narrowing to uint8_t so the sum can't silently wrap around uint8_t. - uint8_t hue = (uint8_t)constrain((int)paletteHue4 + hueOffset, 0, 255); + uint8_t hue = (uint8_t)constrain((int)paletteHue4 + hueOffset, 0, 255); // brightness: minimum 30, boosted by overall loudness (average band value, same 0..255 // scale as any individual band - see paletteSat3's comment for why AVERAGE rather than // the raw SUM in paletteTotal4 is used as the loudness reference) and palette position float avgEnergy4 = paletteTotal4 / (float)NUM_GEQ_CHANNELS; - uint8_t val = (uint8_t)constrain((int)my_mapf(avgEnergy4, 0.0f, 70.0f, 0.0f, 100.0f) + (int)map(x, 0, 255, 30, 255), 30, 255); // original input for avgEnergy4: 0.0f, 255.0f - hsv = CHSV(hue, paletteSat4, val); - value = hsv; + uint8_t val = (uint8_t)constrain((int)my_mapf(avgEnergy4, 0.0f, 70.0f, 0.0f, 100.0f) + (int)map(x, 0, 255, 30, 255), 30, 255); // original input for avgEnergy4: 0.0f, 255.0f + hsv = CHSV(hue, paletteSat4, val); + value = hsv; break; } // AI: end default: if (x == 1) { - value = CRGB(fftResult[10]/2, fftResult[4]/2, fftResult[0]/2); - } else if(x == 255) { - value = CRGB(fftResult[10]/2, fftResult[0]/2, fftResult[4]/2); + value = CRGB(fftResult[10] / 2, fftResult[4] / 2, fftResult[0] / 2); + } else if (x == 255) { + value = CRGB(fftResult[10] / 2, fftResult[0] / 2, fftResult[4] / 2); } else { - value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2); + value = CRGB(fftResult[0] / 2, fftResult[4] / 2, fftResult[10] / 2); } break; } @@ -3698,12 +3708,12 @@ void AudioReactive::fillAudioPalettes() { static unsigned long lastPaletteTime = 0; unsigned long nowMs = millis(); - unsigned long dtMs = lastPaletteTime ? (nowMs - lastPaletteTime) : 20UL; - if (dtMs < 10) return; // rate limit: at least 10ms between updates + unsigned long dtMs = lastPaletteTime ? (nowMs - lastPaletteTime) : 20UL; + if (dtMs < 10) return; // rate limit: at least 10ms between updates // ToDO: (optionally) only re-run the function when new FFT results are availeable (sync with FFTTask) lastPaletteTime = nowMs; - if (dtMs > 2000UL) dtMs = 2000UL; // cap a single "catch-up" jump after a long stall (OTA, file I/O) + if (dtMs > 2000UL) dtMs = 2000UL; // cap a single "catch-up" jump after a long stall (OTA, file I/O) // AI: below section was generated by an AI // Update slowly-smoothed band averages used by palettes 3 & 4, so palette colours @@ -3730,27 +3740,25 @@ void AudioReactive::fillAudioPalettes() { // the 3 getCRGBForBand() calls (x=1/128/255) made below for every refresh. { // Palette 3 ("Track Character"): spectral centroid → hue - // ToDO: This fixed frequency table does not match all supported FFT mappings. - // With freqDist=1, channel 9 receives fftAddAvg(19,25) (about 950 Hz at 22.05 kHz), but this table weights it as 1658 Hz. + // ToDO: This fixed frequency table does not match all supported FFT mappings. + // With freqDist=1, channel 9 receives fftAddAvg(19,25) (about 950 Hz at 22.05 kHz), but this table weights it as 1658 Hz. // Track Character therefore overestimates the spectral centroid and shifts the hue. // Additionally, check if RMS bin averaging can move the central frequencies. - static const float bandFreq[NUM_GEQ_CHANNELS] = { // approximate centre frequency (Hz) of each GEQ channel - need rework to match the frequency mappings of WLED-MM - 65, 107, 172, 258, 365, 495, 689, 969, - 1270, 1658, 2153, 2713, 3359, 4091, 5792, 8182 - }; + static const float bandFreq[NUM_GEQ_CHANNELS] = {// approximate centre frequency (Hz) of each GEQ channel - need rework to match the frequency mappings of WLED-MM + 65, 107, 172, 258, 365, 495, 689, 969, 1270, 1658, 2153, 2713, 3359, 4091, 5792, 8182}; float wSum = 0, tEnergy = 0; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - wSum += paletteBandAvg[i] * bandFreq[i]; // frequency-weighted energy - tEnergy += paletteBandAvg[i]; // total energy + wSum += paletteBandAvg[i] * bandFreq[i]; // frequency-weighted energy + tEnergy += paletteBandAvg[i]; // total energy } // centroid = energy-weighted average frequency; default to 500 Hz when signal is silent - // ToDo: When a high-frequency signal fades to silence, crossing tEnergy == 1 switches the centroid from about 8 kHz to 500 Hz in a single refresh. - // The base hue jumps from about 199 to 86, while case 3's bright endpoint stays at brightness 255. - // Smoothing the band energies therefore does not prevent an abrupt color change at silence. + // ToDo: When a high-frequency signal fades to silence, crossing tEnergy == 1 switches the centroid from about 8 kHz to 500 Hz in a single refresh. + // The base hue jumps from about 199 to 86, while case 3's bright endpoint stays at brightness 255. + // Smoothing the band energies therefore does not prevent an abrupt color change at silence. // --> Retain the last valid hue below the threshold, or smooth the transition to the fallback hue. // --> filter/smooth transitions // float centroid = (tEnergy > 1.0f) ? (wSum / tEnergy) : 500.0f; // original formula - float conf = tEnergy / (tEnergy + 16.0f); // confidence: ~0 when (nearly) silent, ->1 for real signals. Fades towards neutral hues instead of jumping. + float conf = tEnergy / (tEnergy + 16.0f); // confidence: ~0 when (nearly) silent, ->1 for real signals. Fades towards neutral hues instead of jumping. float centroid = (tEnergy > 0.001f) ? (wSum / tEnergy) : 500.0f; // Map centroid to hue on a log scale (human pitch perception is logarithmic). @@ -3761,8 +3769,8 @@ void AudioReactive::fillAudioPalettes() { // float logC = logf(constrain(centroid, 60.0f, 8000.0f)); // original formula // paletteHue3 = (uint8_t)my_mapf(logC, 4.0943f, 8.9872f, 0.0f, 200.0f); // mapf() cannot produce negative results due to previous constrain() --> safe to directly cast to uint8_t // range 110..4000 Hz (ln = 4.7005 .. 8.2940); hue 84 = neutral (500 Hz) - float logC = logf(constrain(centroid, 110.0f, 4000.0f)); - float hue3 = my_mapf(logC, 4.7005f, 8.2940f, 0.0f, 200.0f); + float logC = logf(constrain(centroid, 110.0f, 4000.0f)); + float hue3 = my_mapf(logC, 4.7005f, 8.2940f, 0.0f, 200.0f); paletteHue3 = (uint8_t)constrain((int)(84.0f + conf * (hue3 - 84.0f) + 0.5f), 0, 200); // Saturation rises with overall loudness. tEnergy is the SUM of 16 EMA'd bands, so its // theoretical ceiling is 16*255=4080 - but real music never drives all 16 log-spaced @@ -3772,7 +3780,7 @@ void AudioReactive::fillAudioPalettes() { // in this file (e.g. the `case 1`/`case 2` palettes above), so saturation actually // varies across a musically realistic loudness range. float avgEnergy3 = tEnergy / (float)NUM_GEQ_CHANNELS; - paletteSat3 = (uint8_t)constrain((int)my_mapf(avgEnergy3, 0.0f, 70.0f, 180.0f, 255.0f), 180, 255); // original input for avgEnergy3: 0.0f, 255.0f + paletteSat3 = (uint8_t)constrain((int)my_mapf(avgEnergy3, 0.0f, 70.0f, 180.0f, 255.0f), 180, 255); // original input for avgEnergy3: 0.0f, 255.0f // Palette 4 ("Spectral Balance"): bass/mid/high energy ratio → hue. // Reuses tEnergy (== bassEnergy+midEnergy+highEnergy, since channels 0-15 are already @@ -3780,61 +3788,60 @@ void AudioReactive::fillAudioPalettes() { // sums doing the same addition per refresh. // Palette 4 ("Spectral Balance"): bass/mid/high balance → hue. float bassEnergy = 0, midEnergy = 0, highEnergy = 0; - for (int i = 0; i < 4; i++) bassEnergy += paletteBandAvg[i]; // sub-bass + bass - for (int i = 4; i < 10; i++) midEnergy += paletteBandAvg[i]; // midrange + for (int i = 0; i < 4; i++) bassEnergy += paletteBandAvg[i]; // sub-bass + bass + for (int i = 4; i < 10; i++) midEnergy += paletteBandAvg[i]; // midrange for (int i = 10; i < 16; i++) highEnergy += paletteBandAvg[i]; // high-mid + high // Compare average channel levels rather than sums: the groups contain 4, 6, and 6 // channels, so raw sums systematically over-weight mid and high content. - bassEnergy /= 4.0f; - midEnergy /= 6.0f; - highEnergy /= 2.5f; // give treble a stronger influence on the hue - float bandTotal = bassEnergy + midEnergy + highEnergy; - float bandTotalSafe = (bandTotal < 1.0f) ? 1.0f : bandTotal; // avoid division by zero when silent - float total = (tEnergy < 1.0f) ? 1.0f : tEnergy; // retain total FFT energy for brightness - float bassRatio = bassEnergy / bandTotalSafe; - float midRatio = midEnergy / bandTotalSafe; - float highRatio = highEnergy / bandTotalSafe; + bassEnergy /= 4.0f; + midEnergy /= 6.0f; + highEnergy /= 2.5f; // give treble a stronger influence on the hue + float bandTotal = bassEnergy + midEnergy + highEnergy; + float bandTotalSafe = (bandTotal < 1.0f) ? 1.0f : bandTotal; // avoid division by zero when silent + float total = (tEnergy < 1.0f) ? 1.0f : tEnergy; // retain total FFT energy for brightness + float bassRatio = bassEnergy / bandTotalSafe; + float midRatio = midEnergy / bandTotalSafe; + float highRatio = highEnergy / bandTotalSafe; // Weighted hue: pure bass→20, pure mid→110, pure high→190 int weightedHue = (int)roundf(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); - weightedHue = 110 + (int)roundf(conf * (float)(weightedHue - 110)); // fade to neutral (mid) hue when silent - paletteHue4 = (uint8_t)constrain(weightedHue, 0, 255); + weightedHue = 110 + (int)roundf(conf * (float)(weightedHue - 110)); // fade to neutral (mid) hue when silent + paletteHue4 = (uint8_t)constrain(weightedHue, 0, 255); // Saturation: a clearly dominant band → high sat; a balanced spectrum → lower sat. // maxRatio is the max of three ratios that sum to 1, so its real range is [1/3, 1], // not [0, 1]; map that actual range onto [180, 255] instead of an arbitrary factor, // so saturation actually varies instead of floor/ceiling-clamping almost always. float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); // paletteSat4 = (uint8_t)constrain((int)my_mapf(maxRatio, 1.0f/3.0f, 1.0f, 180.0f, 255.0f), 180, 255); // original formula - paletteSat4 = (uint8_t)constrain((int)(my_mapf(maxRatio, 0.35f, 0.75f, 180.0f, 255.0f) * conf + 180.0f * (1.0f - conf)), 180, 255); + paletteSat4 = (uint8_t)constrain((int)(my_mapf(maxRatio, 0.35f, 0.75f, 180.0f, 255.0f) * conf + 180.0f * (1.0f - conf)), 180, 255); paletteTotal4 = total; } // AI: end - processingPalettes = true; // lock palettes to ensure they don't get removed while processing + processingPalettes = true; // lock palettes to ensure they don't get removed while processing // Scan by name pointer identity to find the palettes we added, palIndex = 0/1/2... selects the getCRGBForBand variant, matching how the entries were created. - for (auto &ump : usermodPalettes) { + for (auto& ump : usermodPalettes) { if (ump.name != _name) continue; const int pal = ump.palIndex; uint8_t tcp[16]; // Needs to be 4 times however many colors are being used. // 3 colors = 12, 4 colors = 16, etc. - - tcp[0] = 0; // anchor of first color - must be zero + tcp[0] = 0; // anchor of first color - must be zero tcp[1] = 0; tcp[2] = 0; tcp[3] = 0; CRGB rgb = getCRGBForBand(1, pal); - tcp[4] = 1; // anchor of first color - tcp[5] = rgb.r; - tcp[6] = rgb.g; - tcp[7] = rgb.b; - - rgb = getCRGBForBand(128, pal); - tcp[8] = 128; - tcp[9] = rgb.r; + tcp[4] = 1; // anchor of first color + tcp[5] = rgb.r; + tcp[6] = rgb.g; + tcp[7] = rgb.b; + + rgb = getCRGBForBand(128, pal); + tcp[8] = 128; + tcp[9] = rgb.r; tcp[10] = rgb.g; tcp[11] = rgb.b; - rgb = getCRGBForBand(255, pal); + rgb = getCRGBForBand(255, pal); tcp[12] = 255; // anchor of last color - must be 255 tcp[13] = rgb.r; tcp[14] = rgb.g; @@ -3842,23 +3849,23 @@ void AudioReactive::fillAudioPalettes() { ump.palette.loadDynamicGradientPalette(tcp); } - processingPalettes = false; // unlock usermodPalettes + processingPalettes = false; // unlock usermodPalettes #endif } // strings to reduce flash memory usage (used more than twice) -const char AudioReactive::_name[] PROGMEM = "AudioReactive"; -const char AudioReactive::_enabled[] PROGMEM = "enabled"; -const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel"; +const char AudioReactive::_name[] PROGMEM = "AudioReactive"; +const char AudioReactive::_enabled[] PROGMEM = "enabled"; +const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel"; #if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) -const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; -#endif -const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic"; -const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure -const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature -const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes"; -const char AudioReactive::_palName0[] PROGMEM = "Ratio"; -const char AudioReactive::_palName1[] PROGMEM = "Hue"; -const char AudioReactive::_palName2[] PROGMEM = "Spectrum"; -const char AudioReactive::_palName3[] PROGMEM = "Track Character"; -const char AudioReactive::_palName4[] PROGMEM = "Spectral Balance"; + const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; +#endif +const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic"; +const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure +const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature +const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes"; +const char AudioReactive::_palName0[] PROGMEM = "Ratio"; +const char AudioReactive::_palName1[] PROGMEM = "Hue"; +const char AudioReactive::_palName2[] PROGMEM = "Spectrum"; +const char AudioReactive::_palName3[] PROGMEM = "Track Character"; +const char AudioReactive::_palName4[] PROGMEM = "Spectral Balance"; From 81c5eb6bf5e82af04f5c145ba62acba655bd231f Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:29:58 +0200 Subject: [PATCH 4/6] fix minor mis-formatting --- audio_reactive.h | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 2fcf2347..c31a14e6 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -78,7 +78,7 @@ extern "C" esp_err_t adc1_lock_release(void) { #include -#endif +#endif // ARDUINO_ARCH_ESP32 #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) #include @@ -267,7 +267,8 @@ static float sampleAvg = 0.0f; // Smoothed Average s static float sampleAgc = 0.0f; // Smoothed AGC sample static uint8_t soundAgc = SR_AGC; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) - enable AGC if default "squelch" was provided -#endif +#endif // ARDUINO_ARCH_ESP32 + 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 @@ -317,8 +318,8 @@ static void detectSamplePeak(void); // peak detection function (needs scaled FF 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 +#ifdef ARDUINO_ARCH_ESP32 // use audio source class (ESP32 specific) #include "audio_source.h" constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) @@ -344,7 +345,6 @@ static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; static uint8_t pinkIndex = SR_FREQ_PROF; // 0: default; 1: line-in; 2: IMNP441 #endif - // // AGC presets // Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" @@ -1277,7 +1277,7 @@ static void detectSamplePeak(void) { } } -#endif +#endif // ARDUINO_ARCH_ESP32 static void autoResetPeak(void) { uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC @@ -1346,7 +1346,8 @@ class AudioReactive : public Usermod { #else int8_t mclkPin = MCLK_PIN; #endif -#endif +#endif // ARDUINO_ARCH_ESP32 + // new "V2" audiosync struct - 44 Bytes struct __attribute__((packed)) audioSyncPacket { // WLEDMM "packed" ensures that there are no additional gaps char header[6]; // 06 Bytes offset 0 - "00002" for protocol version 2 ( includes \0 for c-style string termination) @@ -1405,7 +1406,6 @@ class AudioReactive : public Usermod { // 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() 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 @@ -1413,7 +1413,7 @@ class AudioReactive : public Usermod { 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 +#endif // ARDUINO_ARCH_ESP32 // variables used in effects int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc @@ -1489,7 +1489,7 @@ class AudioReactive : public Usermod { #endif PLOT_PRINTLN(); PLOT_FLUSH(); -#endif +#endif // MIC_LOGGER #ifdef FFT_SAMPLING_LOG #if 0 @@ -1536,11 +1536,10 @@ class AudioReactive : public Usermod { 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 +#endif // FFT_SAMPLING_LOG } // logAudio() #ifdef ARDUINO_ARCH_ESP32 - ////////////////////// // Audio Processing // ////////////////////// @@ -1791,8 +1790,7 @@ class AudioReactive : public Usermod { scaledvalue = (scaledvalue - logMinSample) / (logMaxSample - logMinSample); // 0...1 return fminf(fmaxf(256.0f * scaledvalue, 0.0f), 255.0f); // scaled value } -#endif - +#endif // ARDUINO_ARCH_ESP32 /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) @@ -1932,7 +1930,7 @@ class AudioReactive : public Usermod { frameCounter++; } // transmitAudioData() -#endif +#endif // ARDUINO_ARCH_ESP32 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; } @@ -2157,7 +2155,6 @@ class AudioReactive : public Usermod { } #ifdef ARDUINO_ARCH_ESP32 - // Reset I2S peripheral for good measure - not needed in esp-idf v4.4.x and later. #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed @@ -2369,7 +2366,7 @@ class AudioReactive : public Usermod { USER_PRINTLN(); USER_FLUSH(); } - #endif + #endif // SR_DEBUG #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM #endif @@ -2621,7 +2618,7 @@ class AudioReactive : public Usermod { } 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 +#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 From 042a69a8bfc5b33894e9137cb8bfe85d1c97dc89 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:05:49 +0200 Subject: [PATCH 5/6] full re-format of audio_source.h clang-format --- audio_source.h | 2118 +++++++++++++++++++++++------------------------- 1 file changed, 1021 insertions(+), 1097 deletions(-) diff --git a/audio_source.h b/audio_source.h index a4ab36cb..dfe1b428 100644 --- a/audio_source.h +++ b/audio_source.h @@ -9,43 +9,42 @@ @license Licensed under the EUPL-1.2 or later */ +#ifdef ARDUINO_ARCH_ESP32 + + #include + #include "wled.h" + #include + #if (ESP_IDF_VERSION_MAJOR < 5) + #include // needed for SPH0465 timing workaround (classic ESP32) + #endif + #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) + #include + #include + #include + #endif + // type of i2s_config_t.SampleRate was changed from "int" to "unsigned" in IDF 4.4.x + // also matches i2s_std_clk_config_t.sample_rate_hz of the IDF V5 driver + #define SRate_t uint32_t + #else + #define SRate_t int + #endif + #if (ESP_IDF_VERSION_MAJOR < 4) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 7)) // backwards compatibility patch + #define ADC_ATTEN_DB_12 ADC_ATTEN_DB_11 + #endif -#ifdef ARDUINO_ARCH_ESP32 -#include -#include "wled.h" -#include -#if (ESP_IDF_VERSION_MAJOR < 5) -#include // needed for SPH0465 timing workaround (classic ESP32) -#endif -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) -#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) -#include -#include -#include -#endif -// type of i2s_config_t.SampleRate was changed from "int" to "unsigned" in IDF 4.4.x -// also matches i2s_std_clk_config_t.sample_rate_hz of the IDF V5 driver -#define SRate_t uint32_t -#else -#define SRate_t int -#endif - -#if (ESP_IDF_VERSION_MAJOR < 4) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,4,7)) // backwards compatibility patch - #define ADC_ATTEN_DB_12 ADC_ATTEN_DB_11 -#endif - -#ifndef I2S_PIN_NO_CHANGE -#define I2S_PIN_NO_CHANGE (-1) // common "pin unused" marker (not defined in IDF V5 driver so define it here) -#endif - -// IDF V5 renamed the PDM RX slot config macro (raw PDM format support) - map back to the classic name -#if (ESP_IDF_VERSION_MAJOR >= 5) && !defined(I2S_PDM_RX_SLOT_DEFAULT_CONFIG) && defined(I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG) -#define I2S_PDM_RX_SLOT_DEFAULT_CONFIG I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG -#endif - -constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not change! I2S_NUM_1 possible but this has - // strong limitations -> no MCLK routing, no ADC support, no PDM support + #ifndef I2S_PIN_NO_CHANGE + #define I2S_PIN_NO_CHANGE (-1) // common "pin unused" marker (not defined in IDF V5 driver so define it here) + #endif + + // IDF V5 renamed the PDM RX slot config macro (raw PDM format support) - map back to the classic name + #if (ESP_IDF_VERSION_MAJOR >= 5) && !defined(I2S_PDM_RX_SLOT_DEFAULT_CONFIG) && defined(I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG) + #define I2S_PDM_RX_SLOT_DEFAULT_CONFIG I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG + #endif + +constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not change! I2S_NUM_1 possible but this has + // strong limitations -> no MCLK routing, no ADC support, no PDM support //#include //#include @@ -54,16 +53,16 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c // 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_ESP32H2) || defined(ESP8266) || defined(ESP8265) || !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) - // 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 MCUs without I2S." - #else - #warning "This audio reactive usermod does not support your MCU." + #if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) || !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) + // 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 MCUs without I2S." + #else + #warning "This audio reactive usermod does not support your MCU." + #endif #endif -#endif // PinManager API Compatibility // By default, this usermod uses WLED V16+ PinManager API (PinManager:: static class methods) @@ -74,15 +73,15 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c // #define WLED_USE_PINMANAGER_V14 // Uncomment for V14 compatibility mode /* ToDo: remove. ES7243 is controlled via compiler defines - Until this configuration is moved to the webinterface -*/ + * 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) +// #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; +// 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 @@ -94,420 +93,405 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c // #define I2S_USE_16BIT_SAMPLES //#endif -#ifdef I2S_USE_16BIT_SAMPLES -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)) // see https://github.com/MoonModules/WLED-MM/issues/333#issuecomment-3910779668 -#error "16bit sampling currently does not work with newer framework versions. Please build without I2S_USE_16BIT_SAMPLES" -#endif -#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 + #ifdef I2S_USE_16BIT_SAMPLES + #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)) // see https://github.com/MoonModules/WLED-MM/issues/333#issuecomment-3910779668 + #error "16bit sampling currently does not work with newer framework versions. Please build without I2S_USE_16BIT_SAMPLES" + #endif + #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(5, 0, 0)) // should be fixed in IDF 4.4.5, however arduino-esp32 2.0.14 - 2.0.17 did an "I2S rollback" to 4.4.4 + // 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 -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(5, 0, 0)) // should be fixed in IDF 4.4.5, however arduino-esp32 2.0.14 - 2.0.17 did an "I2S rollback" to 4.4.4 -// 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 - - -// max number of samples for a single i2s_read --> size of global buffer. -#define I2S_SAMPLES_MAX 512 // same as samplesFFT + // max number of samples for a single i2s_read --> size of global buffer. + #define I2S_SAMPLES_MAX 512 // same as samplesFFT /* 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 -*/ + * 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, bool i2sMaster) : - _sampleRate(sampleRate), - _blockSize(blockSize), - _initialized(false), - _i2sMaster(i2sMaster), - _sampleScale(sampleScale) - {}; - - SRate_t _sampleRate; // Microphone sampling rate - int _blockSize; // I2S block size - bool _initialized; // Gets set to true if initialization is successful - bool _i2sMaster; // when false, ESP32 will be in I2S SLAVE mode (for devices that only operate in MASTER mode). Only works in newer IDF >= 4.4.x - float _sampleScale; // pre-scaling factor for I2S samples - I2S_datatype newSampleBuffer[I2S_SAMPLES_MAX+4] = { 0 }; // global buffer for i2s_read + 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, bool i2sMaster) : _sampleRate(sampleRate), _blockSize(blockSize), _initialized(false), _i2sMaster(i2sMaster), _sampleScale(sampleScale) {}; + + SRate_t _sampleRate; // Microphone sampling rate + int _blockSize; // I2S block size + bool _initialized; // Gets set to true if initialization is successful + bool _i2sMaster; // when false, ESP32 will be in I2S SLAVE mode (for devices that only operate in MASTER mode). Only works in newer IDF >= 4.4.x + float _sampleScale; // pre-scaling factor for I2S samples + I2S_datatype newSampleBuffer[I2S_SAMPLES_MAX + 4] = {0}; // global buffer for i2s_read }; -// Helper functions for PinManager API compatibility -#ifdef WLED_USE_PINMANAGER_V14 - // V14 API compatibility - uses pinManager instance methods (lowercase) - inline bool ar_allocatePin(int8_t pin, bool output, PinOwner owner) { - return pinManager.allocatePin(pin, output, owner); - } - inline void ar_deallocatePin(int8_t pin, PinOwner owner) { - pinManager.deallocatePin(pin, owner); - } - inline bool ar_joinWire(int8_t sda, int8_t scl) { - return pinManager.joinWire(sda, scl); - } -#else - // V16+ API (default) - uses PinManager static class methods (uppercase) - inline bool ar_allocatePin(int8_t pin, bool output, PinOwner owner) { - return PinManager::allocatePin(pin, output, owner); - } - inline void ar_deallocatePin(int8_t pin, PinOwner owner) { - PinManager::deallocatePin(pin, owner); - } - inline bool ar_joinWire(int8_t sda, int8_t scl) { - USER_PRINTLN("AR: WARNING PinManager::joinWire() unsupported - returning false."); - return false; - // return PinManager::joinWire(sda, scl); // TODO: implement PinManager::joinWire() - } -#endif + // Helper functions for PinManager API compatibility + #ifdef WLED_USE_PINMANAGER_V14 + // V14 API compatibility - uses pinManager instance methods (lowercase) + inline bool ar_allocatePin(int8_t pin, bool output, PinOwner owner) { + return pinManager.allocatePin(pin, output, owner); + } + inline void ar_deallocatePin(int8_t pin, PinOwner owner) { + pinManager.deallocatePin(pin, owner); + } + inline bool ar_joinWire(int8_t sda, int8_t scl) { + return pinManager.joinWire(sda, scl); + } + #else + // V16+ API (default) - uses PinManager static class methods (uppercase) + inline bool ar_allocatePin(int8_t pin, bool output, PinOwner owner) { + return PinManager::allocatePin(pin, output, owner); + } + inline void ar_deallocatePin(int8_t pin, PinOwner owner) { + PinManager::deallocatePin(pin, owner); + } + inline bool ar_joinWire(int8_t sda, int8_t scl) { + DEBUG_PRINTLN("AR: WARNING PinManager::joinWire() unsupported - returning false."); + return false; + // return PinManager::joinWire(sda, scl); // TODO: implement PinManager::joinWire() + } + #endif /* Basic I2S microphone source - All functions are marked virtual, so derived classes can replace them - WARNING: i2sMaster = false is experimental, and most likely will not work -*/ + * All functions are marked virtual, so derived classes can replace them + * WARNING: i2sMaster = false is experimental, and most likely will not work + */ class I2SSource : public AudioSource { - public: - I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - AudioSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config = { - .mode = i2sMaster ? i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX) : i2s_mode_t(I2S_MODE_SLAVE | I2S_MODE_RX), - .sample_rate = _sampleRate, + public: + I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : AudioSource(sampleRate, blockSize, sampleScale, i2sMaster) { + _config = { + .mode = i2sMaster ? i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX) : i2s_mode_t(I2S_MODE_SLAVE | I2S_MODE_RX), + .sample_rate = _sampleRate, .bits_per_sample = I2S_SAMPLE_RESOLUTION, // slave mode: may help to set this to 96000, as the other side (master) controls sample rates - .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, -#ifdef WLEDMM_FASTPATH - #ifdef WLED_ENABLE_HUB75MATRIX - .intr_alloc_flags = ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt - .dma_buf_count = 18, // 100ms buffer (128 * dma_buf_count / sampleRate) - #else - #if CONFIG_IDF_TARGET_ESP32 && !defined(BOARD_HAS_PSRAM) // still need to test on boards with PSRAM - .intr_alloc_flags = ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3, // IRAM flag reduces missed samples - #else - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3, // seems to reduce noise - #endif - .dma_buf_count = 24, // 140ms buffer (128 * dma_buf_count / sampleRate) - #endif -#else - #ifdef WLED_ENABLE_HUB75MATRIX - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt - #else - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, - #endif - .dma_buf_count = 8, -#endif - .dma_buf_len = _blockSize, - .use_apll = 0, - //.fixed_mclk = 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("I2SSource:: initialize()."); - if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { - if (!ar_allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || - !ar_allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 - ERRORSR_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 (!ar_allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { - ERRORSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); - return; - } - } else { + .channel_format = I2S_MIC_CHANNEL, #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 + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), + //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + #ifdef WLEDMM_FASTPATH + #ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt + .dma_buf_count = 18, // 100ms buffer (128 * dma_buf_count / sampleRate) + #else + #if CONFIG_IDF_TARGET_ESP32 && !defined(BOARD_HAS_PSRAM) // still need to test on boards with PSRAM + .intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // IRAM flag reduces missed samples + #else + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // seems to reduce noise + #endif + .dma_buf_count = 24, // 140ms buffer (128 * dma_buf_count / sampleRate) + #endif + #else + #ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt + #else + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, + #endif + .dma_buf_count = 8, #endif + .dma_buf_len = _blockSize, + .use_apll = 0, + //.fixed_mclk = 0, + .bits_per_chan = I2S_data_size, + #else // ESP-IDF v3.x + .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 - #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) - // 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 - //_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; // not needed - #endif - } + }; + } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - if ((_i2sMaster == false) && (_config.mode & I2S_MODE_SLAVE)) { // I2S slave mode (experimental). - // Seems we need to drive clocks in slave mode - _config.use_apll = true; - _config.fixed_mclk = 512 * int(_config.sample_rate); + 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("I2SSource:: initialize()."); + if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { + if (!ar_allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || !ar_allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); + return; } + } - 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(CONFIG_IDF_TARGET_ESP32) - if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 - #endif - #if defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX) - _config.use_apll = false; // APLL is needed for Ethernet, and possibly for HUB75, too - #endif -#endif - - if (_i2sMaster == false) { - DEBUG_PRINTLN(F("AR: Warning - i2S SLAVE mode is experimental!")); - if (_config.mode & I2S_MODE_PDM) { - // APLL does not work in DAC or PDM "Slave Mode": https://github.com/espressif/esp-idf/issues/1244, https://github.com/espressif/esp-idf/issues/2634 - _config.use_apll = false; - _config.fixed_mclk = 0; - } - if ((_config.mode & I2S_MODE_MASTER) != 0) { - DEBUG_PRINTLN("AR: (oops) I2S SLAVE mode requested but not configured!"); - } + // i2ssckPin needs special treatment, since it might be unused on PDM mics + if (i2sckPin != I2S_PIN_NO_CHANGE) { + if (!ar_allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { + ERRORSR_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(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) + // 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 + //_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; // not needed + #endif + } - // Reserve the master clock pin if provided - _mclkPin = mclkPin; - if (mclkPin != I2S_PIN_NO_CHANGE) { - if(!ar_allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { - ERRORSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); - return; - } else - _routeMclk(mclkPin); + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + if ((_i2sMaster == false) && (_config.mode & I2S_MODE_SLAVE)) { // I2S slave mode (experimental). + // Seems we need to drive clocks in slave mode + _config.use_apll = true; + _config.fixed_mclk = 512 * int(_config.sample_rate); + } + + 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(CONFIG_IDF_TARGET_ESP32) + if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 + #endif + #if defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL is needed for Ethernet, and possibly for HUB75, too + #endif + #endif + + if (_i2sMaster == false) { + DEBUG_PRINTLN(F("AR: Warning - i2S SLAVE mode is experimental!")); + if (_config.mode & I2S_MODE_PDM) { + // APLL does not work in DAC or PDM "Slave Mode": https://github.com/espressif/esp-idf/issues/1244, https://github.com/espressif/esp-idf/issues/2634 + _config.use_apll = false; + _config.fixed_mclk = 0; } + if ((_config.mode & I2S_MODE_MASTER) != 0) { DEBUG_PRINTLN("AR: (oops) I2S SLAVE mode requested but not configured!"); } + } - _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, + // Reserve the master clock pin if provided + _mclkPin = mclkPin; + if (mclkPin != I2S_PIN_NO_CHANGE) { + if (!ar_allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { + ERRORSR_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 - }; + .data_in_num = i2ssdPin}; - //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); + //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); - esp_err_t err = i2s_driver_install(AR_I2S_PORT, &_config, 0, nullptr); - if (err != ESP_OK) { - ERRORSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); - return; - } + esp_err_t err = i2s_driver_install(AR_I2S_PORT, &_config, 0, nullptr); + if (err != ESP_OK) { + ERRORSR_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_MASTER) { - 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.")); - } + 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_MASTER) { + 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 SLAVE mode.")); + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); } + } else { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in SLAVE mode.")); + } - err = i2s_set_pin(AR_I2S_PORT, &_pinConfig); - if (err != ESP_OK) { - ERRORSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); - i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver - return; - } + err = i2s_set_pin(AR_I2S_PORT, &_pinConfig); + if (err != ESP_OK) { + ERRORSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); + i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver + return; + } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - // ToDO: seems that this sometimes reverts PDM timing configuration - err = i2s_set_clk(AR_I2S_PORT, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. - if (err != ESP_OK) { - ERRORSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); - i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver - return; - } -#endif - _initialized = true; + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + // ToDO: seems that this sometimes reverts PDM timing configuration + err = i2s_set_clk(AR_I2S_PORT, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. + if (err != ESP_OK) { + ERRORSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); + i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver + return; } + #endif + _initialized = true; + } + + virtual void deinitialize() { + _initialized = false; + esp_err_t err = i2s_driver_uninstall(AR_I2S_PORT); + if (err != ESP_OK) { + DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); + return; + } + if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); + if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); + // Release the master clock pin + if (_mclkPin != I2S_PIN_NO_CHANGE) ar_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 */ - virtual void deinitialize() { - _initialized = false; - esp_err_t err = i2s_driver_uninstall(AR_I2S_PORT); + memset(buffer, 0, sizeof(float) * num_samples); // clear output buffer + I2S_datatype* newSamples = newSampleBuffer; // use global input buffer + if (num_samples > I2S_SAMPLES_MAX) num_samples = I2S_SAMPLES_MAX; // protect the buffer from overflow + + err = i2s_read(AR_I2S_PORT, (void*)newSamples, num_samples * sizeof(I2S_datatype), &bytes_read, portMAX_DELAY); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); + DEBUGSR_PRINTF("Failed to get samples: %d\n", err); return; } - if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); - if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); - if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) ar_deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); - // Release the master clock pin - if (_mclkPin != I2S_PIN_NO_CHANGE) ar_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 */ - memset(buffer, 0, sizeof(float) * num_samples); // clear output buffer - I2S_datatype *newSamples = newSampleBuffer; // use global input buffer - if (num_samples > I2S_SAMPLES_MAX) num_samples = I2S_SAMPLES_MAX; // protect the buffer from overflow + // For correct operation, we need to read exactly sizeof(samples) bytes from i2s + if (bytes_read != (num_samples * sizeof(I2S_datatype))) { + DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", num_samples * sizeof(I2S_datatype), bytes_read); + return; + } - err = i2s_read(AR_I2S_PORT, (void *)newSamples, num_samples * sizeof(I2S_datatype), &bytes_read, portMAX_DELAY); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to get samples: %d\n", err); - return; - } + // Store samples in sample buffer and update DC offset + for (int i = 0; i < num_samples; i++) { - // For correct operation, we need to read exactly sizeof(samples) bytes from i2s - if (bytes_read != (num_samples * sizeof(I2S_datatype))) { - DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", num_samples * sizeof(I2S_datatype), bytes_read); - return; - } + newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples) - // 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 - } + 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(__riscv) && !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 + protected: + void _routeMclk(int8_t mclkPin) { + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(__riscv) && !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 - */ + * 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); + 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 { + } else { PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2); WRITE_PERI_REG(PIN_CTRL, 0xFF00); } + #endif #endif -#endif - } + } - i2s_config_t _config; - i2s_pin_config_t _pinConfig; - int8_t _mclkPin; + i2s_config_t _config; + i2s_pin_config_t _pinConfig; + int8_t _mclkPin; }; /* ES7243 Microphone @@ -515,66 +499,57 @@ class I2SSource : public AudioSource { I2C before I2S data can be received */ class ES7243 : public I2SSource { - private: - // I2C initialization functions for ES7243 - void _es7243I2cBegin() { - Wire.setClock(100000); - } + private: + // I2C initialization functions for ES7243 + void _es7243I2cBegin() { Wire.setClock(100000); } - 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 _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() { - _es7243I2cBegin(); - _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); - } + void _es7243InitAdc() { + _es7243I2cBegin(); + _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, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; - }; + public: + ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; }; - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN("ES7243:: initialize();"); + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES7243:: initialize();"); - // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too - // ERRORSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - // return; - // } - if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" - ERRORSR_PRINTF("\nAR: invalid ES7243 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#ifdef _MoonModules_WLED_ - if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins - ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#endif - // First route mclk, then configure ADC over I2C, then configure I2S - _es7243InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid ES7243 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } - - void deinitialize() { - I2SSource::deinitialize(); + #ifdef _MoonModules_WLED_ + if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } + #endif + // 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 @@ -582,744 +557,693 @@ class ES7243 : public I2SSource { I2C before I2S data can be received. */ class ES8388Source : public I2SSource { - private: - // I2C initialization functions for ES8388 - void _es8388I2cBegin() { - Wire.setClock(100000); - } + private: + // I2C initialization functions for ES8388 + void _es8388I2cBegin() { Wire.setClock(100000); } - void _es8388I2cWrite(uint8_t reg, uint8_t val) { - #ifndef ES8388_ADDR - #define ES8388_ADDR 0x10 // default address - #endif - Wire.beginTransmission(ES8388_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: 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. - // - _es8388I2cBegin(); - _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 + void _es8388I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES8388_ADDR + #define ES8388_ADDR 0x10 // default address #endif + Wire.beginTransmission(ES8388_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: ES8388 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8388_ADDR, reg, val); } + } - } - - public: - ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; - }; - - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN("ES8388Source:: initialize();"); - - // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too - // ERRORSR_PRINTF("\nAR: invalid I2S ES8388 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - // return; - // } - // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. - // Workaround: Set I2C pins here, which will also set them globally. - // Bug also exists in ES7243. - if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" - ERRORSR_PRINTF("\nAR: invalid ES8388 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#ifdef _MoonModules_WLED_ - if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins - ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#endif + 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. + // + _es8388I2cBegin(); + _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 // es8388 aux-in + _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 // es8388 + } - // First route mclk, then configure ADC over I2C, then configure I2S - _es8388InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + public: + ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES8388Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S ES8388 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid ES8388 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } - - void deinitialize() { - I2SSource::deinitialize(); + #ifdef _MoonModules_WLED_ + if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } + #endif + // First route mclk, then configure ADC over I2C, then configure I2S + _es8388InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { I2SSource::deinitialize(); } }; /* ES8311 Sound Module This is an I2S sound processing unit that requires initialization over I2C before I2S data can be received. */ - class ES8311Source : public I2SSource { - private: - bool ES7210_present = false; // tracks whether ES7210 co-processor was detected - - bool es7210_present() { - Wire.beginTransmission(0x40); - return (Wire.endTransmission() == 0); - } +class ES8311Source : public I2SSource { + private: + bool ES7210_present = false; // tracks whether ES7210 co-processor was detected - // I2C initialization functions for es8311 - void _es8311I2cBegin() { - Wire.setClock(100000); - } + bool es7210_present() { + Wire.beginTransmission(0x40); + return (Wire.endTransmission() == 0); + } - void _es8311I2cWrite(uint8_t reg, uint8_t val) { - #ifndef ES8311_ADDR - #define ES8311_ADDR 0x18 // default address is... foggy - #endif - if (ES7210_present) { - Wire.beginTransmission(0x40); - } else { - Wire.beginTransmission(ES8311_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: ES8311 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8311_ADDR, reg, val); - } - } + // I2C initialization functions for es8311 + void _es8311I2cBegin() { Wire.setClock(100000); } - void es7210_init_22k_32bit() { - _es8311I2cBegin(); - - // --- 1. RESET --- - _es8311I2cWrite(0x00, 0xFF); - vTaskDelay(pdMS_TO_TICKS(10)); - _es8311I2cWrite(0x00, 0x32); - - // --- 2. SLAVE MODE (clocks from ESP32) --- - _es8311I2cWrite(0x08, 0x00); // Slave mode - // _es8311I2cWrite(0x06, 0x04); // DLL off (not needed in slave mode) - - // --- 3. I2S FORMAT --- - _es8311I2cWrite(0x09, 0x30); // Timing control - _es8311I2cWrite(0x0A, 0x30); // Timing control - _es8311I2cWrite(0x11, 0x80); // 32-bit I2S - _es8311I2cWrite(0x12, 0x00); // MIC1/2 on SDOUT1 - - // --- 4. HIGH PASS FILTER --- - _es8311I2cWrite(0x22, 0x0A); - _es8311I2cWrite(0x23, 0x2A); - - // --- 5. ANALOG POWER --- - _es8311I2cWrite(0x40, 0xC3); - _es8311I2cWrite(0x41, 0x70); // 0x70 standard bias (0x7F is max) - - // --- 6. GAIN (no ALC) --- - _es8311I2cWrite(0x43, 0x18); - _es8311I2cWrite(0x44, 0x18); - _es8311I2cWrite(0x16, 0x00); // ALC off - - // --- 7. MIC POWER --- - _es8311I2cWrite(0x47, 0x08); // MIC1 power - _es8311I2cWrite(0x48, 0x08); // MIC2 power - _es8311I2cWrite(0x49, 0x00); // MIC3 OFF - _es8311I2cWrite(0x4A, 0x00); // MIC4 OFF - _es8311I2cWrite(0x4B, 0x0F); // ADC1/2 power - _es8311I2cWrite(0x4C, 0x00); // ADC3/4 OFF - - // --- 8. START --- - _es8311I2cWrite(0x00, 0x71); - _es8311I2cWrite(0x00, 0x41); + void _es8311I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES8311_ADDR + #define ES8311_ADDR 0x18 // default address is... foggy + #endif + if (ES7210_present) { + Wire.beginTransmission(0x40); + } else { + Wire.beginTransmission(ES8311_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: ES8311 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8311_ADDR, reg, val); } + } - void _es8311InitAdc() { - // - // Currently only tested with the ESP32-P4 boards with the onboard mic. - // Datasheet with I2C commands: https://dl.xkwy2018.com/downloads/RK3588/01_Official%20Release/04_Product%20Line%20Branch_NVR/02_Key%20Device%20Specifications/ES8311%20DS.pdf - // If making changes, make sure to completely power off the board - sometimes settings are kept until the board is powered off! - // - _es8311I2cBegin(); - _es8311I2cWrite(0x00, 0b00011111); // RESET, default value was 0b00011111 new from ESPHome example - _es8311I2cWrite(0x00, 0b00000000); // RESET, added this from ESPHome example - _es8311I2cWrite(0x45, 0b00000000); // GP, default value - _es8311I2cWrite(0x01, 0b00111010); // CLOCK MANAGER (MCLK enable?) - - _es8311I2cWrite(0x02, 0b00000000); // 22050hz calculated - _es8311I2cWrite(0x05, 0b00000000); // 22050hz calculated - _es8311I2cWrite(0x03, 0b00010000); // 22050hz calculated - _es8311I2cWrite(0x04, 0b00010000); // 22050hz calculated - _es8311I2cWrite(0x07, 0b00000000); // 22050hz calculated - _es8311I2cWrite(0x08, 0b11111111); // 22050hz calculated - _es8311I2cWrite(0x06, 0b11100011); // 22050hz calculated - - _es8311I2cWrite(0x16, 0b00100100); // ADC synchronize filter counter with "standard" LRCK and ADC RAM clear when lrck/adc_mclk active - _es8311I2cWrite(0x0B, 0b00000000); // SYSTEM at default - _es8311I2cWrite(0x0C, 0b00100000); // SYSTEM power up things - _es8311I2cWrite(0x10, 0b00010011); // SYSTEM internal things - _es8311I2cWrite(0x0D, 0b00000001); // ESPHome: Power up analog circuitry - _es8311I2cWrite(0x11, 0b01111100); // *** SYSTEM undocumented bits, seems to be important - _es8311I2cWrite(0x00, 0b11000000); // *** RESET (again - seems important?) - _es8311I2cWrite(0x01, 0b00111010); // *** CLOCK MANAGER - _es8311I2cWrite(0x14, 0b00010000); // *** SYSTEM PGA gain - _es8311I2cWrite(0x0A, 0b00001000); // *** SDP OUT = I2S 32-bit - _es8311I2cWrite(0x0E, 0b00000010); // *** SYSTEM undocumented bits, seems to be important - _es8311I2cWrite(0x0F, 0b01000100); // SYSTEM enable LPPGA and LPDACVRP in low power mode. No idea. - _es8311I2cWrite(0x15, 0b00010000); // ADC soft ramp - _es8311I2cWrite(0x1B, 0b00000101); // ADC soft-mute enabled - _es8311I2cWrite(0x1C, 0b11100101); // ADC dynamic HPF enabled - _es8311I2cWrite(0x17, 0b10111111); // ADC volume = 0db (max gain) - _es8311I2cWrite(0x18, 0b11001000); // ADC ALC enabled and AutoMute enabled - _es8311I2cWrite(0x19, 0b11110000); // ADC ALC max (-6dB) and min (-30dB) - _es8311I2cWrite(0x00, 0b10000000); // *** RESET (This is very required! Thanks to ESPHome for the hint!) - } + void es7210_init_22k_32bit() { + _es8311I2cBegin(); + + // --- 1. RESET --- + _es8311I2cWrite(0x00, 0xFF); + vTaskDelay(pdMS_TO_TICKS(10)); + _es8311I2cWrite(0x00, 0x32); + + // --- 2. SLAVE MODE (clocks from ESP32) --- + _es8311I2cWrite(0x08, 0x00); // Slave mode + // _es8311I2cWrite(0x06, 0x04); // DLL off (not needed in slave mode) + + // --- 3. I2S FORMAT --- + _es8311I2cWrite(0x09, 0x30); // Timing control + _es8311I2cWrite(0x0A, 0x30); // Timing control + _es8311I2cWrite(0x11, 0x80); // 32-bit I2S + _es8311I2cWrite(0x12, 0x00); // MIC1/2 on SDOUT1 + + // --- 4. HIGH PASS FILTER --- + _es8311I2cWrite(0x22, 0x0A); + _es8311I2cWrite(0x23, 0x2A); + + // --- 5. ANALOG POWER --- + _es8311I2cWrite(0x40, 0xC3); + _es8311I2cWrite(0x41, 0x70); // 0x70 standard bias (0x7F is max) + + // --- 6. GAIN (no ALC) --- + _es8311I2cWrite(0x43, 0x18); + _es8311I2cWrite(0x44, 0x18); + _es8311I2cWrite(0x16, 0x00); // ALC off + + // --- 7. MIC POWER --- + _es8311I2cWrite(0x47, 0x08); // MIC1 power + _es8311I2cWrite(0x48, 0x08); // MIC2 power + _es8311I2cWrite(0x49, 0x00); // MIC3 OFF + _es8311I2cWrite(0x4A, 0x00); // MIC4 OFF + _es8311I2cWrite(0x4B, 0x0F); // ADC1/2 power + _es8311I2cWrite(0x4C, 0x00); // ADC3/4 OFF + + // --- 8. START --- + _es8311I2cWrite(0x00, 0x71); + _es8311I2cWrite(0x00, 0x41); + } - void es8311_disable() { - Wire.setClock(100000); + void _es8311InitAdc() { + // + // Currently only tested with the ESP32-P4 boards with the onboard mic. + // Datasheet with I2C commands: https://dl.xkwy2018.com/downloads/RK3588/01_Official%20Release/04_Product%20Line%20Branch_NVR/02_Key%20Device%20Specifications/ES8311%20DS.pdf + // If making changes, make sure to completely power off the board - sometimes settings are kept until the board is powered off! + // + _es8311I2cBegin(); + _es8311I2cWrite(0x00, 0b00011111); // RESET, default value was 0b00011111 new from ESPHome example + _es8311I2cWrite(0x00, 0b00000000); // RESET, added this from ESPHome example + _es8311I2cWrite(0x45, 0b00000000); // GP, default value + _es8311I2cWrite(0x01, 0b00111010); // CLOCK MANAGER (MCLK enable?) + + _es8311I2cWrite(0x02, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x05, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x03, 0b00010000); // 22050hz calculated + _es8311I2cWrite(0x04, 0b00010000); // 22050hz calculated + _es8311I2cWrite(0x07, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x08, 0b11111111); // 22050hz calculated + _es8311I2cWrite(0x06, 0b11100011); // 22050hz calculated + + _es8311I2cWrite(0x16, 0b00100100); // ADC synchronize filter counter with "standard" LRCK and ADC RAM clear when lrck/adc_mclk active + _es8311I2cWrite(0x0B, 0b00000000); // SYSTEM at default + _es8311I2cWrite(0x0C, 0b00100000); // SYSTEM power up things + _es8311I2cWrite(0x10, 0b00010011); // SYSTEM internal things + _es8311I2cWrite(0x0D, 0b00000001); // ESPHome: Power up analog circuitry + _es8311I2cWrite(0x11, 0b01111100); // *** SYSTEM undocumented bits, seems to be important + _es8311I2cWrite(0x00, 0b11000000); // *** RESET (again - seems important?) + _es8311I2cWrite(0x01, 0b00111010); // *** CLOCK MANAGER + _es8311I2cWrite(0x14, 0b00010000); // *** SYSTEM PGA gain + _es8311I2cWrite(0x0A, 0b00001000); // *** SDP OUT = I2S 32-bit + _es8311I2cWrite(0x0E, 0b00000010); // *** SYSTEM undocumented bits, seems to be important + _es8311I2cWrite(0x0F, 0b01000100); // SYSTEM enable LPPGA and LPDACVRP in low power mode. No idea. + _es8311I2cWrite(0x15, 0b00010000); // ADC soft ramp + _es8311I2cWrite(0x1B, 0b00000101); // ADC soft-mute enabled + _es8311I2cWrite(0x1C, 0b11100101); // ADC dynamic HPF enabled + _es8311I2cWrite(0x17, 0b10111111); // ADC volume = 0db (max gain) + _es8311I2cWrite(0x18, 0b11001000); // ADC ALC enabled and AutoMute enabled + _es8311I2cWrite(0x19, 0b11110000); // ADC ALC max (-6dB) and min (-30dB) + _es8311I2cWrite(0x00, 0b10000000); // *** RESET (This is very required! Thanks to ESPHome for the hint!) + } - Wire.beginTransmission(0x18); - Wire.write(0x00); - Wire.write(0x1F); // Hold in reset - Wire.endTransmission(); + void es8311_disable() { + Wire.setClock(100000); - Wire.beginTransmission(0x18); - Wire.write(0x0D); - Wire.write(0x00); // Power down analog - Wire.endTransmission(); + Wire.beginTransmission(0x18); + Wire.write(0x00); + Wire.write(0x1F); // Hold in reset + Wire.endTransmission(); - Wire.beginTransmission(0x18); - Wire.write(0x0C); - Wire.write(0x00); // Power down digital - Wire.endTransmission(); - } + Wire.beginTransmission(0x18); + Wire.write(0x0D); + Wire.write(0x00); // Power down analog + Wire.endTransmission(); - public: - ES8311Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; - }; + Wire.beginTransmission(0x18); + Wire.write(0x0C); + Wire.write(0x00); // Power down digital + Wire.endTransmission(); + } - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN("es8311Source:: initialize();"); - - // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too - // ERRORSR_PRINTF("\nAR: invalid I2S es8311 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - // return; - // } - // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. - // Workaround: Set I2C pins here, which will also set them globally. - // Bug also exists in ES7243. - if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" - ERRORSR_PRINTF("\nAR: invalid es8311 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#ifdef _MoonModules_WLED_ - if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins - ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#endif - - // First route mclk, then configure ADC over I2C, then configure I2S - if (es7210_present()) { - USER_PRINTLN("Overriding ES8311 because an ES7210 is present."); - es8311_disable(); - ES7210_present = true; - es7210_init_22k_32bit(); - } else { - _es8311InitAdc(); - } - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + public: + ES8311Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("es8311Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S es8311 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid es8311 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + #ifdef _MoonModules_WLED_ + if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } + #endif - void deinitialize() { - I2SSource::deinitialize(); + // First route mclk, then configure ADC over I2C, then configure I2S + if (es7210_present()) { + USER_PRINTLN("Overriding ES8311 because an ES7210 is present."); + es8311_disable(); + ES7210_present = true; + es7210_init_22k_32bit(); + } else { + _es8311InitAdc(); } + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + void deinitialize() { I2SSource::deinitialize(); } }; class WM8978Source : public I2SSource { - private: - // I2C initialization functions for WM8978 - void _wm8978I2cBegin() { - Wire.setClock(400000); - } + private: + // I2C initialization functions for WM8978 + void _wm8978I2cBegin() { Wire.setClock(400000); } - void _wm8978I2cWrite(uint8_t reg, uint16_t val) { - #ifndef WM8978_ADDR - #define WM8978_ADDR 0x1A - #endif - char buf[2]; - buf[0] = (reg << 1) | ((val >> 8) & 0X01); - buf[1] = val & 0XFF; - Wire.beginTransmission(WM8978_ADDR); - Wire.write((const uint8_t*)buf, 2); - uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK - if (i2cErr != 0) { - DEBUGSR_PRINTF("AR: WM8978 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, WM8978_ADDR, reg, val); - } - } - - void _wm8978InitAdc() { - // https://www.mouser.com/datasheet/2/76/WM8978_v4.5-1141768.pdf - // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. - // Registries are decimal, settings are 9-bit binary as that's how everything is listed in the docs - // ...which makes it easier to reference the docs. - // - _wm8978I2cBegin(); - - _wm8978I2cWrite( 0,0b000000000); // Reset all settings - _wm8978I2cWrite( 1,0b000111110); // Power Management 1 - power off most things, but enable mic bias and I/O tie-off to help mitigate mic leakage. - _wm8978I2cWrite( 2,0b110111111); // Power Management 2 - enable output and amp stages (amps may lift signal but it works better on the ADCs) - _wm8978I2cWrite( 3,0b000001100); // Power Management 3 - enable L&R output mixers - - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - _wm8978I2cWrite( 4,0b001010000); // Audio Interface - standard I2S, 24-bit - #else - _wm8978I2cWrite( 4,0b001001000); // Audio Interface - left-justified I2S, 24-bit - #endif - - _wm8978I2cWrite( 6,0b000000000); // Clock generation control - use external mclk - _wm8978I2cWrite( 7,0b000000100); // Sets sample rate to ~24kHz (only used for internal calculations, not I2S) - _wm8978I2cWrite(14,0b010001000); // 128x ADC oversampling - high pass filter disabled as it kills the bass response - _wm8978I2cWrite(43,0b000110000); // Mute signal paths we don't use - _wm8978I2cWrite(44,0b100000000); // Disconnect microphones - _wm8978I2cWrite(45,0b111000000); // Mute signal paths we don't use - _wm8978I2cWrite(46,0b111000000); // Mute signal paths we don't use - _wm8978I2cWrite(47,0b001000000); // 0dB gain on left line-in - _wm8978I2cWrite(48,0b001000000); // 0dB gain on right line-in - _wm8978I2cWrite(49,0b000000011); // Mixer thermal shutdown enable and unused IOs to 30kΩ - _wm8978I2cWrite(50,0b000010110); // Output mixer enable only left bypass at 0dB gain - _wm8978I2cWrite(51,0b000010110); // Output mixer enable only right bypass at 0dB gain - _wm8978I2cWrite(52,0b110111001); // Left line-out enabled at 0dB gain - _wm8978I2cWrite(53,0b110111001); // Right line-out enabled at 0db gain - _wm8978I2cWrite(54,0b111000000); // Mute left speaker output - _wm8978I2cWrite(55,0b111000000); // Mute right speaker output + void _wm8978I2cWrite(uint8_t reg, uint16_t val) { + #ifndef WM8978_ADDR + #define WM8978_ADDR 0x1A + #endif + char buf[2]; + buf[0] = (reg << 1) | ((val >> 8) & 0X01); + buf[1] = val & 0XFF; + Wire.beginTransmission(WM8978_ADDR); + Wire.write((const uint8_t*)buf, 2); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { DEBUGSR_PRINTF("AR: WM8978 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, WM8978_ADDR, reg, val); } + } - } + void _wm8978InitAdc() { + // https://www.mouser.com/datasheet/2/76/WM8978_v4.5-1141768.pdf + // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. + // Registries are decimal, settings are 9-bit binary as that's how everything is listed in the docs + // ...which makes it easier to reference the docs. + // + _wm8978I2cBegin(); + + _wm8978I2cWrite(0, 0b000000000); // Reset all settings + _wm8978I2cWrite(1, 0b000111110); // Power Management 1 - power off most things, but enable mic bias and I/O tie-off to help mitigate mic leakage. + _wm8978I2cWrite(2, 0b110111111); // Power Management 2 - enable output and amp stages (amps may lift signal but it works better on the ADCs) + _wm8978I2cWrite(3, 0b000001100); // Power Management 3 - enable L&R output mixers + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + _wm8978I2cWrite(4, 0b001010000); // Audio Interface - standard I2S, 24-bit + #else + _wm8978I2cWrite(4, 0b001001000); // Audio Interface - left-justified I2S, 24-bit + #endif - public: - WM8978Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; - }; + _wm8978I2cWrite(6, 0b000000000); // Clock generation control - use external mclk + _wm8978I2cWrite(7, 0b000000100); // Sets sample rate to ~24kHz (only used for internal calculations, not I2S) + _wm8978I2cWrite(14, 0b010001000); // 128x ADC oversampling - high pass filter disabled as it kills the bass response + _wm8978I2cWrite(43, 0b000110000); // Mute signal paths we don't use + _wm8978I2cWrite(44, 0b100000000); // Disconnect microphones + _wm8978I2cWrite(45, 0b111000000); // Mute signal paths we don't use + _wm8978I2cWrite(46, 0b111000000); // Mute signal paths we don't use + _wm8978I2cWrite(47, 0b001000000); // 0dB gain on left line-in + _wm8978I2cWrite(48, 0b001000000); // 0dB gain on right line-in + _wm8978I2cWrite(49, 0b000000011); // Mixer thermal shutdown enable and unused IOs to 30kΩ + _wm8978I2cWrite(50, 0b000010110); // Output mixer enable only left bypass at 0dB gain + _wm8978I2cWrite(51, 0b000010110); // Output mixer enable only right bypass at 0dB gain + _wm8978I2cWrite(52, 0b110111001); // Left line-out enabled at 0dB gain + _wm8978I2cWrite(53, 0b110111001); // Right line-out enabled at 0db gain + _wm8978I2cWrite(54, 0b111000000); // Mute left speaker output + _wm8978I2cWrite(55, 0b111000000); // Mute right speaker output + } - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN("WM8978Source:: initialize();"); - - // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too - // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - // return; - // } - // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. - // Workaround: Set I2C pins here, which will also set them globally. - // Bug also exists in ES7243. - if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" - ERRORSR_PRINTF("\nAR: invalid WM8978 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#ifdef _MoonModules_WLED_ - if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins - ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#endif - // First route mclk, then configure ADC over I2C, then configure I2S - _wm8978InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + public: + WM8978Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("WM8978Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid WM8978 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } - - void deinitialize() { - I2SSource::deinitialize(); + #ifdef _MoonModules_WLED_ + if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } + #endif + // First route mclk, then configure ADC over I2C, then configure I2S + _wm8978InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + void deinitialize() { I2SSource::deinitialize(); } }; class AC101Source : public I2SSource { - private: - // I2C initialization functions for WM8978 - void _ac101I2cBegin() { - Wire.setClock(400000); - } + private: + // I2C initialization functions for WM8978 + void _ac101I2cBegin() { Wire.setClock(400000); } - void _ac101I2cWrite(uint8_t reg_addr, uint16_t val) { - #ifndef AC101_ADDR - #define AC101_ADDR 0x1A - #endif - char send_buff[3]; - send_buff[0] = reg_addr; - send_buff[1] = uint8_t((val >> 8) & 0xff); - send_buff[2] = uint8_t(val & 0xff); - Wire.beginTransmission(AC101_ADDR); - Wire.write((const uint8_t*)send_buff, 3); - uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK - if (i2cErr != 0) { - DEBUGSR_PRINTF("AR: AC101 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, AC101_ADDR, reg_addr, val); - } - } - - void _ac101InitAdc() { - // https://files.seeedstudio.com/wiki/ReSpeaker_6-Mics_Circular_Array_kit_for_Raspberry_Pi/reg/AC101_User_Manual_v1.1.pdf - // This supports mostly the older AI Thinkier AudioKit A1S that has an AC101 chip - // Newer versions use the ES3833 chip - which we also support. - - _ac101I2cBegin(); - - #define CHIP_AUDIO_RS 0x00 - #define SYSCLK_CTRL 0x03 - #define MOD_CLK_ENA 0x04 - #define MOD_RST_CTRL 0x05 - #define I2S_SR_CTRL 0x06 - #define I2S1LCK_CTRL 0x10 - #define I2S1_SDOUT_CTRL 0x11 - #define I2S1_MXR_SRC 0x13 - #define ADC_DIG_CTRL 0x40 - #define ADC_APC_CTRL 0x50 - #define ADC_SRC 0x51 - #define ADC_SRCBST_CTRL 0x52 - #define OMIXER_DACA_CTRL 0x53 - #define OMIXER_SR 0x54 - #define HPOUT_CTRL 0x56 - - _ac101I2cWrite(CHIP_AUDIO_RS, 0x123); // I think anything written here is a reset as 0x123 is kinda suss. - - delay(100); - - _ac101I2cWrite(SYSCLK_CTRL, 0b0000100000001000); // System Clock is I2S MCLK - _ac101I2cWrite(MOD_CLK_ENA, 0b1000000000001000); // I2S and ADC Clock Enable - _ac101I2cWrite(MOD_RST_CTRL, 0b1000000000001000); // I2S and ADC Clock Enable - _ac101I2cWrite(I2S_SR_CTRL, 0b0100000000000000); // set to 22050hz just in case - _ac101I2cWrite(I2S1LCK_CTRL, 0b1000000000110000); // set I2S slave mode, 24-bit word size - _ac101I2cWrite(I2S1_SDOUT_CTRL, 0b1100000000000000); // I2S enable Left/Right channels - _ac101I2cWrite(I2S1_MXR_SRC, 0b0010001000000000); // I2S digital Mixer, ADC L/R data - _ac101I2cWrite(ADC_SRCBST_CTRL, 0b0000000000000100); // mute all boosts. last 3 bits are reserved/default - _ac101I2cWrite(OMIXER_SR, 0b0000010000001000); // Line L/R to output mixer - _ac101I2cWrite(ADC_SRC, 0b0000010000001000); // Line L/R to ADC - _ac101I2cWrite(ADC_DIG_CTRL, 0b1000000000000000); // Enable ADC - _ac101I2cWrite(ADC_APC_CTRL, 0b1011100100000000); // ADC L/R enabled, 0dB gain - _ac101I2cWrite(OMIXER_DACA_CTRL, 0b0011111110000000); // L/R Analog Output Mixer enabled, headphone DC offset default - _ac101I2cWrite(HPOUT_CTRL, 0b1111101111110001); // Headphone out from Analog Mixer stage, no reduction in volume - - } + void _ac101I2cWrite(uint8_t reg_addr, uint16_t val) { + #ifndef AC101_ADDR + #define AC101_ADDR 0x1A + #endif + char send_buff[3]; + send_buff[0] = reg_addr; + send_buff[1] = uint8_t((val >> 8) & 0xff); + send_buff[2] = uint8_t(val & 0xff); + Wire.beginTransmission(AC101_ADDR); + Wire.write((const uint8_t*)send_buff, 3); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { DEBUGSR_PRINTF("AR: AC101 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, AC101_ADDR, reg_addr, val); } + } - public: - AC101Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { - _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; - }; + void _ac101InitAdc() { + // https://files.seeedstudio.com/wiki/ReSpeaker_6-Mics_Circular_Array_kit_for_Raspberry_Pi/reg/AC101_User_Manual_v1.1.pdf + // This supports mostly the older AI Thinkier AudioKit A1S that has an AC101 chip + // Newer versions use the ES3833 chip - which we also support. + + _ac101I2cBegin(); + + #define CHIP_AUDIO_RS 0x00 + #define SYSCLK_CTRL 0x03 + #define MOD_CLK_ENA 0x04 + #define MOD_RST_CTRL 0x05 + #define I2S_SR_CTRL 0x06 + #define I2S1LCK_CTRL 0x10 + #define I2S1_SDOUT_CTRL 0x11 + #define I2S1_MXR_SRC 0x13 + #define ADC_DIG_CTRL 0x40 + #define ADC_APC_CTRL 0x50 + #define ADC_SRC 0x51 + #define ADC_SRCBST_CTRL 0x52 + #define OMIXER_DACA_CTRL 0x53 + #define OMIXER_SR 0x54 + #define HPOUT_CTRL 0x56 + + _ac101I2cWrite(CHIP_AUDIO_RS, 0x123); // I think anything written here is a reset as 0x123 is kinda suss. + + delay(100); + + _ac101I2cWrite(SYSCLK_CTRL, 0b0000100000001000); // System Clock is I2S MCLK + _ac101I2cWrite(MOD_CLK_ENA, 0b1000000000001000); // I2S and ADC Clock Enable + _ac101I2cWrite(MOD_RST_CTRL, 0b1000000000001000); // I2S and ADC Clock Enable + _ac101I2cWrite(I2S_SR_CTRL, 0b0100000000000000); // set to 22050hz just in case + _ac101I2cWrite(I2S1LCK_CTRL, 0b1000000000110000); // set I2S slave mode, 24-bit word size + _ac101I2cWrite(I2S1_SDOUT_CTRL, 0b1100000000000000); // I2S enable Left/Right channels + _ac101I2cWrite(I2S1_MXR_SRC, 0b0010001000000000); // I2S digital Mixer, ADC L/R data + _ac101I2cWrite(ADC_SRCBST_CTRL, 0b0000000000000100); // mute all boosts. last 3 bits are reserved/default + _ac101I2cWrite(OMIXER_SR, 0b0000010000001000); // Line L/R to output mixer + _ac101I2cWrite(ADC_SRC, 0b0000010000001000); // Line L/R to ADC + _ac101I2cWrite(ADC_DIG_CTRL, 0b1000000000000000); // Enable ADC + _ac101I2cWrite(ADC_APC_CTRL, 0b1011100100000000); // ADC L/R enabled, 0dB gain + _ac101I2cWrite(OMIXER_DACA_CTRL, 0b0011111110000000); // L/R Analog Output Mixer enabled, headphone DC offset default + _ac101I2cWrite(HPOUT_CTRL, 0b1111101111110001); // Headphone out from Analog Mixer stage, no reduction in volume + } - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - DEBUGSR_PRINTLN("AC101Source:: initialize();"); - - // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too - // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); - // return; - // } - // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. - // Workaround: Set I2C pins here, which will also set them globally. - // Bug also exists in ES7243. - if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" - ERRORSR_PRINTF("\nAR: invalid AC101 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#ifdef _MoonModules_WLED_ - if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins - ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); - return; - } -#endif - // First route mclk, then configure ADC over I2C, then configure I2S - _ac101InitAdc(); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + public: + AC101Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("AC101Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid AC101 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } - - void deinitialize() { - I2SSource::deinitialize(); + #ifdef _MoonModules_WLED_ + if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; } + #endif + // First route mclk, then configure ADC over I2C, then configure I2S + _ac101InitAdc(); + 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 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_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // ADC over I2S is only available 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 -*/ + * 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, true) { - _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);} + public: + I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : I2SSource(sampleRate, blockSize, sampleScale, true) { + _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}; + } - 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("I2SAdcSource:: initialize()."); - _myADCchannel = 0x0F; - if(!ar_allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { - ERRORSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); - return; - } - _audioPin = audioPin; + /* identify Audiosource type - I2S-ADC*/ + AudioSourceType getType(void) { return (Type_I2SAdc); } - // Determine Analog channel. Only Channels on ADC1 are supported - int8_t channel = digitalPinToAnalogChannel(_audioPin); - if ((channel < 0) || (channel > 9)) { // channel == -1 means "not an ADC pin" - USER_PRINTF("AR: Incompatible GPIO used for analog audio input: %d\n", _audioPin); - return; - } else { - adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); - _myADCchannel = channel; - } + 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("I2SAdcSource:: initialize()."); + _myADCchannel = 0x0F; + if (!ar_allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { + ERRORSR_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 < 0) || (channel > 9)) { // channel == -1 means "not an ADC pin" + USER_PRINTF("AR: 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) { - ERRORSR_PRINTF("Failed to install i2s driver: %d\n", err); - return; - } + // Install Driver + esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); + if (err != ESP_OK) { + ERRORSR_PRINTF("Failed to install i2s driver: %d\n", err); + return; + } - // adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution - should not be needed, because i2s_set_adc_mode does that any way + // adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution - should not be needed, because i2s_set_adc_mode does that any way - // Enable I2S mode of ADC - err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); - if (err != ESP_OK) { - USER_PRINTF("AR: Failed to set i2s adc mode: %d\n", err); - return; - } + // Enable I2S mode of ADC + err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); + if (err != ESP_OK) { + USER_PRINTF("AR: 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_12); // configure ADC input amplification + // 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_12); // configure ADC input amplification - #if defined(I2S_GRAB_ADC1_COMPLETELY) + #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; - } + 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); + // //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; + _initialized = true; + } - // 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 + 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; - 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) + // 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 - lastADCsample = sample_out; // update ADC last sample - return(sample_out); - } + // 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) - 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 + lastADCsample = sample_out; // update ADC last sample + return (sample_out); + } - I2SSource::getSamples(buffer, num_samples); + 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 - #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 - } - } + I2SSource::getSamples(buffer, num_samples); - void deinitialize() { - ar_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 !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 + } + } - 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; - } + void deinitialize() { + ar_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" + private: + int8_t _audioPin; + int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined" }; -#endif +#endif // CONFIG_IDF_TARGET_ESP32 /* SPH0645 Microphone - This is an I2S microphone with some timing quirks that need - special consideration. -*/ + * 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, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) - { - // SPH0645 sends MSB format (no 1-bit shift like Philips STD) - to be handled by the slot config - } + public: + SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster = true) : I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { + // SPH0645 sends MSB format (no 1-bit shift like Philips STD) - to be handled by the slot config + } - void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN("SPH0654:: initialize();"); - I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); -#if defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION_MAJOR < 5) -// these registers are only existing in "classic" ESP32 + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("SPH0654:: initialize();"); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); + #if defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION_MAJOR < 5) + // these registers are only existing in "classic" ESP32 REG_SET_BIT(I2S_TIMING_REG(AR_I2S_PORT), BIT(9)); REG_SET_BIT(I2S_CONF_REG(AR_I2S_PORT), I2S_RX_MSB_SHIFT); -#else + #else #pragma message "FIX ME! Maybe." -#endif - } + #endif + } }; -#endif +#endif // ARDUINO_ARCH_ESP32 From 872c060ba1fc8d32e1d328a9fcd7f77c55fc8185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:13:06 +0200 Subject: [PATCH 6/6] Correct comments in .clang-format Fix typo in AlignArrayOfStructures comment and adjust formatting. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 86a71658..82dd1a41 100644 --- a/.clang-format +++ b/.clang-format @@ -35,7 +35,7 @@ IndentPPDirectives: BeforeHash # makes nested #ifdef easie AlignConsecutiveBitFields: AcrossEmptyLines # align bitfield definitions AllowShortBlocksOnASingleLine: true # while (true) { continue; } can be put on a single line -AlignArrayOfStructures: Right # aligns array initializer colums +AlignArrayOfStructures: Right # aligns array initializer columns AlignConsecutiveAssignments: # aligns the "=" in consecutive assignment rows. A matter of taste, but I do like it. Enabled: true AlignCompound: true