diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 384e2e2b..9d833715 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -4,7 +4,7 @@ #include #include -#ifndef ESP32 +#ifndef ARDUINO_ARCH_ESP32 #error This audio reactive usermod does not support the ESP8266. #endif @@ -25,38 +25,46 @@ // #define FFT_SAMPLING_LOG // FFT result debugging // #define SR_DEBUG // generic SR DEBUG messages - #ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) Serial.print(x) - #define DEBUGSR_PRINTLN(x) Serial.println(x) - #define DEBUGSR_PRINTF(x...) Serial.printf(x) + #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) #else #define DEBUGSR_PRINT(x) #define DEBUGSR_PRINTLN(x) #define DEBUGSR_PRINTF(x...) #endif +#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) + #define PLOT_PRINT(x) DEBUGOUT.print(x) + #define PLOT_PRINTLN(x) DEBUGOUT.println(x) + #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define PLOT_PRINT(x) + #define PLOT_PRINTLN(x) + #define PLOT_PRINTF(x...) +#endif +// use audio source class (ESP32 specific) #include "audio_source.h" - -constexpr i2s_port_t I2S_PORT = I2S_NUM_0; -constexpr int BLOCK_SIZE = 128; -constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms -//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms -//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms - -#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling -//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling -//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling +constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) +constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) // globals static uint8_t inputLevel = 128; // UI slider value -static uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) -static uint8_t sampleGain = 60; // sample gain (config value) -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +#ifndef SR_SQUELCH + uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) +#else + uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) +#endif +#ifndef SR_GAIN + uint8_t sampleGain = 60; // sample gain (config value) +#else + uint8_t sampleGain = SR_GAIN; // sample gain (config value) +#endif +static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group // user settable parameters for limitSoundDynamics() static bool limiterOn = true; // bool: enable / disable dynamics limiter @@ -86,11 +94,13 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // static AudioSource *audioSource = nullptr; static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. // audioreactive variables shared with FFT task static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample // peak detection static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() @@ -106,6 +116,62 @@ static void autoResetPeak(void); // peak auto-reset function // 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 +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 + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +static TaskHandle_t FFT_Task = nullptr; + +// Table of multiplication factors so that we can even out the frequency response. +static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; + +// globals and FFT Output variables shared with animations +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects +#if defined(WLED_DEBUG) || defined(SR_DEBUG) +static uint64_t fftTime = 0; +static uint64_t sampleTime = 0; +#endif + +// FFT Task variables (filtering and post-processing) +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +#ifdef SR_DEBUG +static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. +#endif + +// audio source parameters and constant +constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms +//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms +//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms +#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling +//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling +//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling + +// FFT Constants +constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 +constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. +// the following are observed values, supported by a bit of "educated guessing" +//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels +#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +#define LOG_256 5.54517744f // log(256) + +// These are the input and output vectors. Input vectors receive computed results from FFT. +static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins +static float vImag[samplesFFT] = {0.0f}; // imaginary parts +#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT +static float windowWeighingFactors[samplesFFT] = {0.0f}; +#endif + +// Create FFT object #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and an a few other speedups @@ -116,58 +182,20 @@ static void autoResetPeak(void); // peak auto-reset function #endif #include -// FFT Output variables shared with animations -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects - -// 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. - -// These are the input and output vectors. Input vectors receive computed results from FFT. -static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins -static float vImag[samplesFFT] = {0.0f}; // imaginary parts - -// the following are observed values, supported by a bit of "educated guessing" -//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels -#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels -#define LOG_256 5.54517744 - -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static float windowWeighingFactors[samplesFFT] = {0.0f}; -#endif - -// Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; -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 - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; -#endif - -// 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 }; - -// Create FFT object #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); #else static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); #endif -static TaskHandle_t FFT_Task = nullptr; +// 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 resut bins static float fftAddAvg(int from, int to) { float result = 0.0f; for (int i = from; i <= to; i++) { @@ -176,7 +204,9 @@ static float fftAddAvg(int from, int to) { return result / float(to - from + 1); } +// // FFT main task +// void FFTcode(void * parameter) { DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); @@ -213,6 +243,10 @@ void FFTcode(void * parameter) xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + // band pass filter - can reduce noise floor by a factor of 50 + // downside: frequencies below 100Hz will be ignored + if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + // find highest sample in the batch float maxSample = 0.0f; // max sample from FFT batch for (int i=0; i < samplesFFT; i++) { @@ -229,7 +263,7 @@ void FFTcode(void * parameter) #ifdef SR_DEBUG if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization #else - if (sampleAvg > 0.5f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. + if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. #endif // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) @@ -273,7 +307,7 @@ void FFTcode(void * parameter) } // for() // mapping of FFT result bins to frequency channels - if (sampleAvg > 0.5f) { // noise gate open + 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. * @@ -303,10 +337,22 @@ void FFTcode(void * parameter) #else /* new mapping, optimized for 22050 Hz by softhack007 */ // bins frequency range - 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 + 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 @@ -318,8 +364,6 @@ void FFTcode(void * parameter) 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 - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) #endif } else { // noise gate closed - just decay old values for (int i=0; i < NUM_GEQ_CHANNELS; i++) { @@ -329,9 +373,67 @@ void FFTcode(void * parameter) } // post-processing of frequency channels (pink noise adjustment, AGC, smooting, scaling) - for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); - if (sampleAvg > 0.5f) { // noise gate open +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows + uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth + } +#endif + // run peak detection + autoResetPeak(); + detectSamplePeak(); + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC + #endif + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + + } // for(;;)ever +} // FFTcode() task end + + +/////////////////////////// +// Pre / Postprocessing // +/////////////////////////// + +static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) +{ + // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency + //constexpr float alpha = 0.04f; // 150Hz + //constexpr float alpha = 0.03f; // 110Hz + constexpr float alpha = 0.0225f; // 80hz + //constexpr float alpha = 0.01693f;// 60hz + // high frequency cutoff parameter + //constexpr float beta1 = 0.75f; // 11Khz + //constexpr float beta1 = 0.82f; // 15Khz + //constexpr float beta1 = 0.8285f; // 18Khz + constexpr float beta1 = 0.85f; // 20Khz + + constexpr float beta2 = (1.0f - beta1) / 2.0; + static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter + static float lowfilt = 0.0f; // IIR low frequency cutoff filter + + for (int i=0; i < numSamples; i++) { + // FIR lowpass, to remove high frequency noise + float highFilteredSample; + if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // spcial handling for last sample in array + last_vals[1] = last_vals[0]; + last_vals[0] = sampleBuffer[i]; + sampleBuffer[i] = highFilteredSample; + // IIR highpass, to remove low frequency noise + lowfilt += alpha * (sampleBuffer[i] - lowfilt); + sampleBuffer[i] = sampleBuffer[i] - lowfilt; + } +} + +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels +{ + for (int i=0; i < numberOfChannels; i++) { + + if (noiseGateOpen) { // noise gate open // Adjustment for frequency curves. fftCalc[i] *= fftResultPink[i]; if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function @@ -401,36 +503,23 @@ void FFTcode(void * parameter) } fftResult[i] = constrain((int)currentResult, 0, 255); } - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows - uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth - } -#endif - // run peak detection - autoResetPeak(); - detectSamplePeak(); - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC - #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - - } // for(;;)ever -} // FFTcode() task end - - +} //////////////////// // Peak detection // //////////////////// // peak detection is called from FFT task when vReal[] contains valid FFT results static void detectSamplePeak(void) { + bool havePeak = false; + // 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 > 1) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { - // 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. + havePeak = true; + } + + if (havePeak) { samplePeak = true; timeOfPeak = millis(); udpSamplePeak = true; @@ -459,10 +548,11 @@ class AudioReactive : public Usermod { #else int8_t audioPin = AUDIOPIN; #endif - #ifndef DMTYPE // I2S mic type + #ifndef SR_DMTYPE // I2S mic type uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S + #define SR_DMTYPE 1 // default type = I2S #else - uint8_t dmType = DMTYPE; + uint8_t dmType = SR_DMTYPE; #endif #ifndef I2S_SDPIN // aka DOUT int8_t i2ssdPin = 32; @@ -526,7 +616,6 @@ class AudioReactive : public Usermod { // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) - bool udpSyncConnected = false;// UDP connection status -> true if connected to multicast group unsigned long lastTime = 0; // last time of running UDP Microphone Sync const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED uint16_t audioSyncPort= 11988;// default port for UDP sound sync @@ -538,15 +627,14 @@ class AudioReactive : public Usermod { // 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 controler. - float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller + double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller float expAdjF = 0.0f; // Used for exponential filter. float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) int16_t rawSampleAgc = 0; // not smoothed AGC sample - float sampleAgc = 0.0f; // Smoothed AGC sample // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample + 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 @@ -576,28 +664,28 @@ class AudioReactive : public Usermod { if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable #ifdef MIC_LOGGER // Debugging functions for audio input and sound processing. Comment out the values you want to see - Serial.print("micReal:"); Serial.print(micDataReal); Serial.print("\t"); - Serial.print("volumeSmth:"); Serial.print(volumeSmth); Serial.print("\t"); - //Serial.print("volumeRaw:"); Serial.print(volumeRaw); Serial.print("\t"); - //Serial.print("DC_Level:"); Serial.print(micLev); Serial.print("\t"); - //Serial.print("sampleAgc:"); Serial.print(sampleAgc); Serial.print("\t"); - //Serial.print("sampleAvg:"); Serial.print(sampleAvg); Serial.print("\t"); - //Serial.print("sampleReal:"); Serial.print(sampleReal); Serial.print("\t"); - //Serial.print("micIn:"); Serial.print(micIn); Serial.print("\t"); - //Serial.print("sample:"); Serial.print(sample); Serial.print("\t"); - //Serial.print("sampleMax:"); Serial.print(sampleMax); Serial.print("\t"); - //Serial.print("samplePeak:"); Serial.print((samplePeak!=0) ? 128:0); Serial.print("\t"); - //Serial.print("multAgc:"); Serial.print(multAgc, 4); Serial.print("\t"); - Serial.println(); + PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); + PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); + PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + //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"); + PLOT_PRINTLN(); #endif #ifdef FFT_SAMPLING_LOG #if 0 for(int i=0; i 0.5f)) { sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax); // new peak - with some filtering + // another simple way to detect samplePeak + if ((binNum < 10) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } } else { if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly @@ -1015,11 +1110,14 @@ class AudioReactive : public Usermod { } // Reset I2S peripheral for good measure - i2s_driver_uninstall(I2S_NUM_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 delay(100); // Give that poor microphone some time to setup. + + useBandPassFilter = false; 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 @@ -1048,14 +1146,15 @@ class AudioReactive : public Usermod { break; case 4: DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) case 5: - DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); + DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); + useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); break; @@ -1079,7 +1178,11 @@ class AudioReactive : public Usermod { if (enabled) disableSoundProcessing = false; // all good - enable audio processing if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync + #ifdef WLED_DEBUG + DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #else DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #endif disableSoundProcessing = true; } @@ -1353,10 +1456,11 @@ class AudioReactive : public Usermod { if (enabled) { // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running - if (soundAgc > 0) + if (soundAgc > 0) { infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies - else + } else { infoArr = user.createNestedArray(F("Audio Input Level")); + } uiDomString = F("
= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow infoArr.add("! ms"); else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability @@ -1541,8 +1645,10 @@ class AudioReactive : public Usermod { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); amic["pin"] = audioPin; + #endif JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); dmic[F("type")] = dmType; @@ -1595,9 +1701,20 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); + #else + audioPin = -1; // MCU does not support analog mic + #endif configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + #endif + #endif + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index c76b2bb0..16bbbb65 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -23,11 +23,15 @@ // 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_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) +#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) // there are two things in these MCUs that could lead to problems with audio processing: // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) // * single core, so FFT task might slow down other things like LED updates + #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) + #error This audio reactive usermod does not support ESP32-C2, ESP32-C3 or ESP32-S2. + #else #warning This audio reactive usermod does not support ESP32-C2, ESP32-C3 or ESP32-S2. + #endif #endif /* ToDo: remove. ES7243 is controlled via compiler defines @@ -76,11 +80,15 @@ #ifdef I2S_USE_RIGHT_CHANNEL #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT #define I2S_MIC_CHANNEL_TEXT "right channel only (work-around swapped channel bug in IDF 4.4)." +#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT +#define I2S_PDM_MIC_CHANNEL_TEXT "right channel only" #else //#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ALL_LEFT //#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_RIGHT_LEFT #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT #define I2S_MIC_CHANNEL_TEXT "left channel only (work-around swapped channel bug in IDF 4.4)." +#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT +#define I2S_PDM_MIC_CHANNEL_TEXT "left channel only." #endif #else @@ -92,6 +100,9 @@ #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT #define I2S_MIC_CHANNEL_TEXT "left channel only." #endif +#define I2S_PDM_MIC_CHANNEL I2S_MIC_CHANNEL +#define I2S_PDM_MIC_CHANNEL_TEXT I2S_MIC_CHANNEL_TEXT + #endif @@ -138,15 +149,17 @@ 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) : + AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) : _sampleRate(sampleRate), _blockSize(blockSize), - _initialized(false) + _initialized(false), + _sampleScale(sampleScale) {}; SRate_t _sampleRate; // Microphone sampling rate int _blockSize; // I2S block size bool _initialized; // Gets set to true if initialization is successful + float _sampleScale; // pre-scaling factor for I2S samples }; /* Basic I2S microphone source @@ -154,8 +167,8 @@ class AudioSource { */ class I2SSource : public AudioSource { public: - I2SSource(SRate_t sampleRate, int blockSize) : - AudioSource(sampleRate, blockSize) { + I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + AudioSource(sampleRate, blockSize, sampleScale) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = _sampleRate, @@ -195,18 +208,51 @@ class I2SSource : public AudioSource { return; } } else { + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) + #warning this MCU does not support PDM microphones + #endif + #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) // This is an I2S PDM microphone, these microphones only use a clock and - // data line, to make it simpler to debug, use the WS pin as CLK and SD - // pin as DATA + // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA + // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c + + // note to self: PDM has known bugs on S3, and does not work on C3 + // * S3: PDM sample rate only at 50% of expected rate: https://github.com/espressif/esp-idf/issues/9893 + // * S3: I2S PDM has very low amplitude: https://github.com/espressif/esp-idf/issues/8660 + // * C3: does not support PDM to PCM input. SoC would allow PDM RX, but there is no hardware to directly convert to PCM so it will not work. https://github.com/espressif/esp-idf/issues/8796 + _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3 + _config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel. + _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality #endif } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + if (mclkPin != I2S_PIN_NO_CHANGE) { + _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches. + // //_config.fixed_mclk = 512 * _sampleRate; + // //_config.fixed_mclk = 256 * _sampleRate; + } + + #if !defined(SOC_I2S_SUPPORTS_APLL) + #warning this MCU does not have an APLL high accuracy clock for audio + // S3: not supported; S2: supported; C3: not supported + _config.use_apll = false; // APLL not supported on this MCU + #endif + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 + #endif +#endif + // Reserve the master clock pin if provided _mclkPin = mclkPin; if (mclkPin != I2S_PIN_NO_CHANGE) { - if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) return; + if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { + DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); + return; + } else _routeMclk(mclkPin); } @@ -220,15 +266,25 @@ class I2SSource : public AudioSource { .data_in_num = i2ssdPin }; + //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); + esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); + DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); return; } + DEBUGSR_PRINTF("AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\n", _config.use_apll? "uses":"without", _config.fixed_mclk); + DEBUGSR_PRINTF("AR: %d bits, Sample scaling factor = %6.4f\n", _config.bits_per_sample, _sampleScale); + if (_config.mode & I2S_MODE_PDM) { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in PDM MASTER mode.")); + } else { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); + } + err = i2s_set_pin(I2S_NUM_0, &_pinConfig); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to set i2s pin config: %d\n", err); + DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver return; } @@ -236,7 +292,7 @@ class I2SSource : public AudioSource { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to configure i2s clocks: %d\n", err); + DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver return; } @@ -288,6 +344,7 @@ class I2SSource : public AudioSource { currSample = (float) newSamples[i]; // 16bit input -> use as-is #endif buffer[i] = currSample; + buffer[i] *= _sampleScale; // scale samples } } } @@ -328,18 +385,25 @@ class ES7243 : public I2SSource { private: // I2C initialization functions for ES7243 void _es7243I2cBegin() { - Wire.begin(pin_ES7243_SDA, pin_ES7243_SCL, 100000U); + bool i2c_initialized = Wire.begin(pin_ES7243_SDA, pin_ES7243_SCL, 100000U); + if (i2c_initialized == false) { + DEBUGSR_PRINTLN(F("AR: ES7243 failed to initialize I2C bus driver.")); + } } void _es7243I2cWrite(uint8_t reg, uint8_t val) { #ifndef ES7243_ADDR Wire.beginTransmission(0x13); + #define ES7243_ADDR 0x13 // default address #else Wire.beginTransmission(ES7243_ADDR); #endif Wire.write((uint8_t)reg); Wire.write((uint8_t)val); - Wire.endTransmission(); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: ES7243 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES7243_ADDR, reg, val); + } } void _es7243InitAdc() { @@ -353,15 +417,28 @@ class ES7243 : public I2SSource { } public: - ES7243(SRate_t sampleRate, int blockSize) : - I2SSource(sampleRate, blockSize) { + ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; }; void initialize(int8_t sdaPin, int8_t sclPin, int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + // check that pins are valid + if ((sdaPin < 0) || (sclPin < 0)) { + DEBUGSR_PRINTF("\nAR: invalid ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); + return; + } + + if ((i2sckPin < 0) || (mclkPin < 0)) { + DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + return; + } + // Reserve SDA and SCL pins of the I2C interface - if (!pinManager.allocatePin(sdaPin, true, PinOwner::HW_I2C) || - !pinManager.allocatePin(sclPin, true, PinOwner::HW_I2C)) { + PinManagerPinType es7243Pins[2] = { { sdaPin, true }, { sclPin, true } }; + if (!pinManager.allocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C)) { + pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); + DEBUGSR_PRINTF("\nAR: Failed to allocate ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); return; } @@ -375,8 +452,8 @@ public: void deinitialize() { // Release SDA and SCL pins of the I2C interface - pinManager.deallocatePin(pin_ES7243_SDA, PinOwner::HW_I2C); - pinManager.deallocatePin(pin_ES7243_SCL, PinOwner::HW_I2C); + PinManagerPinType es7243Pins[2] = { { pin_ES7243_SDA, true }, { pin_ES7243_SCL, true } }; + pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); I2SSource::deinitialize(); } @@ -385,6 +462,13 @@ public: int8_t pin_ES7243_SCL; }; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) +#if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC) + #warning this MCU does not support analog sound input +#endif +#endif + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // ADC over I2S is only availeable in "classic" ESP32 @@ -395,8 +479,8 @@ public: */ class I2SAdcSource : public I2SSource { public: - I2SAdcSource(SRate_t sampleRate, int blockSize) : - I2SSource(sampleRate, blockSize) { + I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), .sample_rate = _sampleRate, @@ -430,7 +514,7 @@ class I2SAdcSource : public I2SSource { // Determine Analog channel. Only Channels on ADC1 are supported int8_t channel = digitalPinToAnalogChannel(_audioPin); if (channel > 9) { - DEBUGSR_PRINTF("Incompatible GPIO used for audio in: %d\n", _audioPin); + DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin); return; } else { adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); @@ -465,11 +549,12 @@ class I2SAdcSource : public I2SSource { //return; } #else - err = i2s_adc_disable(I2S_NUM_0); - //err = i2s_stop(I2S_NUM_0); - if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to initially disable i2s adc: %d\n", err); - } + // bugfix: do not disable ADC initially - its already disabled after driver install. + //err = i2s_adc_disable(I2S_NUM_0); + // //err = i2s_stop(I2S_NUM_0); + //if (err != ESP_OK) { + // DEBUGSR_PRINTF("Failed to initially disable i2s adc: %d\n", err); + //} #endif _initialized = true; @@ -585,8 +670,8 @@ 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) : - I2SSource(sampleRate, blockSize) + SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : + I2SSource(sampleRate, blockSize, sampleScale) {} void initialize(uint8_t i2swsPin, uint8_t i2ssdPin, uint8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index f14c168d..f11e219e 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -1,36 +1,73 @@ # Audioreactive usermod This usermod allows controlling LEDs using audio input. Audio input can be either microphone or analog-in (AUX) using appropriate adapter. -Supported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...). +Supported microphones range from cheap analog (MAX4466, MAX9814, ...) to high quality digital (INMP441, ICS-43434, ...) and dgital Line-In. The usermod does audio processing and provides data structure that specially written effect can use. The usermod **does not** provide effects or draws anything to LED strip/matrix. +## Additional Documentation +This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): +* [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) +* [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. +* [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) +* [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) +* [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) + + +## Supported MCUs +This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. + +It will compile succesfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. + +Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. + +Currently ESP8266 is not supported, due to low speed and small RAM of this chip. +There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation -Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment as well as `arduinoFFT` to your `lib_deps`. +### using customised _arduinoFFT_ library for use with this usermod +Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git +### using latest (develop) _arduinoFFT_ library +Alternatively, you can use the latest arduinoFFT development version. +ArduinoFFT `develop` library is slightly more accurate, and slighly faster than our customised library, however also needs additional 2kB RAM. + +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` +* `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` + ## Configuration All parameters are runtime configurable though some may require hard boot after change (I2S microphone or selected GPIOs). -If you want to define default GPIOs during compile time use the following (default values in parentheses): +If you want to define default GPIOs during compile time use the following addtional build_flags (default values in parentheses): -- `DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S, 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S -- `AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) -- `I2S_SDPIN=x` : GPIO for SD pin on digital mcrophone (32) -- `I2S_WSPIN=x` : GPIO for WS pin on digital mcrophone (15) -- `I2S_CKPIN=x` : GPIO for SCK pin on digital mcrophone (14) -- `ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) -- `ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) -- `MCLK_PIN=x` : GPIO for master clock pin on digital mcrophone (-1) +- `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S +- `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) +- `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) +- `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) +- `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) +- `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) +- `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) +- `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) **NOTE** Due to the fact that usermod uses I2S peripherial for analog audio sampling, use of analog *buttons* (i.e. potentiometers) is disabled while running this usermod with analog microphone. +### Advanced Compile-Time Options +You can use the following additional flags in your `build_flags` +* `-D SR_SQUELCH=x` : Default "squelch" setting (10) +* `-D SR_GAIN=x` : Default "gain" setting (60) +* `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). +* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). +* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. +* `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) +* `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. + ## Release notes -2022-06 Ported from [soundreactive](https://github.com/atuline/WLED) by @blazoncek (AKA Blaz Kristan) +* 2022-06 Ported from [soundreactive WLED](https://github.com/atuline/WLED) - by @blazoncek (AKA Blaz Kristan) and the [SR-WLED team](https://github.com/atuline/WLED/wiki#sound-reactive-wled-fork-team). +* 2022-11 Updated to align with "[MoonModules/WLED](https://amg.wled.me)" audioreactive usermod - by @softhack007 (AKA Frank Möhle).