From 4feadf9b28dbae68d972194e2646a23d4aab9f9e Mon Sep 17 00:00:00 2001 From: netmindz Date: Mon, 6 Jan 2025 00:50:51 +0000 Subject: [PATCH] Copy current MoonModules usermod to AC --- usermods/audioreactive/audio_reactive.h | 2279 +++++++++++++++++------ usermods/audioreactive/audio_source.h | 529 +++++- 2 files changed, 2188 insertions(+), 620 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 9c463e0a19..9f0d077c0a 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1,5 +1,16 @@ #pragma once +/* + @title MoonModules WLED - audioreactive usermod + @file audio_reactive.h + @repo https://github.com/MoonModules/WLED, submit changes to this file as PRs to MoonModules/WLED + @Authors https://github.com/MoonModules/WLED/commits/mdev/ + @Copyright © 2024 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license Licensed under the EUPL-1.2 or later + +*/ + + #include "wled.h" #ifdef ARDUINO_ARCH_ESP32 @@ -7,10 +18,7 @@ #include #include -#ifdef WLED_ENABLE_DMX - #error This audio reactive usermod is not compatible with DMX Out. -#endif - +#include #endif #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) @@ -25,10 +33,37 @@ * .... */ + +#if defined(WLEDMM_FASTPATH) && defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) +#define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap +#endif + + +#define FFT_PREFER_EXACT_PEAKS // use different FFT windowing -> results in "sharper" peaks and less "leaking" into other frequencies +//#define SR_STATS + #if !defined(FFTTASK_PRIORITY) +#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) +// 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 asyc_tcp -//#define FFTTASK_PRIORITY 4 // above asyc_tcp +//#define FFTTASK_PRIORITY 2 // above looptask, below async_tcp +#endif +#endif + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +// 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(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#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) #endif // Comment/Uncomment to toggle usb serial debugging @@ -37,29 +72,64 @@ // #define SR_DEBUG // generic SR DEBUG messages #ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGSR_PRINT(x) DEBUGOUT(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUTLN(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUTF(x) #else #define DEBUGSR_PRINT(x) #define DEBUGSR_PRINTLN(x) #define DEBUGSR_PRINTF(x...) #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) +#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 +#endif + #if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT.print(x) - #define PLOT_PRINTLN(x) DEBUGOUT.println(x) - #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) + #define PLOT_PRINT(x) DEBUGOUT(x) + #define PLOT_PRINTLN(x) DEBUGOUTLN(x) + #define PLOT_PRINTF(x...) DEBUGOUTF(x) + #define PLOT_FLUSH() DEBUGOUTFlush() #else #define PLOT_PRINT(x) #define PLOT_PRINTLN(x) #define PLOT_PRINTF(x...) + #define PLOT_FLUSH() +#endif + +// sanity checks +#ifdef ARDUINO_ARCH_ESP32 + // we need more space in for oappend() stack buffer -> SETTINGS_STACK_BUF_SIZE and CONFIG_ASYNC_TCP_TASK_STACK_SIZE + #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_TASK_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_TASK_STACK_SIZE + #endif #endif -#define MAX_PALETTES 3 +// 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_IDLE_MS 2500 // timeout for "receiver idle" (milliseconds) static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static 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 bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! @@ -70,18 +140,29 @@ static float micDataReal = 0.0f; // MicIn data with full 24bit re 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 +#ifdef SR_SQUELCH +static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) - enable AGC if default "squelch" was provided +#else static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif -//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample + +#endif +static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getFrameTime() +static bool 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. -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects +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 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 @@ -89,8 +170,14 @@ static bool limiterOn = false; // bool: enable / disable dynamic #else static bool limiterOn = true; #endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec +static uint8_t micQuality = 0; // affects input filtering; 0 normal, 1 minimal filtering, 2 no filtering +#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. +#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 +#endif // peak detection #ifdef ARDUINO_ARCH_ESP32 @@ -104,7 +191,6 @@ 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 i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) // globals @@ -119,8 +205,15 @@ static uint8_t inputLevel = 128; // UI slider 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 + // // AGC presets @@ -138,103 +231,314 @@ const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // 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 bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. +static uint8_t useInputFilter = 0; // enables low-cut filtering. Applies before FFT. + +//WLEDMM add experimental settings +static uint8_t micLevelMethod = 0; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) +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 //////////////////// // Begin FFT Code // //////////////////// // some prototypes, to ensure consistent interfaces +static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float 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 +void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels +static 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. -static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; +#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 + + { 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) + + { 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). + * Try not to use Bluetooth or MP3 when playing the "pink noise" audio. BT-audio and MP3 both perform "acoustic adjustments" that we don't want now. + + * SR WLED: enable AGC ("standard" or "lazy"), set squelch to a low level, check that LEDs don't reacts in silence. + * SR WLED: select "Generic Line-In" as your Frequency Profile, "Linear" or "Square Root" as Frequency Scale + * SR WLED: Dynamic Limiter On, Dynamics Fall Time around 4200 - makes GEQ hold peaks for much longer + * SR WLED: Select GEQ effect, move all effect slider to max (i.e. right side) + + * Measure: play Pink Noise for 2-3 minutes - for examples from youtube https://www.youtube.com/watch?v=ZXtimhT-ff4 + * Measure: Take a Photo. Make sure that LEDs for each "bar" are well visible (ou need to count them later) + + * Your own profile: + * - Target for each LED bar is 50% to 75% of the max height --> 8(high) x 16(wide) panel means target = 5. 32 x 16 means target = 22. + * - From left to right - count the LEDs in each of the 16 frequency columns (that's why you need the photo). This is the barheight for each channel. + * - math time! Find the multiplier that will bring each bar to to target. + * * in case of square root scale: multiplier = (target * target) / (barheight * barheight) + * * in case of linear scale: multiplier = target / barheight + * + * - replace one of the "userdef" lines with a copy of the parameter line for "Line-In", + * - go through your new "userdef" parameter line, multiply each entry with the mutliplier you found for that column. + + * Compile + upload + * Test your new profile (same procedure as above). Iterate the process to improve results. + */ // globals and FFT Output variables shared with animations -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; +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 fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) -#ifdef SR_DEBUG -static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. -#endif +static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) +#if !defined(CONFIG_IDF_TARGET_ESP32C3) // 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 +// 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; // meaningfull part of FFT results - only the "lower half" contains useful information. +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.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 + // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 -// these options actually cause slow-downs on all esp32 processors, don't use them. -// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 -// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 -// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() -// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +// 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 an 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 -#include // FFT object is created in FFTcode // Helper functions +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + // compute average of several FFT result bins -static float fftAddAvg(int from, int to) { +// linear average +static float fftAddAvgLin(int from, int to) { float result = 0.0f; 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]; + } + 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 defined(CONFIG_IDF_TARGET_ESP32C3) +constexpr bool skipSecondFFT = true; +#else +constexpr bool skipSecondFFT = false; +#endif + +// allocate FFT sample buffers from heap +static bool alocateFFTBuffers(void) { + #ifdef SR_DEBUG + USER_PRINT(F("\nFree heap ")); USER_PRINTLN(ESP.getFreeHeap()); + #endif + + if (vReal) free(vReal); // should not happen + if (vImag) free(vImag); // should not happen + if ((vReal = (float*) calloc(sizeof(float), samplesFFT)) == nullptr) return false; // calloc or die + if ((vImag = (float*) calloc(sizeof(float), samplesFFT)) == nullptr) return false; +#ifdef FFT_MAJORPEAK_HUMAN_EAR + if (pinkFactors) free(pinkFactors); + if ((pinkFactors = (float*) calloc(sizeof(float), samplesFFT)) == 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_FLUSH(); + #endif + return(true); // success +} + +// 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 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; + } +} // // FFT main task // void FFTcode(void * parameter) { - DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); - - // allocate FFT buffers on first call - if (vReal == nullptr) vReal = (float*) calloc(sizeof(float), samplesFFT); - if (vImag == nullptr) vImag = (float*) calloc(sizeof(float), samplesFFT); - if ((vReal == nullptr) || (vImag == nullptr)) { - // something went wrong - if (vReal) free(vReal); vReal = nullptr; - if (vImag) free(vImag); vImag = nullptr; - return; - } - // Create FFT object with weighing factor storage - ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); + #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(); + #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; + +#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*) calloc(sizeof(float), samplesFFT_2); // allocate on first run + if (!oldSamples) { disableSoundProcessing = true; return; } // no memory -> die +#endif + + bool success = true; + if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run + if (success == false) { disableSoundProcessing = true; return; } // no memory -> die + + // 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*) calloc(sizeof(float), samplesFFT); // cache for FFT windowing factors - use heap + #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 + + #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 + #endif TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { @@ -242,161 +546,384 @@ void FFTcode(void * parameter) // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + 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 continue; } -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#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 + 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; + } + lastCycleStart = start; #endif // get a fresh batch of samples from I2S + 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 + usingOldSamples = true; + readOffset = samplesFFT_2; + } else { + usingOldSamples = false; + 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); + readOffset += samplesFFT_2; + } while (readOffset < samplesFFT); +#else if (audioSource) audioSource->getSamples(vReal, samplesFFT); - memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0 +#endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#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); + if (minStackFree > stackFree) { + minStackFree = stackFree; + 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; // smooth + sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10.0; // smooth } - start = esp_timer_get_time(); // start measuring FFT time + start = esp_timer_get_time(); // start measuring filter time #endif xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + isFirstRun = !isFirstRun; // toggle throttle + +#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) { + datMin = datMax = vReal[i]; + } else { + if (datMin > vReal[i]) datMin = vReal[i]; + if (datMax < vReal[i]) datMax = vReal[i]; + } + datAvg += vReal[i]; + } +#endif +#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) + // 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 + + // normal mode: filter everything + float *samplesStart = vReal; + uint16_t sampleCount = samplesFFT; + #ifdef FFT_USE_SLIDING_WINDOW + if (usingOldSamples) { + // sliding window mode: only latest 50% need filtering + samplesStart = vReal + samplesFFT_2; + sampleCount = samplesFFT_2; + } + #endif // band pass filter - can reduce noise floor by a factor of 50 // downside: frequencies below 100Hz will be ignored - if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + 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; - // find highest sample in the batch +#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 + } + 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; + #endif + + // find highest sample in the batch, and count zero crossings float maxSample = 0.0f; // max sample from FFT batch + uint_fast16_t newZeroCrossingCount = 0; for (int i=0; i < samplesFFT; i++) { // pick our our current mic sample - we take the max value from all samples that go into FFT - if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts + if ((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 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++; + } } + 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 SR_DEBUG - if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization -#else - if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. +#ifdef MIC_LOGGER + micReal_min = datMin; + micReal_max = datMax; + micReal_avg = datAvg / samplesFFT; +#if 0 + // compute mix/max again after filering - usefull 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 - // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) - FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy - //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection - FFT.compute( FFTDirection::Forward ); // Compute FFT - FFT.complexToMagnitude(); // Compute magnitudes - vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. + 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 ((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 + case 1: + FFT.windowing(FFTWindow::Hann, FFTDirection::Forward); // recommended for 50% overlap + wc = 0.66415918066; // 1.8554726898 * 2.0 + break; + case 2: + 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; + case 3: + 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 + 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 + } + #ifdef FFT_USE_SLIDING_WINDOW + 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. + + 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]; + #endif - FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + #if defined(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 + #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 + + #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); + } + #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 -#if defined(WLED_DEBUG) || defined(SR_DEBUG) + } 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 } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, samplesFFT * sizeof(float)); + memset(vReal, 0, sizeof(float) * samplesFFT); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; } - for (int i = 0; i < samplesFFT; i++) { - float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way - vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. - } // for() - - // mapping of FFT result bins to frequency channels - if (fabsf(sampleAvg) > 0.5f) { // noise gate open -#if 0 - /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. - * - * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. - * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. - * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. - * End frequency = Start frequency * multiplier ^ 16 - * Multiplier = (End frequency/ Start frequency) ^ 1/16 - * Multiplier = 1.320367784 - */ // Range - fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate -#else - /* new mapping, optimized for 22050 Hz by softhack007 */ - // bins frequency range - if (useBandPassFilter) { - // skip frequencies below 100hz - fftCalc[ 0] = 0.8f * fftAddAvg(3,4); - fftCalc[ 1] = 0.9f * fftAddAvg(4,5); - fftCalc[ 2] = fftAddAvg(5,6); - fftCalc[ 3] = fftAddAvg(6,7); - // don't use the last bins from 206 to 255. - fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass - fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass - fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass - fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - } - fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange - fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange - fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange - fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange - fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange - fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid - fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid - fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid - fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping -#endif + 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() + + // mapping of FFT result bins to frequency channels + //if (fabsf(sampleAvg) > 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) { + // 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 + } 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[ 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; - } + } } + + 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 } // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) - postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); + 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 -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#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*3 + fftTime*7)/10; // smooth + 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 #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - + { + #ifdef FFT_USE_SLIDING_WINDOW + if (!usingOldSamples) { + vTaskDelayUntil( &xLastWakeTime, xFrequencyDouble); // we need a double wait when no old data was used + } else + #endif + if ((skipSecondFFT == false) || (fabsf(volumeSmth) < 0.25f)) { + 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 + } + } } // for(;;)ever } // FFTcode() task end @@ -418,7 +945,7 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p //constexpr float beta1 = 0.8285f; // 18Khz constexpr float beta1 = 0.85f; // 20Khz - constexpr float beta2 = (1.0f - beta1) / 2.0f; + 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 @@ -436,69 +963,88 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p } } -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // 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[i]; + 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; } - // smooth results - rise fast, fall slower - if(fftCalc[i] > fftAvg[i]) // rise fast - fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] - else { // fall slow - if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero - else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero + 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 { + // 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; - if(limiterOn == true) - currentResult = fftAvg[i]; - else - currentResult = fftCalc[i]; + 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.42f; // 42 is the answer ;-) - currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks - if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function - else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined + currentResult *= 0.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 = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] break; case 2: // Linear scaling currentResult *= 0.30f; // needs a bit more damping, get stay below 255 - currentResult -= 4.0f; // giving a bit more room for peaks + 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.0f) currentResult = sqrtf(currentResult); - else currentResult = 0.0f; // special handling, because sqrt(0) = undefined + 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 = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] break; case 0: default: // no scaling - leave freq bins as-is - currentResult -= 4; // just a bit more room for peaks + currentResult -= 2; // just a bit more room for peaks break; } @@ -508,7 +1054,7 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // p if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; currentResult *= post_gain; } - fftResult[i] = constrain((int)currentResult, 0, 255); + fftResult[i] = max(min((int)(currentResult+0.5f), 255), 0); // +0.5 for proper rounding } } //////////////////// @@ -518,13 +1064,24 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // p // peak detection is called from FFT task when vReal[] contains valid FFT results static void detectSamplePeak(void) { bool havePeak = false; - // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. +#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 (havePeak) { samplePeak = true; @@ -536,14 +1093,13 @@ static void detectSamplePeak(void) { #endif static void autoResetPeak(void) { - uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime()); - if (millis() - timeOfPeak > peakDelay) { // Auto-reset of samplePeak after at least one complete frame has passed. + 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 == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + if (audioSyncEnabled == AUDIOSYNC_NONE) udpSamplePeak = false; // this is normally reset by transmitAudioData } } - //////////////////// // usermod class // //////////////////// @@ -554,6 +1110,11 @@ class AudioReactive : public Usermod { private: #ifdef ARDUINO_ARCH_ESP32 +// HUB75 workaround - audio receive only +#ifdef WLED_ENABLE_HUB75MATRIX +#undef SR_DMTYPE +#define SR_DMTYPE 254 // "network receive only" +#endif #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -580,33 +1141,42 @@ class AudioReactive : public Usermod { #else int8_t i2sckPin = I2S_CKPIN; #endif + #ifndef ES7243_SDAPIN + int8_t sdaPin = -1; + #else + int8_t sdaPin = ES7243_SDAPIN; + #endif + #ifndef ES7243_SCLPIN + int8_t sclPin = -1; + #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 { // "packed" ensures that there are no additional gaps - char header[6]; // 06 Bytes offset 0 - uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet + 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 reserved2; // 01 Bytes offset 17 - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes offset 18 - uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet - float FFT_Magnitude; // 04 Bytes offset 36 - float FFT_MajorPeak; // 04 Bytes offset 40 + 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 (with padding added by compiler) - for backwards compatibility + // 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 - int sampleAgc; // 04 Bytes - int sampleRaw; // 04 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 @@ -614,23 +1184,24 @@ class AudioReactive : public Usermod { double FFT_MajorPeak; // 08 Bytes }; - #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync + #define UDPSOUND_MAX_PACKET 96 // max packet size for audiosync, with a bit of "headroom" // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) - #ifdef UM_AUDIOREACTIVE_ENABLE - bool enabled = true; - #else + #if defined(SR_ENABLE_DEFAULT) || defined(UM_AUDIOREACTIVE_ENABLE) + bool enabled = true; // WLEDMM + #else bool enabled = false; - #endif - + #endif bool initDone = false; - bool addPalettes = false; - int8_t palettes = 0; // variables for UDP sound sync - WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) + 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 bool updateIsRunning = false; // true during OTA. @@ -640,9 +1211,7 @@ class AudioReactive : public Usermod { int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error - // variables used by getSample() and agcAvg() - int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller float expAdjF = 0.0f; // Used for exponential filter. @@ -652,9 +1221,9 @@ class AudioReactive : public Usermod { #endif // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc + 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 datapacket @@ -666,47 +1235,45 @@ class AudioReactive : public Usermod { // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; - static const char _config[]; - static const char _dynamics[]; - static const char _frequency[]; static const char _inputLvl[]; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) static const char _analogmic[]; #endif static const char _digitalmic[]; - static const char _addPalettes[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; // private methods - void removeAudioPalettes(void); - void createAudioPalettes(void); - CRGB getCRGBForBand(int x, int pal); - void fillAudioPalettes(void); //////////////////// // Debug support // //////////////////// void logAudio() { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + 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("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + PLOT_PRINT("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"); //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); - #ifdef ARDUINO_ARCH_ESP32 - //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); #endif PLOT_PRINTLN(); + PLOT_FLUSH(); #endif #ifdef FFT_SAMPLING_LOG @@ -716,7 +1283,7 @@ class AudioReactive : public Usermod { PLOT_PRINT("\t"); } PLOT_PRINTLN(); - #endif + #endif // OPTIONS are in the following format: Description \n Option // @@ -760,8 +1327,8 @@ class AudioReactive : public Usermod { #endif // FFT_SAMPLING_LOG } // logAudio() - #ifdef ARDUINO_ARCH_ESP32 + ////////////////////// // Audio Processing // ////////////////////// @@ -799,7 +1366,7 @@ class AudioReactive : public Usermod { if (time_now - last_time > 2) { last_time = time_now; - if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0)) { + 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 @@ -823,11 +1390,11 @@ class AudioReactive : public Usermod { && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping else - control_integrated *= 0.9; // spin down that beasty integrator + 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 energy zone + 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" @@ -850,13 +1417,26 @@ class AudioReactive : public Usermod { // 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); + } + } 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() @@ -866,60 +1446,81 @@ class AudioReactive : public Usermod { { float sampleAdj; // Gain adjusted sample value float tmpSample; // An interim sample variable used for calculations. - const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. + const 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; + } - #ifdef WLED_DISABLE_SOUND - micIn = inoise8(millis(), millis()); // Simulated analog read - micDataReal = micIn; - #else - #ifdef ARDUINO_ARCH_ESP32 - micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; - #else - // this is the minimal code for reading analog mic input on 8266. - // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. - static unsigned long lastAnalogTime = 0; - static float lastAnalogValue = 0.0f; - if (millis() - lastAnalogTime > 20) { - micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. - lastAnalogTime = millis(); - lastAnalogValue = micDataReal; - yield(); - } else micDataReal = lastAnalogValue; - micIn = int(micDataReal); - #endif - #endif - - micLev += (micDataReal-micLev) / 12288.0f; - if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal + 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 + } - micIn -= micLev; // Let's center it to 0 now // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. float micInNoDC = fabsf(micDataReal - micLev); - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); + + 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 = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate - if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0" + 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; + } + + 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) tmpSample = expAdjF; - micIn = abs(micIn); // And get the absolute value of each sample - sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment - sampleReal = tmpSample; + // 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), 0); // Question: why are we limiting the value to 8 bits ??? + 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 @@ -928,12 +1529,75 @@ class AudioReactive : public Usermod { } 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 + } 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 + } + + // 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() { + // some constants + constexpr float logMinSample = 0.8329091229351f; // ln(2.3) + constexpr float sampleMin = 2.3f; + constexpr float logMaxSample = 10.1895683436f; // ln(32767 - 6144) + constexpr float sampleMax = 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 its 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 <= sampleMin) return 0.0f; + if(micSampleMax >= sampleMax) return 255.0f; + + // apply logarithmic scaling + float scaledvalue = logf(micSampleMax); + scaledvalue = (scaledvalue - logMinSample) / (logMaxSample - logMinSample); // 0...1 + return fminf(fmaxf(256.0*scaledvalue, 0), 255.0); // scaled value + } #endif + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) */ @@ -946,7 +1610,7 @@ class AudioReactive : public Usermod { if (limiterOn == false) return; long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up float deltaSample = volumeSmth - last_volumeSmth; if (attackTime > 0) { // user has defined attack time > 0 @@ -964,6 +1628,38 @@ class AudioReactive : public Usermod { last_time = millis(); } + // 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; + + 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]; + } + } + + 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(); + } ////////////////////// // UDP Sound Sync // @@ -975,21 +1671,29 @@ class AudioReactive : public Usermod { // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection static unsigned long last_connection_attempt = 0; - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if ((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 (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; + if (updateIsRunning) return; // don't reconnect during OTA // 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; @@ -1001,11 +1705,21 @@ class AudioReactive : public Usermod { 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] = (uint8_t)constrain(fftResult[i], 0, 254); + 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; @@ -1013,11 +1727,10 @@ class AudioReactive : public Usermod { fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); fftUdp.endPacket(); } - return; + + frameCounter++; } // transmitAudioData() - #endif - static bool isValidUdpSyncVersion(const char *header) { return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; } @@ -1025,10 +1738,31 @@ class AudioReactive : public Usermod { return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } - void decodeAudioData(int packetSize, uint8_t *fftBuff) { + 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# + 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; + // add info for UI + if ((receivedPacket.frameCounter > 0) && (lastFrameCounter > 0)) receivedFormat = 3; // v2+ + else receivedFormat = 2; // v2 + // check sequence + bool sequenceOK = false; + if(receivedPacket.frameCounter > lastFrameCounter) sequenceOK = true; // sequence OK + if((lastFrameCounter < 12) && (receivedPacket.frameCounter > 248)) sequenceOK = false; // prevent sequence "roll-back" due to late packets (1->254) + if((lastFrameCounter > 248) && (receivedPacket.frameCounter < 12)) sequenceOK = true; // handle roll-over (255 -> 0) + if(audioSyncSequence == false) sequenceOK = true; // sequence checking disabled by user + if((sequenceOK == false) && (receivedPacket.frameCounter != 0)) { // always accept "0" - its 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); @@ -1039,7 +1773,7 @@ class AudioReactive : public Usermod { sampleAvg = volumeSmth; rawSampleAgc = volumeRaw; sampleAgc = volumeSmth; - multAgc = 1.0f; + multAgc = 1.0f; #endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. @@ -1054,6 +1788,22 @@ class AudioReactive : public Usermod { 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; } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { @@ -1067,8 +1817,8 @@ class AudioReactive : public Usermod { sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; sampleAgc = volumeSmth; rawSampleAgc = volumeRaw; - multAgc = 1.0f; -#endif + 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(); @@ -1082,6 +1832,8 @@ class AudioReactive : public Usermod { 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() // check & process new data. return TRUE in case that new audio data was received. @@ -1089,27 +1841,42 @@ class AudioReactive : public Usermod { if (!udpSyncConnected) return false; bool haveFreshData = false; - size_t packetSize = fftUdp.parsePacket(); + size_t packetSize = 0; + // WLEDMM use exception handler to catch out-of-memory errors + #if __cpp_exceptions + try{ + packetSize = fftUdp.parsePacket(); + } catch(...) { + packetSize = 0; // low heap memory -> discard packet. #ifdef ARDUINO_ARCH_ESP32 - if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 + fftUdp.flush(); // this does not work on 8266 +#endif + DEBUG_PRINTLN(F("receiveAudioData: parsePacket out of memory exception caught!")); + USER_FLUSH(); + } + #else + packetSize = fftUdp.parsePacket(); + #endif + +#ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) #endif if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { + static uint8_t fftUdpBuffer[UDPSOUND_MAX_PACKET+1] = { 0 }; // static buffer for receiving, to reuse the same memory and avoid heap fragmentation //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays - fftUdp.read(fftBuff, packetSize); + fftUdp.read(fftUdpBuffer, packetSize); // VERIFY THAT THIS IS A COMPATIBLE PACKET - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { - decodeAudioData(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; + if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftUdpBuffer))) { receivedFormat = 2; + haveFreshData = decodeAudioData(packetSize, fftUdpBuffer); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); } else { - if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { - decodeAudioData_v1(packetSize, fftBuff); + if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftUdpBuffer))) { + decodeAudioData_v1(packetSize, fftUdpBuffer); + receivedFormat = 1; //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); haveFreshData = true; - receivedFormat = 1; } else receivedFormat = 0; // unknown format } } @@ -1129,19 +1896,19 @@ class AudioReactive : public Usermod { * You can use it to initialize variables, sensors or similar. * It is called *AFTER* readFromConfig() */ - void setup() override + void setup() { disableSoundProcessing = true; // just to be sure if (!initDone) { // usermod exchangeable data // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers um_data = new um_data_t; - um_data->u_size = 8; + um_data->u_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_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; @@ -1149,37 +1916,52 @@ class AudioReactive : public Usermod { 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_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; +#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; } - #ifdef ARDUINO_ARCH_ESP32 - // Reset I2S peripheral for good measure + // 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 #if !defined(CONFIG_IDF_TARGET_ESP32C3) delay(100); periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 #endif + #endif delay(100); // Give that poor microphone some time to setup. - useBandPassFilter = false; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + 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) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips case 0: //ADC analog #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) case 5: //PDM Microphone + case 51: //legacy PDM Microphone #endif #endif case 1: @@ -1190,8 +1972,15 @@ class AudioReactive : public Usermod { 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: @@ -1203,6 +1992,7 @@ class AudioReactive : public Usermod { 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; @@ -1210,52 +2000,146 @@ class AudioReactive : public Usermod { case 5: DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + 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: - DEBUGSR_PRINTLN(F("AR: ES8388 Source")); - audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); + #ifdef use_es8388_mic + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Mic)")); + #else + 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: + #ifdef use_wm8978_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; + 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_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // 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); - useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog if (audioSource) audioSource->initialize(audioPin); break; #endif } delay(250); // give microphone enough time to initialise - if (!audioSource) enabled = false; // audio failed to initialise + if (!audioSource && (dmType < 254)) enabled = false; // audio failed to initialise #endif - if (enabled) onUpdateBegin(false); // create FFT task, and initialize network - + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed + if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif + + 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; + } 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(); + } + #endif + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1263,20 +2147,33 @@ class AudioReactive : public Usermod { * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ - void connected() override + void connected() { 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 & 0x03)) { + 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).")); + } } + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1290,7 +2187,7 @@ 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 + void loop() { static unsigned long lastUMRun = millis(); @@ -1300,7 +2197,18 @@ class AudioReactive : public Usermod { return; } // We cannot wait indefinitely before processing audio data - if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + 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 && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) + useNetworkAudio = true; + else + useNetworkAudio = false; + if (audioSyncEnabled == AUDIOSYNC_REC) + useNetworkAudio = true; // don't fall back to local audio in standard "receive mode" + } // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed @@ -1310,43 +2218,57 @@ class AudioReactive : public Usermod { ||(realtimeMode == REALTIME_MODE_ADALIGHT) ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + #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 == 0) && audioSource->isInitialized()) { // we just switched to "enabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + 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 == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + if ((disableSoundProcessing == true) && (audioSyncEnabled != AUDIOSYNC_REC)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping disableSoundProcessing = false; } - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode + 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->isInitialized()) disableSoundProcessing = true; // no audio source - + 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 & 0x02) && !disableSoundProcessing) { + 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. - #ifdef WLED_DEBUG - // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS - //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); + #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 @@ -1368,6 +2290,20 @@ class AudioReactive : public Usermod { 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 + 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 + } limitSampleDynamics(); } // if (!disableSoundProcessing) #endif @@ -1378,21 +2314,48 @@ class AudioReactive : public Usermod { connectUDPSoundSync(); // ensure we have a connection - if needed // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + 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) { have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); + if (have_new_sample) { + last_UDPTime = millis(); + useNetworkAudio = true; // UDP input arrived - use it + } + lastTime = millis(); + } else { #ifdef ARDUINO_ARCH_ESP32 - else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. + fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. #endif - lastTime = millis(); } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + if (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 ( (audioSyncEnabled & AUDIOSYNC_REC) // receive mode + && udpSyncConnected // connected + && (receivedFormat > 0) // we actually received something in the past + && ((millis() - last_UDPTime) > 25000)) { // 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 defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) @@ -1407,7 +2370,7 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); - maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing + 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 @@ -1425,29 +2388,38 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + #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 + #endif + haveNewFFTResult = false; // reset notification // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); } #endif - - fillAudioPalettes(); } +#if defined(_MoonModules_WLED_) && defined(WLEDMM_FASTPATH) + void loop2(void) { + loop(); + } +#endif - bool getUMData(um_data_t **data) override + bool getUMData(um_data_t **data) { 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 + void onUpdateBegin(bool init) { #ifdef WLED_DEBUG - fftTime = sampleTime = 0; + fftTime = sampleTime = filterTime = 0; #endif // gracefully suspend FFT task (if running) disableSoundProcessing = true; @@ -1468,19 +2440,24 @@ class AudioReactive : public Usermod { autoResetPeak(); if (init && FFT_Task) { - delay(25); // give some time for I2S driver to finish sampling before we suspend it + delay(25); // WLEDMM: givesome time for I2S driver to finish sampling vTaskSuspend(FFT_Task); // update is about to begin, disable task to prevent crash if (udpSyncConnected) { // close UDP sync connection (if open) udpSyncConnected = false; fftUdp.stop(); + DEBUGSR_PRINTLN(F("AR onUpdateBegin(true): UDP connection closed.")); + receivedFormat = 0; } } else { // update has failed or create task requested if (FFT_Task) { vTaskResume(FFT_Task); connected(); // resume UDP - } else - xTaskCreateUniversal( // xTaskCreateUniversal also works on -S2 and -C3 with single core + } else { + if (audioSource) // WLEDMM only create FFT task if we have a valid audio source +// xTaskCreatePinnedToCore( +// xTaskCreate( // no need to "pin" this task to core #0 + xTaskCreateUniversal( FFTcode, // Function to implement the task "FFT", // Name of the task 3592, // Stack size in words // 3592 leaves 800-1024 bytes of task stack free @@ -1489,10 +2466,15 @@ class AudioReactive : public Usermod { &FFT_Task // Task handle , 0 // Core where the task should run ); + } } micDataReal = 0.0f; // just to be sure - if (enabled) disableSoundProcessing = false; + if (enabled && audioSource) disableSoundProcessing = false; updateIsRunning = init; + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } #else // reduced function for 8266 @@ -1504,6 +2486,7 @@ class AudioReactive : public Usermod { volumeRaw = 0; volumeSmth = 0; for(int i=(init?0:1); i 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")); } @@ -1586,14 +2583,13 @@ class AudioReactive : public Usermod { } #endif // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG - // current Audio input infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { + 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 < 2500) + if (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS) infoArr.add(F(" - receiving")); else infoArr.add(F(" - idle")); @@ -1612,10 +2608,13 @@ class AudioReactive : public Usermod { if (audioSource->getType() == AudioSource::Type_I2SAdc) { infoArr.add(F("ADC analog")); } else { - infoArr.add(F("I2S digital")); + if (dmType != 51) + infoArr.add(F("I2S digital")); + else + infoArr.add(F("legacy I2S PDM")); } // input level or "silence" - if (maxSample5sec > 1.0f) { + 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); @@ -1625,7 +2624,7 @@ class AudioReactive : public Usermod { } else { // error during audio source setup infoArr.add(F("not initialized")); - infoArr.add(F(" - check pin settings")); + if (dmType < 254) infoArr.add(F(" - check pin settings")); } } @@ -1638,13 +2637,13 @@ class AudioReactive : public Usermod { } // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + 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 && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + 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"); @@ -1653,37 +2652,59 @@ class AudioReactive : public Usermod { // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { + if (audioSyncEnabled & AUDIOSYNC_SEND) { infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { + if ((udpSyncConnected) && (millis() - lastTime < AUDIOSYNC_IDLE_MS)) 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")); } } else infoArr.add("off"); if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + 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) + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) #ifdef ARDUINO_ARCH_ESP32 + infoArr = user.createNestedArray(F("I2S cycle time")); + infoArr.add(roundf(fftTaskCycle)/100.0f); + infoArr.add(" ms"); + infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(float(sampleTime)/100.0f); + 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(float(fftTime)/100.0f); - if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow + infoArr.add(roundf(fftTime)/100.0f); + +#ifdef FFT_USE_SLIDING_WINDOW + unsigned timeBudget = doSlidingFFT ? (FFT_MIN_CYCLE) : fftTaskCycle / 115; +#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/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability + 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 Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); + 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 } @@ -1694,7 +2715,7 @@ class AudioReactive : public Usermod { * 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 + void addToJsonState(JsonObject& root) { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; @@ -1709,7 +2730,7 @@ class AudioReactive : public Usermod { * 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 + void readFromJsonState(JsonObject& root) { if (!initDone) return; // prevent crash on boot applyPreset() bool prevEnabled = enabled; @@ -1718,11 +2739,6 @@ class AudioReactive : public Usermod { 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()) { @@ -1730,18 +2746,8 @@ class AudioReactive : public Usermod { } #endif } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } } - void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && strip.customPalettes.size()<10) { - // if palettes were removed during JSON call re-add them - createAudioPalettes(); - } - } /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -1778,12 +2784,10 @@ 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 + void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; - top[FPSTR(_addPalettes)] = addPalettes; - #ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); @@ -1791,30 +2795,48 @@ class AudioReactive : public Usermod { #endif JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); - dmic["type"] = dmType; + 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(FPSTR(_config)); + JsonObject cfg = top.createNestedObject("config"); cfg[F("squelch")] = soundSquelch; cfg[F("gain")] = sampleGain; cfg[F("AGC")] = soundAgc; - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); + //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(FPSTR(_dynamics)); + JsonObject dynLim = top.createNestedObject("dynamics"); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; dynLim[F("fall")] = decayTime; JsonObject sync = top.createNestedObject("sync"); - sync["port"] = audioSyncPort; - sync["mode"] = audioSyncEnabled; + sync[F("port")] = audioSyncPort; + sync[F("mode")] = audioSyncEnabled; + sync[F("check_sequence")] = audioSyncSequence; } @@ -1833,16 +2855,22 @@ 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 + bool readFromConfig(JsonObject& root) { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); - bool oldEnabled = enabled; - bool oldAddPalettes = addPalettes; - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); - configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); +#ifdef ARDUINO_ARCH_ESP32 + // 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); #ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); @@ -1855,90 +2883,319 @@ class AudioReactive : public Usermod { if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM #endif + #else + if (dmType == 5) useInputFilter = 1; // enable filter for PDM + if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM #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 - configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); - configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); - configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); + configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); + configComplete &= getJsonValue(top["frequency"][F("profile")], pinkIndex); //WLEDMM #endif - configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); - configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); + 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("check_sequence")], audioSyncSequence); + // WLEDMM notify user when a reboot is necessary + #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(); - } // else setup() will create palettes + if ((audioSource != nullptr) && (oldDMType != dmType)) errorFlag = ERR_REBOOT_NEEDED; // changing mic type requires reboot + if ( (audioSource != nullptr) && (enabled==true) + && ((oldI2SsdPin != i2ssdPin) || (oldI2SsdPin != i2ssdPin) || (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; } - void appendConfigData(Print& uiScript) override + void appendConfigData() { - uiScript.print(F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style + 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_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -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 + #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_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #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(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if SR_DMTYPE==5 + oappend(SET_F("addOption(dd,'Generic I2S PDM (⎌)',5);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + #endif + #if SR_DMTYPE==51 + oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾ (⎌)',51);")); + #else + oappend(SET_F("addOption(dd,'.Legacy I2S 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);")); + +#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 + + 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 - uiScript.print(F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] - uiScript.print(F("dd=addDropdown(ux,'digitalmic:type');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - uiScript.print(F("addOption(dd,'Generic Analog',0);")); - #endif - uiScript.print(F("addOption(dd,'Generic I2S',1);")); - uiScript.print(F("addOption(dd,'ES7243',2);")); - uiScript.print(F("addOption(dd,'SPH0654',3);")); - uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - uiScript.print(F("addOption(dd,'Generic I2S PDM',5);")); - #endif - uiScript.print(F("addOption(dd,'ES8388',6);")); - - uiScript.print(F("dd=addDropdown(ux,'config:AGC');")); - uiScript.print(F("addOption(dd,'Off',0);")); - uiScript.print(F("addOption(dd,'Normal',1);")); - uiScript.print(F("addOption(dd,'Vivid',2);")); - uiScript.print(F("addOption(dd,'Lazy',3);")); - - uiScript.print(F("dd=addDropdown(ux,'dynamics:limiter');")); - uiScript.print(F("addOption(dd,'Off',0);")); - uiScript.print(F("addOption(dd,'On',1);")); - uiScript.print(F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - uiScript.print(F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); - uiScript.print(F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); - - uiScript.print(F("dd=addDropdown(ux,'frequency:scale');")); - uiScript.print(F("addOption(dd,'None',0);")); - uiScript.print(F("addOption(dd,'Linear (Amplitude)',2);")); - uiScript.print(F("addOption(dd,'Square Root (Energy)',3);")); - uiScript.print(F("addOption(dd,'Logarithmic (Loudness)',1);")); -#endif - - uiScript.print(F("dd=addDropdown(ux,'sync:mode');")); - uiScript.print(F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Send',1);")); // AUDIOSYNC_SEND +#endif + oappend(SET_F("addOption(dd,'Receive',2);")); // AUDIOSYNC_REC #ifdef ARDUINO_ARCH_ESP32 - uiScript.print(F("addOption(dd,'Send',1);")); + oappend(SET_F("addOption(dd,'Receive or Local',6);")); // AUDIOSYNC_REC_PLUS #endif - uiScript.print(F("addOption(dd,'Receive',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
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' + + oappend(SET_F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field #ifdef ARDUINO_ARCH_ESP32 - uiScript.print(F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - uiScript.print(F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); - uiScript.print(F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); - uiScript.print(F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); + 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_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - uiScript.print(F("addInfo(uxp,3,'only use -1, 0, 1 or 3','I2S MCLK');")); - #else - uiScript.print(F("addInfo(uxp,3,'master clock','I2S MCLK');")); + 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 } @@ -1948,7 +3205,7 @@ 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() override + //void handleOverlayDraw() //{ //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black //} @@ -1958,109 +3215,19 @@ class AudioReactive : public Usermod { * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). * This could be used in the future for the system to determine whether your usermod is installed. */ - uint16_t getId() override + uint16_t getId() { return USERMOD_ID_AUDIOREACTIVE; } }; -void AudioReactive::removeAudioPalettes(void) { - DEBUG_PRINTLN(F("Removing audio palettes.")); - while (palettes>0) { - strip.customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); -} - -void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); - if (palettes) return; - DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; pal #include "wled.h" #include #include @@ -15,6 +28,9 @@ #define SRate_t int #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 //#include @@ -22,7 +38,7 @@ // see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents // and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes -#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) +#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) // there are two things in these MCUs that could lead to problems with audio processing: // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) // * single core, so FFT task might slow down other things like LED updates @@ -49,6 +65,11 @@ // data type requested from the I2S driver - currently we always use 32bit //#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible +#if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) + // this is bitter, but necessary to survive + #define I2S_USE_16BIT_SAMPLES +#endif + #ifdef I2S_USE_16BIT_SAMPLES #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT #define I2S_datatype int16_t @@ -71,7 +92,7 @@ * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8)) // 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) @@ -105,6 +126,9 @@ #endif +// 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 @@ -148,38 +172,61 @@ class AudioSource { 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) : + 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 }; /* 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 */ class I2SSource : public AudioSource { public: - I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - AudioSource(sampleRate, blockSize, sampleScale) { + I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + AudioSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config = { - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), + .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, + .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), @@ -192,19 +239,19 @@ class I2SSource : public AudioSource { } virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("I2SSource:: initialize().")); + DEBUGSR_PRINTLN("I2SSource:: initialize()."); if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { - if (!PinManager::allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || - !PinManager::allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); + if (!pinManager.allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || + !pinManager.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 (!PinManager::allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); + if (!pinManager.allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); return; } } else { @@ -226,10 +273,17 @@ class I2SSource : public AudioSource { _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); + } + 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; @@ -244,13 +298,28 @@ class I2SSource : public AudioSource { #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 #endif + #if defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL needed for HUB75 DMA driver ? + #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!"); + } + } + // Reserve the master clock pin if provided _mclkPin = mclkPin; if (mclkPin != I2S_PIN_NO_CHANGE) { - if(!PinManager::allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); + if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); return; } else _routeMclk(mclkPin); @@ -268,32 +337,36 @@ class I2SSource : public AudioSource { //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); - esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); + esp_err_t err = i2s_driver_install(AR_I2S_PORT, &_config, 0, nullptr); if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); + 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_PDM) { + 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 { + } else { 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(I2S_NUM_0, &_pinConfig); + err = i2s_set_pin(AR_I2S_PORT, &_pinConfig); if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + 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) - err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. + 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) { - DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + ERRORSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); + i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver return; } #endif @@ -302,33 +375,36 @@ class I2SSource : public AudioSource { virtual void deinitialize() { _initialized = false; - esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); + 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) PinManager::deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); - if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); - if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); + if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); // Release the master clock pin - if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); + if (_mclkPin != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); } virtual void getSamples(float *buffer, uint16_t num_samples) { if (_initialized) { esp_err_t err; size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ - I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ - err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); + 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 get samples: %d\n", err); return; } // For correct operation, we need to read exactly sizeof(samples) bytes from i2s - if (bytes_read != sizeof(newSamples)) { - DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); + 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; } @@ -383,6 +459,10 @@ class I2SSource : public AudioSource { */ class ES7243 : public I2SSource { private: + // I2C initialization functions for ES7243 + void _es7243I2cBegin() { + Wire.setClock(100000); + } void _es7243I2cWrite(uint8_t reg, uint8_t val) { #ifndef ES7243_ADDR @@ -398,6 +478,7 @@ class ES7243 : public I2SSource { } void _es7243InitAdc() { + _es7243I2cBegin(); _es7243I2cWrite(0x00, 0x01); _es7243I2cWrite(0x06, 0x00); _es7243I2cWrite(0x05, 0x1B); @@ -407,15 +488,24 @@ class ES7243 : public I2SSource { } public: - ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { + 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(F("ES7243:: initialize();")); - if ((i2sckPin < 0) || (mclkPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, 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; + } + if (!pinManager.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; } @@ -435,14 +525,16 @@ class ES7243 : public I2SSource { */ class ES8388Source : public I2SSource { private: + // I2C initialization functions for ES8388 + void _es8388I2cBegin() { + Wire.setClock(100000); + } void _es8388I2cWrite(uint8_t reg, uint8_t val) { -#ifndef ES8388_ADDR - Wire.beginTransmission(0x10); - #define ES8388_ADDR 0x10 // default address -#else + #ifndef ES8388_ADDR + #define ES8388_ADDR 0x10 // default address + #endif Wire.beginTransmission(ES8388_ADDR); -#endif Wire.write((uint8_t)reg); Wire.write((uint8_t)val); uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK @@ -459,6 +551,7 @@ class ES8388Source : public I2SSource { // 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 @@ -524,14 +617,26 @@ class ES8388Source : public I2SSource { public: ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : - I2SSource(sampleRate, blockSize, sampleScale) { + 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(F("ES8388Source:: initialize();")); - if ((i2sckPin < 0) || (mclkPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, 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; + } + if (!pinManager.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; } @@ -546,6 +651,302 @@ class ES8388Source : public I2SSource { }; +/* 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: + // I2C initialization functions for es8311 + void _es8311I2cBegin() { + Wire.setClock(100000); + } + + void _es8311I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES8311_ADDR + #define ES8311_ADDR 0x18 // default address is... foggy + #endif + 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 + // + _es8311I2cBegin(); + _es8311I2cWrite(0x00, 0b00011111); // RESET, default value + _es8311I2cWrite(0x45, 0b00000000); // GP, default value + _es8311I2cWrite(0x01, 0b00111010); // CLOCK MANAGER was 0b00110000 trying 0b00111010 (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 was 0b00000011 trying 0b00100100 was good + _es8311I2cWrite(0x0B, 0b00000000); // SYSTEM at default + _es8311I2cWrite(0x0C, 0b00100000); // SYSTEM was 0b00001111 trying 0b00100000 + _es8311I2cWrite(0x10, 0b00010011); // SYSTEM was 0b00011111 trying 0b00010011 + _es8311I2cWrite(0x11, 0b01111100); // SYSTEM was 0b01111111 trying 0b01111100 + _es8311I2cWrite(0x00, 0b11000000); // *** RESET (again - seems important?) + _es8311I2cWrite(0x01, 0b00111010); // *** CLOCK MANAGER was 0b00111111 trying 0b00111010 (again?? seems important) + _es8311I2cWrite(0x14, 0b00010000); // *** SYSTEM was 0b00011010 trying 0b00010000 (PGA gain) + _es8311I2cWrite(0x0A, 0b00001000); // *** SDP OUT, was 0b00001100 trying 0b00001000 (I2S 32-bit) + _es8311I2cWrite(0x0E, 0b00000010); // *** SYSTEM was 0b00000010 trying 0b00000010 + _es8311I2cWrite(0x0F, 0b01000100); // SYSTEM was 0b01000100 + _es8311I2cWrite(0x15, 0b00010000); // ADC soft ramp (disabled 0000xxxx) + _es8311I2cWrite(0x1B, 0b00000101); // ADC soft-mute was 0b00000101 + _es8311I2cWrite(0x1C, 0b01100101); // ADC EQ and offset freeze at 0b01100101 (bad at 0b00101100) + _es8311I2cWrite(0x17, 0b10111111); // ADC volume was 0b11111111 trying ADC volume 0b10111111 = 0db (maxgain) + _es8311I2cWrite(0x18, 0b10000001); // ADC ALC enabled and AutoMute disabled. + // _es8311I2cWrite(0x19, 0b11110100); // ADC ALC max and min - not sure how best to use this, default seems fine + } + + 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; + } + if (!pinManager.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; + } + + // First route mclk, then configure ADC over I2C, then configure I2S + _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); + } + + 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 + + } + + 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; + } + if (!pinManager.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; + } + + // 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); + } + + 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 + + } + + 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; + } + if (!pinManager.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; + } + + // 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 @@ -553,7 +954,7 @@ class ES8388Source : public I2SSource { #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) -// ADC over I2S is only availeable in "classic" ESP32 +// ADC over I2S is only available in "classic" ESP32 /* ADC over I2S Microphone This microphone is an ADC pin sampled via the I2S interval @@ -563,7 +964,7 @@ class ES8388Source : public I2SSource { class I2SAdcSource : public I2SSource { public: I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { + I2SSource(sampleRate, blockSize, sampleScale, true) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), .sample_rate = _sampleRate, @@ -587,18 +988,18 @@ class I2SAdcSource : public I2SSource { AudioSourceType getType(void) {return(Type_I2SAdc);} void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("I2SAdcSource:: initialize().")); + DEBUGSR_PRINTLN("I2SAdcSource:: initialize()."); _myADCchannel = 0x0F; - if(!PinManager::allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); + if(!pinManager.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 > 9) { - DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _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)); @@ -608,16 +1009,16 @@ class I2SAdcSource : public I2SSource { // Install Driver esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); + 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 + // 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) { - DEBUGSR_PRINTF("Failed to set i2s adc mode: %d\n", err); + USER_PRINTF("AR: Failed to set i2s adc mode: %d\n", err); return; } @@ -717,7 +1118,7 @@ class I2SAdcSource : public I2SSource { } void deinitialize() { - PinManager::deallocatePin(_audioPin, PinOwner::UM_Audioreactive); + pinManager.deallocatePin(_audioPin, PinOwner::UM_Audioreactive); _initialized = false; _myADCchannel = 0x0F; @@ -754,17 +1155,17 @@ class I2SAdcSource : public I2SSource { // a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin(). class SPH0654 : public I2SSource { public: - SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) + SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) {} void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { - DEBUGSR_PRINTLN(F("SPH0654:: initialize();")); + DEBUGSR_PRINTLN("SPH0654:: initialize();"); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // these registers are only existing in "classic" ESP32 - REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9)); - REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT); + 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 #warning FIX ME! Please. #endif