2022-06-17 16:24:25 +02:00
# pragma once
# include "wled.h"
# include <driver/i2s.h>
2022-07-10 22:47:42 +02:00
# include <driver/adc.h>
2022-06-17 16:24:25 +02:00
# ifndef ESP32
# error This audio reactive usermod does not support the ESP8266.
# endif
2022-07-04 13:58:25 +02:00
# ifdef WLED_DEBUG
# include <esp_timer.h>
# endif
2022-06-17 16:24:25 +02:00
/*
* Usermods allow you to add own functionality to WLED more easily
* See : https : //github.com/Aircoookie/WLED/wiki/Add-own-functionality
*
* This is an audioreactive v2 usermod .
* . . . .
*/
// Comment/Uncomment to toggle usb serial debugging
2022-08-06 17:24:39 +02:00
// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter)
// #define FFT_SAMPLING_LOG // FFT result debugging
2022-08-14 13:58:07 +02:00
// #define SR_DEBUG // generic SR DEBUG messages (including MIC_LOGGER)
// #define NO_MIC_LOGGER // exclude MIC_LOGGER from SR_DEBUG
2022-08-06 17:24:39 +02:00
// hackers corner
2022-08-14 13:58:07 +02:00
# if !defined(SOUND_DYNAMICS_LIMITER) && !defined(NO_SOUND_DYNAMICS_LIMITER)
# define SOUND_DYNAMICS_LIMITER // experimental: define to enable a dynamics limiter that avoids "sudden flashes" at onsets. Makes some effects look more "smooth and fluent"
# endif
2022-08-06 17:24:39 +02:00
2022-06-17 16:24:25 +02:00
# ifdef SR_DEBUG
# define DEBUGSR_PRINT(x) Serial.print(x)
# define DEBUGSR_PRINTLN(x) Serial.println(x)
# define DEBUGSR_PRINTF(x...) Serial.printf(x)
# else
# define DEBUGSR_PRINT(x)
# define DEBUGSR_PRINTLN(x)
# define DEBUGSR_PRINTF(x...)
# endif
2022-08-06 17:24:39 +02:00
// legacy support
2022-08-15 14:28:51 +02:00
// #if defined(SR_DEBUG) && !defined(MIC_LOGGER) && !defined(NO_MIC_LOGGER)
// #define MIC_LOGGER
// #endif
2022-08-06 17:24:39 +02:00
2022-06-17 16:24:25 +02:00
# include "audio_source.h"
constexpr i2s_port_t I2S_PORT = I2S_NUM_0 ;
constexpr int BLOCK_SIZE = 128 ;
2022-08-07 22:04:26 +02:00
//constexpr int SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms
constexpr int SAMPLE_RATE = 20480 ; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms
//constexpr int SAMPLE_RATE = 10240; // Base sample rate in Hz - standard. Physical sample time -> 50ms
2022-06-17 16:24:25 +02:00
2022-08-07 22:19:38 +02:00
# define FFT_MIN_CYCLE 22 // minimum time before FFT task is repeated. Must be less than time needed to read 512 samples at SAMPLE_RATE -> not the same as I2S time!!
2022-06-17 16:24:25 +02:00
// globals
2022-06-19 19:15:34 +02:00
static uint8_t inputLevel = 128 ; // UI slider value
static uint8_t soundSquelch = 10 ; // squelch value for volume reactive routines (config value)
2022-07-11 14:30:03 +02:00
static uint8_t sampleGain = 60 ; // sample gain (config value)
2022-06-19 19:15:34 +02:00
static uint8_t soundAgc = 0 ; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value)
2022-06-20 22:17:01 +02:00
static uint8_t audioSyncEnabled = 0 ; // bit field: bit 0 - send, bit 1 - receive (config value)
2022-06-17 16:24:25 +02:00
2022-08-14 13:58:07 +02:00
// user settable parameters for limitSoundDynamics()
static int attackTime = 80 ; // int: attack time in milliseconds. Default 0.1sec
static int decayTime = 1400 ; // int: decay time in milliseconds. Default 1.4sec
2022-06-17 16:24:25 +02:00
//
// AGC presets
// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const"
//
2022-06-19 19:15:34 +02:00
# define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy
2022-07-04 12:34:32 +02:00
const double agcSampleDecay [ AGC_NUM_PRESETS ] = { 0.9994f , 0.9985f , 0.9997f } ; // decay factor for sampleMax, in case the current sample is below sampleMax
2022-08-06 16:48:26 +02:00
const float agcZoneLow [ AGC_NUM_PRESETS ] = { 32 , 28 , 36 } ; // low volume emergency zone
const float agcZoneHigh [ AGC_NUM_PRESETS ] = { 240 , 240 , 248 } ; // high volume emergency zone
const float agcZoneStop [ AGC_NUM_PRESETS ] = { 336 , 448 , 304 } ; // disable AGC integrator if we get above this level
const float agcTarget0 [ AGC_NUM_PRESETS ] = { 112 , 144 , 164 } ; // first AGC setPoint -> between 40% and 65%
const float agcTarget0Up [ AGC_NUM_PRESETS ] = { 88 , 64 , 116 } ; // setpoint switching value (a poor man's bang-bang)
const float agcTarget1 [ AGC_NUM_PRESETS ] = { 220 , 224 , 216 } ; // second AGC setPoint -> around 85%
2022-07-04 12:34:32 +02:00
const double agcFollowFast [ AGC_NUM_PRESETS ] = { 1 / 192.f , 1 / 128.f , 1 / 256.f } ; // quickly follow setpoint - ~0.15 sec
const double agcFollowSlow [ AGC_NUM_PRESETS ] = { 1 / 6144.f , 1 / 4096.f , 1 / 8192.f } ; // slowly follow setpoint - ~2-15 secs
const double agcControlKp [ AGC_NUM_PRESETS ] = { 0.6f , 1.5f , 0.65f } ; // AGC - PI control, proportional gain parameter
const double agcControlKi [ AGC_NUM_PRESETS ] = { 1.7f , 1.85f , 1.2f } ; // AGC - PI control, integral gain parameter
2022-07-10 22:47:42 +02:00
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)
2022-06-17 16:24:25 +02:00
// AGC presets end
2022-06-19 19:15:34 +02:00
static AudioSource * audioSource = nullptr ;
2022-07-10 22:47:42 +02:00
static volatile bool disableSoundProcessing = false ; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks.
2022-06-19 19:15:34 +02:00
2022-08-08 10:51:46 +02:00
static float micDataReal = 0.0f ; // MicIn data with full 24bit resolution - lowest 8bit after decimal point
2022-06-19 19:15:34 +02:00
static float multAgc = 1.0f ; // sample * multAgc = sampleAgc. Our AGC multiplier
2022-06-17 16:24:25 +02:00
////////////////////
// Begin FFT Code //
////////////////////
2022-07-04 17:09:20 +02:00
# 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
# define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt
2022-08-08 13:53:46 +02:00
# define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION)
2022-07-04 17:09:20 +02:00
# endif
2022-06-17 16:24:25 +02:00
# include "arduinoFFT.h"
// FFT Variables
constexpr uint16_t samplesFFT = 512 ; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2
2022-08-14 13:58:07 +02:00
constexpr uint16_t samplesFFT_2 = 256 ; // meaningfull part of FFT results - only the "lower half" contains useful information.
2022-06-17 16:24:25 +02:00
2022-07-03 23:00:32 +02:00
static float FFT_MajorPeak = 0.0f ;
static float FFT_Magnitude = 0.0f ;
2022-06-17 16:24:25 +02:00
// These are the input and output vectors. Input vectors receive computed results from FFT.
2022-08-16 12:02:22 +02:00
static float vReal [ samplesFFT ] = { 0.0f } ;
static float vImag [ samplesFFT ] = { 0.0f } ;
static float fftBin [ samplesFFT_2 ] = { 0.0f } ;
2022-06-17 16:24:25 +02:00
2022-07-04 17:09:20 +02:00
# ifdef UM_AUDIOREACTIVE_USE_NEW_FFT
2022-08-16 12:02:22 +02:00
static float windowWeighingFactors [ samplesFFT ] = { 0.0f } ;
2022-07-04 17:09:20 +02:00
# endif
2022-06-17 16:24:25 +02:00
// Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256.
// Oh, and bins 0,1,2 are no good, so we'll zero them out.
2022-08-16 12:02:22 +02:00
static float fftCalc [ 16 ] = { 0.0f } ;
static uint8_t fftResult [ 16 ] = { 0 } ; // Our calculated result table, which we feed to the animations.
2022-06-17 16:24:25 +02:00
# ifdef SR_DEBUG
2022-08-16 12:02:22 +02:00
static float fftResultMax [ 16 ] = { 0.0f } ; // A table used for testing to determine how our post-processing is working.
2022-06-17 16:24:25 +02:00
# endif
2022-08-16 12:02:22 +02:00
static float fftAvg [ 16 ] = { 0.0f } ;
2022-06-17 16:24:25 +02:00
2022-07-03 23:00:32 +02:00
# ifdef WLED_DEBUG
static unsigned long fftTime = 0 ;
static unsigned long sampleTime = 0 ;
# endif
2022-06-17 16:24:25 +02:00
// Table of linearNoise results to be multiplied by soundSquelch in order to reduce squelch across fftResult bins.
static uint8_t linearNoise [ 16 ] = { 34 , 28 , 26 , 25 , 20 , 12 , 9 , 6 , 4 , 4 , 3 , 2 , 2 , 2 , 2 , 2 } ;
// Table of multiplication factors so that we can even out the frequency response.
static float fftResultPink [ 16 ] = { 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
2022-07-04 17:09:20 +02:00
# ifdef UM_AUDIOREACTIVE_USE_NEW_FFT
static ArduinoFFT < float > FFT = ArduinoFFT < float > ( vReal , vImag , samplesFFT , SAMPLE_RATE , windowWeighingFactors ) ;
# else
2022-06-17 16:24:25 +02:00
static arduinoFFT FFT = arduinoFFT ( vReal , vImag , samplesFFT , SAMPLE_RATE ) ;
2022-07-04 17:09:20 +02:00
# endif
2022-07-06 19:42:48 +02:00
static TaskHandle_t FFT_Task = nullptr ;
2022-06-17 16:24:25 +02:00
2022-08-16 12:02:22 +02:00
static float fftAddAvg ( int from , int to ) {
2022-06-17 16:24:25 +02:00
float result = 0.0f ;
for ( int i = from ; i < = to ; i + + ) {
result + = fftBin [ i ] ;
}
2022-07-03 23:00:32 +02:00
return result / float ( to - from + 1 ) ;
2022-06-17 16:24:25 +02:00
}
// FFT main code
void FFTcode ( void * parameter )
{
2022-08-07 22:19:38 +02:00
DEBUGSR_PRINT ( " FFT started on core: " ) ; DEBUGSR_PRINTLN ( xPortGetCoreID ( ) ) ;
// see https://www.freertos.org/vtaskdelayuntil.html
const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS ;
2022-08-08 13:53:46 +02:00
//const TickType_t xFrequency_2 = (FFT_MIN_CYCLE * portTICK_PERIOD_MS) / 2;
2022-06-17 16:24:25 +02:00
for ( ; ; ) {
2022-08-08 13:53:46 +02:00
TickType_t xLastWakeTime = xTaskGetTickCount ( ) ;
2022-06-17 16:24:25 +02:00
delay ( 1 ) ; // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy.
// taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work.
2022-07-10 22:47:42 +02:00
// Only run the FFT computing code if we're not in Receive mode and not in realtime mode
if ( disableSoundProcessing | | ( audioSyncEnabled & 0x02 ) ) {
2022-08-07 22:19:38 +02:00
//delay(7); // release CPU - delay is implemeted using vTaskDelay(). cannot use yield() because we are out of arduino loop context
vTaskDelayUntil ( & xLastWakeTime , xFrequency ) ; // release CPU, by doing nothing for FFT_MIN_CYCLE millis
2022-07-10 22:47:42 +02:00
continue ;
}
2022-06-17 16:24:25 +02:00
2022-08-08 13:53:46 +02:00
vTaskDelayUntil ( & xLastWakeTime , xFrequency ) ; // release CPU, and let I2S fill its buffers
//vTaskDelayUntil( &xLastWakeTime, xFrequency_2); // release CPU, and let I2S fill its buffers
2022-07-03 23:00:32 +02:00
# ifdef WLED_DEBUG
2022-07-04 13:58:25 +02:00
uint64_t start = esp_timer_get_time ( ) ;
2022-07-03 23:00:32 +02:00
# endif
2022-06-17 16:24:25 +02:00
if ( audioSource ) audioSource - > getSamples ( vReal , samplesFFT ) ;
2022-07-03 23:00:32 +02:00
# ifdef WLED_DEBUG
2022-07-04 13:58:25 +02:00
if ( start < esp_timer_get_time ( ) ) { // filter out overflows
unsigned long sampleTimeInMillis = ( esp_timer_get_time ( ) - start + 500ULL ) / 1000ULL ; // "+500" to ensure proper rounding
sampleTime = ( sampleTimeInMillis * 3 + sampleTime * 7 ) / 10 ; // smooth
}
2022-07-03 23:00:32 +02:00
# endif
2022-06-17 16:24:25 +02:00
const int halfSamplesFFT = samplesFFT / 2 ; // samplesFFT divided by 2
float maxSample1 = 0.0f ; // max sample from first half of FFT batch
float maxSample2 = 0.0f ; // max sample from second half of FFT batch
for ( int i = 0 ; i < halfSamplesFFT ; i + + ) {
// set imaginary parts to 0
vImag [ i ] = 0 ;
// pick our our current mic sample - we take the max value from all samples that go into FFT
if ( ( vReal [ i ] < = ( INT16_MAX - 1024 ) ) & & ( vReal [ i ] > = ( INT16_MIN + 1024 ) ) ) //skip extreme values - normally these are artefacts
if ( fabsf ( ( float ) vReal [ i ] ) > maxSample1 ) maxSample1 = fabsf ( ( float ) vReal [ i ] ) ;
}
for ( int i = halfSamplesFFT ; i < samplesFFT ; i + + ) {
// set imaginary parts to 0
vImag [ i ] = 0 ;
// pick our our current mic sample - we take the max value from all samples that go into FFT
if ( ( vReal [ i ] < = ( INT16_MAX - 1024 ) ) & & ( vReal [ i ] > = ( INT16_MIN + 1024 ) ) ) //skip extreme values - normally these are artefacts
if ( fabsf ( ( float ) vReal [ i ] ) > maxSample2 ) maxSample2 = fabsf ( ( float ) vReal [ i ] ) ;
}
// release first sample to volume reactive effects
micDataReal = maxSample1 ;
2022-07-04 17:09:20 +02:00
# ifdef UM_AUDIOREACTIVE_USE_NEW_FFT
FFT . dcRemoval ( ) ; // remove DC offset
FFT . windowing ( FFTWindow : : Flat_top , FFTDirection : : Forward ) ; // Weigh data
FFT . compute ( FFTDirection : : Forward ) ; // Compute FFT
FFT . complexToMagnitude ( ) ; // Compute magnitudes
# else
2022-06-17 16:24:25 +02:00
FFT . DCRemoval ( ) ; // let FFT lib remove DC component, so we don't need to care about this in getSamples()
//FFT.Windowing( FFT_WIN_TYP_HAMMING, FFT_FORWARD ); // Weigh data - standard Hamming window
//FFT.Windowing( FFT_WIN_TYP_BLACKMAN, FFT_FORWARD ); // Blackman window - better side freq rejection
//FFT.Windowing( FFT_WIN_TYP_BLACKMAN_HARRIS, FFT_FORWARD );// Blackman-Harris - excellent sideband rejection
FFT . Windowing ( FFT_WIN_TYP_FLT_TOP , FFT_FORWARD ) ; // Flat Top Window - better amplitude accuracy
FFT . Compute ( FFT_FORWARD ) ; // Compute FFT
FFT . ComplexToMagnitude ( ) ; // Compute magnitudes
2022-07-04 17:09:20 +02:00
# endif
2022-06-17 16:24:25 +02:00
//
// vReal[3 .. 255] contain useful data, each a 20Hz interval (60Hz - 5120Hz).
// There could be interesting data at bins 0 to 2, but there are too many artifacts.
//
2022-07-04 17:09:20 +02:00
# ifdef UM_AUDIOREACTIVE_USE_NEW_FFT
FFT . majorPeak ( FFT_MajorPeak , FFT_Magnitude ) ; // let the effects know which freq was most dominant
# else
2022-06-17 16:24:25 +02:00
FFT . MajorPeak ( & FFT_MajorPeak , & FFT_Magnitude ) ; // let the effects know which freq was most dominant
2022-07-04 17:09:20 +02:00
# endif
2022-06-17 16:24:25 +02:00
2022-08-10 18:14:28 +02:00
for ( int i = 0 ; i < samplesFFT_2 ; i + + ) { // Values for bins 0 and 1 are WAY too large. Might as well start at 3.
2022-06-17 16:24:25 +02:00
float t = fabs ( vReal [ i ] ) ; // just to be sure - values in fft bins should be positive any way
2022-07-03 23:00:32 +02:00
fftBin [ i ] = t / 16.0f ; // Reduce magnitude. Want end result to be linear and ~4096 max.
2022-06-17 16:24:25 +02:00
} // for()
/* 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 60 Hz and multiply by 1.320367784 to get the next frequency and so on until the end . Then detetermine the bins .
* End frequency = Start frequency * multiplier ^ 16
* Multiplier = ( End frequency / Start frequency ) ^ 1 / 16
* Multiplier = 1.320367784
*/
2022-07-03 23:00:32 +02:00
// Range
fftCalc [ 0 ] = fftAddAvg ( 3 , 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
2022-08-06 16:48:26 +02:00
fftCalc [ 7 ] = fftAddAvg ( 21 , 29 ) ; // 420 - 600
2022-07-03 23:00:32 +02:00
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 , 255 ) ; // 3880 - 5120
2022-06-17 16:24:25 +02:00
for ( int i = 0 ; i < 16 ; i + + ) {
// Noise supression of fftCalc bins using soundSquelch adjustment for different input types.
fftCalc [ i ] = ( fftCalc [ i ] < ( ( float ) soundSquelch * ( float ) linearNoise [ i ] / 4.0f ) ) ? 0 : fftCalc [ i ] ;
// Adjustment for frequency curves.
fftCalc [ i ] * = fftResultPink [ i ] ;
// 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 ) ; //with inputLevel adjustment
2022-08-15 14:28:51 +02:00
// 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
fftAvg [ i ] = fftCalc [ i ] * 0.1f + 0.9f * fftAvg [ i ] ; // will need approx 5 cycles (150ms) for converging against fftCalc[i]
//fftAvg[i] = fftCalc[i]*0.05f + 0.95f*fftAvg[i]; // will need approx 10 cycles (250ms) for converging against fftCalc[i]
2022-06-17 16:24:25 +02:00
// Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely.
2022-08-15 14:28:51 +02:00
# if !defined(SOUND_DYNAMICS_LIMITER)
fftResult [ i ] = constrain ( ( int ) fftCalc [ i ] , 0 , 254 ) ;
# else
2022-08-14 13:58:07 +02:00
fftResult [ i ] = constrain ( ( int ) fftAvg [ i ] , 0 , 254 ) ;
2022-08-15 14:28:51 +02:00
# endif
2022-06-17 16:24:25 +02:00
}
2022-07-03 23:00:32 +02:00
# ifdef WLED_DEBUG
2022-07-04 13:58:25 +02:00
if ( start < esp_timer_get_time ( ) ) { // filter out overflows
unsigned long fftTimeInMillis = ( ( esp_timer_get_time ( ) - start ) + 500ULL ) / 1000ULL ; // "+500" to ensure proper rounding
fftTime = ( fftTimeInMillis * 3 + fftTime * 7 ) / 10 ; // smooth
}
2022-07-03 23:00:32 +02:00
# endif
2022-08-08 13:53:46 +02:00
//vTaskDelayUntil( &xLastWakeTime, xFrequency_2); // release CPU, by waiting until FFT_MIN_CYCLE is over
2022-08-07 22:19:38 +02:00
// release second sample to volume reactive effects.
// Releasing a second sample now effectively doubles the "sample rate"
micDataReal = maxSample2 ;
2022-06-17 16:24:25 +02:00
} // for(;;)
} // FFTcode()
//class name. Use something descriptive and leave the ": public Usermod" part :)
class AudioReactive : public Usermod {
private :
# ifndef AUDIOPIN
int8_t audioPin = 36 ;
# else
int8_t audioPin = AUDIOPIN ;
# endif
2022-06-21 22:49:45 +02:00
# ifndef DMTYPE // I2S mic type
2022-07-11 14:30:03 +02:00
uint8_t dmType = 1 ; // 0=none/disabled/analog; 1=generic I2S
2022-06-17 16:24:25 +02:00
# else
2022-06-21 22:49:45 +02:00
uint8_t dmType = DMTYPE ;
2022-06-17 16:24:25 +02:00
# endif
# ifndef I2S_SDPIN // aka DOUT
int8_t i2ssdPin = 32 ;
# else
int8_t i2ssdPin = I2S_SDPIN ;
# endif
# ifndef I2S_WSPIN // aka LRCL
int8_t i2swsPin = 15 ;
# else
int8_t i2swsPin = I2S_WSPIN ;
# endif
# ifndef I2S_CKPIN // aka BCLK
int8_t i2sckPin = 14 ;
# 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 = - 1 ;
# else
int8_t mclkPin = MLCK_PIN ;
# endif
2022-08-06 16:48:26 +02:00
// new "V2" audiosync struct - 40 Bytes
2022-06-17 16:24:25 +02:00
struct audioSyncPacket {
2022-08-06 16:48:26 +02:00
char header [ 6 ] ; // 06 Bytes
float sampleRaw ; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting
float sampleSmth ; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting
uint8_t samplePeak ; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude
uint8_t reserved1 ; // 01 Bytes - for future extensions - not used yet
2022-06-17 16:24:25 +02:00
uint8_t fftResult [ 16 ] ; // 16 Bytes
2022-08-06 16:48:26 +02:00
float FFT_Magnitude ; // 04 Bytes
float FFT_MajorPeak ; // 04 Bytes
} ;
// old "V1" audiosync struct - 83 Bytes - for backwards compatibility
struct audioSyncPacket_v1 {
char header [ 6 ] ; // 06 Bytes
uint8_t myVals [ 32 ] ; // 32 Bytes
int sampleAgc ; // 04 Bytes
int sampleRaw ; // 04 Bytes
float sampleAvg ; // 04 Bytes
bool samplePeak ; // 01 Bytes
uint8_t fftResult [ 16 ] ; // 16 Bytes
double FFT_Magnitude ; // 08 Bytes
double FFT_MajorPeak ; // 08 Bytes
2022-06-17 16:24:25 +02:00
} ;
WiFiUDP fftUdp ;
// set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)
2022-08-10 20:20:36 +02:00
bool enabled = false ;
2022-06-17 16:24:25 +02:00
bool initDone = false ;
const uint16_t delayMs = 10 ; // I don't want to sample too often and overload WLED
2022-06-19 19:15:34 +02:00
// variables used in effects
2022-08-08 10:51:46 +02:00
uint8_t maxVol = 10 ; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated)
uint8_t binNum = 8 ; // Used to select the bin for FFT based beat detection (deprecated)
2022-06-19 19:15:34 +02:00
bool samplePeak = 0 ; // Boolean flag for peak. Responding routine must reset this flag
2022-08-16 12:02:22 +02:00
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
2022-06-19 19:15:34 +02:00
2022-06-17 16:24:25 +02:00
bool udpSamplePeak = 0 ; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData
int16_t micIn = 0 ; // Current sample starts with negative values and large values, which is why it's 16 bit signed
2022-08-16 12:02:22 +02:00
int16_t sampleRaw = 0 ; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel; smoothed over 16 samples)
2022-07-04 12:34:32 +02:00
double sampleMax = 0.0 ; // Max sample over a few seconds. Needed for AGC controler.
2022-06-19 19:15:34 +02:00
float sampleReal = 0.0f ; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC.
float sampleAvg = 0.0f ; // Smoothed Average sampleRaw
2022-06-17 16:24:25 +02:00
float sampleAgc = 0.0f ; // Our AGC sample
int16_t rawSampleAgc = 0 ; // Our AGC sample - raw
uint32_t timeOfPeak = 0 ;
2022-07-10 22:47:42 +02:00
unsigned long lastTime = 0 ; // last time of running UDP Microphone Sync
2022-06-17 16:24:25 +02:00
float micLev = 0.0f ; // Used to convert returned value to have '0' as minimum. A leveller
2022-06-19 19:15:34 +02:00
float expAdjF = 0.0f ; // Used for exponential filter.
2022-06-17 16:24:25 +02:00
bool udpSyncConnected = false ;
2022-06-19 19:15:34 +02:00
uint16_t audioSyncPort = 11988 ;
2022-06-17 16:24:25 +02:00
// used for AGC
uint8_t lastMode = 0 ; // last known effect mode
int last_soundAgc = - 1 ;
2022-07-04 12:34:32 +02:00
double control_integrated = 0.0 ; // persistent across calls to agcAvg(); "integrator control" = accumulated error
2022-06-17 16:24:25 +02:00
unsigned long last_update_time = 0 ;
unsigned long last_kick_time = 0 ;
uint8_t last_user_inputLevel = 0 ;
2022-08-17 00:15:06 +02:00
// used to feed "Info" Page
unsigned long last_UDPTime = 0 ; // time of last valid UDP sound sync datapacket
float maxSample5sec = 0.0f ; // max sample (after AGC) in last 5 seconds
unsigned long sampleMaxTimer = 0 ; // last time maxSample5sec was reset
# define CYCLE_SAMPLEMAX 3500 // time window for merasuring
2022-06-17 16:24:25 +02:00
// strings to reduce flash memory usage (used more than twice)
static const char _name [ ] ;
static const char _enabled [ ] ;
2022-07-03 23:00:32 +02:00
static const char _inputLvl [ ] ;
2022-06-17 16:24:25 +02:00
static const char _analogmic [ ] ;
static const char _digitalmic [ ] ;
static const char UDP_SYNC_HEADER [ ] ;
2022-08-06 16:48:26 +02:00
static const char UDP_SYNC_HEADER_v1 [ ] ;
2022-06-17 16:24:25 +02:00
// private methods
void logAudio ( )
{
# ifdef MIC_LOGGER
2022-08-06 17:24:39 +02:00
// 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("micIn:"); Serial.print(micIn); Serial.print("\t");
//Serial.print("micLev:"); Serial.print(micLev); Serial.print("\t");
//Serial.print("sampleReal:"); Serial.print(sampleReal); Serial.print("\t");
//Serial.print("sample:"); Serial.print(sample); Serial.print("\t");
//Serial.print("sampleAvg:"); Serial.print(sampleAvg); 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 . print ( " sampleAgc: " ) ; Serial . print ( sampleAgc ) ; Serial . print ( " \t " ) ;
//Serial.print("volumeRaw:"); Serial.print(volumeRaw); Serial.print("\t");
//Serial.print("volumeSmth:"); Serial.print(volumeSmth); Serial.print("\t");
2022-06-17 16:24:25 +02:00
2022-06-18 12:57:54 +02:00
Serial . println ( ) ;
2022-06-17 16:24:25 +02:00
# endif
# ifdef FFT_SAMPLING_LOG
#if 0
for ( int i = 0 ; i < 16 ; i + + ) {
Serial . print ( fftResult [ i ] ) ;
Serial . print ( " \t " ) ;
}
2022-06-18 12:57:54 +02:00
Serial . println ( ) ;
2022-06-17 16:24:25 +02:00
# endif
// OPTIONS are in the following format: Description \n Option
//
// Set true if wanting to see all the bands in their own vertical space on the Serial Plotter, false if wanting to see values in Serial Monitor
const bool mapValuesToPlotterSpace = false ;
// Set true to apply an auto-gain like setting to to the data (this hasn't been tested recently)
const bool scaleValuesFromCurrentMaxVal = false ;
// prints the max value seen in the current data
const bool printMaxVal = false ;
// prints the min value seen in the current data
const bool printMinVal = false ;
// if !scaleValuesFromCurrentMaxVal, we scale values from [0..defaultScalingFromHighValue] to [0..scalingToHighValue], lower this if you want to see smaller values easier
const int defaultScalingFromHighValue = 256 ;
// Print values to terminal in range of [0..scalingToHighValue] if !mapValuesToPlotterSpace, or [(i)*scalingToHighValue..(i+1)*scalingToHighValue] if mapValuesToPlotterSpace
const int scalingToHighValue = 256 ;
// set higher if using scaleValuesFromCurrentMaxVal and you want a small value that's also the current maxVal to look small on the plotter (can't be 0 to avoid divide by zero error)
const int minimumMaxVal = 1 ;
int maxVal = minimumMaxVal ;
int minVal = 0 ;
for ( int i = 0 ; i < 16 ; i + + ) {
if ( fftResult [ i ] > maxVal ) maxVal = fftResult [ i ] ;
if ( fftResult [ i ] < minVal ) minVal = fftResult [ i ] ;
}
for ( int i = 0 ; i < 16 ; i + + ) {
Serial . print ( i ) ; Serial . print ( " : " ) ;
2022-08-08 10:51:46 +02:00
Serial . printf ( " %04ld " , map ( fftResult [ i ] , 0 , ( scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue ) , ( mapValuesToPlotterSpace * i * scalingToHighValue ) + 0 , ( mapValuesToPlotterSpace * i * scalingToHighValue ) + scalingToHighValue - 1 ) ) ;
2022-06-17 16:24:25 +02:00
}
if ( printMaxVal ) {
Serial . printf ( " maxVal:%04d " , maxVal + ( mapValuesToPlotterSpace ? 16 * 256 : 0 ) ) ;
}
if ( printMinVal ) {
Serial . printf ( " %04d:minVal " , minVal ) ; // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter
}
if ( mapValuesToPlotterSpace )
Serial . printf ( " max:%04d " , ( printMaxVal ? 17 : 16 ) * 256 ) ; // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis
else
Serial . printf ( " max:%04d " , 256 ) ;
Serial . println ( ) ;
# endif // FFT_SAMPLING_LOG
} // logAudio()
/*
2022-06-18 12:36:10 +02:00
* A " PI controller " multiplier to automatically adjust sound sensitivity .
2022-06-17 16:24:25 +02:00
*
* A few tricks are implemented so that sampleAgc does ' t only utilize 0 % and 100 % :
* 0. don ' t amplify anything below squelch ( but keep previous gain )
* 1. gain input = maximum signal observed in the last 5 - 10 seconds
* 2. we use two setpoints , one at ~ 60 % , and one at ~ 80 % of the maximum signal
* 3. the amplification depends on signal level :
* a ) normal zone - very slow adjustment
* b ) emergency zome ( < 10 % or > 90 % ) - very fast adjustment
*/
2022-08-06 18:17:45 +02:00
void agcAvg ( unsigned long the_time )
2022-06-17 16:24:25 +02:00
{
const int AGC_preset = ( soundAgc > 0 ) ? ( soundAgc - 1 ) : 0 ; // make sure the _compiler_ knows this value will not change while we are inside the function
float lastMultAgc = multAgc ; // last muliplier used
float multAgcTemp = multAgc ; // new multiplier
float tmpAgc = sampleReal * multAgc ; // what-if amplified signal
float control_error ; // "control error" input for PI control
if ( last_soundAgc ! = soundAgc )
2022-07-04 12:34:32 +02:00
control_integrated = 0.0 ; // new preset - reset integrator
2022-06-17 16:24:25 +02:00
// For PI controller, we need to have a constant "frequency"
// so let's make sure that the control loop is not running at insane speed
static unsigned long last_time = 0 ;
unsigned long time_now = millis ( ) ;
2022-08-06 18:17:45 +02:00
if ( ( the_time > 0 ) & & ( the_time < time_now ) ) time_now = the_time ; // allow caller to override my clock
2022-06-17 16:24:25 +02:00
if ( time_now - last_time > 2 ) {
last_time = time_now ;
if ( ( fabs ( sampleReal ) < 2.0f ) | | ( sampleMax < 1.0f ) ) {
// MIC signal is "squelched" - deliver silence
//multAgcTemp = multAgc; // keep old control value (no change)
tmpAgc = 0 ;
// we need to "spin down" the intgrated error buffer
2022-07-04 12:34:32 +02:00
if ( fabs ( control_integrated ) < 0.01 ) control_integrated = 0.0 ;
else control_integrated * = 0.91 ;
2022-06-17 16:24:25 +02:00
} else {
// compute new setpoint
if ( tmpAgc < = agcTarget0Up [ AGC_preset ] )
multAgcTemp = agcTarget0 [ AGC_preset ] / sampleMax ; // Make the multiplier so that sampleMax * multiplier = first setpoint
else
multAgcTemp = agcTarget1 [ AGC_preset ] / sampleMax ; // Make the multiplier so that sampleMax * multiplier = second setpoint
}
// limit amplification
//multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32
if ( multAgcTemp > 32.0f ) multAgcTemp = 32.0f ;
if ( multAgcTemp < 1.0f / 64.0f ) multAgcTemp = 1.0f / 64.0f ;
// compute error terms
control_error = multAgcTemp - lastMultAgc ;
if ( ( ( multAgcTemp > 0.085f ) & & ( multAgcTemp < 6.5f ) ) //integrator anti-windup by clamping
& & ( multAgc * sampleMax < agcZoneStop [ AGC_preset ] ) ) //integrator ceiling (>140% of max)
2022-07-04 12:34:32 +02:00
control_integrated + = control_error * 0.002 * 0.25 ; // 2ms = intgration time; 0.25 for damping
2022-06-17 16:24:25 +02:00
else
2022-07-04 12:34:32 +02:00
control_integrated * = 0.9 ; // spin down that beasty integrator
2022-06-17 16:24:25 +02:00
// 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 emergy zone
multAgcTemp = lastMultAgc + agcFollowFast [ AGC_preset ] * agcControlKp [ AGC_preset ] * control_error ;
multAgcTemp + = agcFollowFast [ AGC_preset ] * agcControlKi [ AGC_preset ] * control_integrated ;
} else { // "normal zone"
multAgcTemp = lastMultAgc + agcFollowSlow [ AGC_preset ] * agcControlKp [ AGC_preset ] * control_error ;
multAgcTemp + = agcFollowSlow [ AGC_preset ] * agcControlKi [ AGC_preset ] * control_integrated ;
}
// limit amplification again - PI controler sometimes "overshoots"
//multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32
if ( multAgcTemp > 32.0f ) multAgcTemp = 32.0f ;
if ( multAgcTemp < 1.0f / 64.0f ) multAgcTemp = 1.0f / 64.0f ;
}
// NOW finally amplify the signal
tmpAgc = sampleReal * multAgcTemp ; // apply gain to signal
2022-07-03 23:00:32 +02:00
if ( fabsf ( sampleReal ) < 2.0f ) tmpAgc = 0.0f ; // apply squelch threshold
2022-06-17 16:24:25 +02:00
//tmpAgc = constrain(tmpAgc, 0, 255);
2022-07-03 23:00:32 +02:00
if ( tmpAgc > 255 ) tmpAgc = 255.0f ; // limit to 8bit
if ( tmpAgc < 1 ) tmpAgc = 0.0f ; // just to be sure
2022-06-17 16:24:25 +02:00
// update global vars ONCE - multAgc, sampleAGC, rawSampleAgc
multAgc = multAgcTemp ;
rawSampleAgc = 0.8f * tmpAgc + 0.2f * ( float ) rawSampleAgc ;
// update smoothed AGC sample
2022-07-03 23:00:32 +02:00
if ( fabsf ( tmpAgc ) < 1.0f )
2022-06-19 19:15:34 +02:00
sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc ; // fast path to zero
2022-06-17 16:24:25 +02:00
else
2022-06-19 19:15:34 +02:00
sampleAgc + = agcSampleSmooth [ AGC_preset ] * ( tmpAgc - sampleAgc ) ; // smooth path
2022-06-17 16:24:25 +02:00
//userVar0 = sampleAvg * 4;
//if (userVar0 > 255) userVar0 = 255;
last_soundAgc = soundAgc ;
} // agcAvg()
void getSample ( )
{
2022-06-18 12:57:54 +02:00
float sampleAdj ; // Gain adjusted sample value
float tmpSample ; // An interim sample variable used for calculatioins.
const float weighting = 0.2f ; // Exponential filter weighting. Will be adjustable in a future release.
const int AGC_preset = ( soundAgc > 0 ) ? ( soundAgc - 1 ) : 0 ; // make sure the _compiler_ knows this value will not change while we are inside the function
2022-06-17 16:24:25 +02:00
# ifdef WLED_DISABLE_SOUND
micIn = inoise8 ( millis ( ) , millis ( ) ) ; // Simulated analog read
micDataReal = micIn ;
# else
2022-08-07 22:04:26 +02:00
# ifdef ESP32
2022-08-08 10:51:46 +02:00
micIn = int ( micDataReal ) ; // micDataSm = ((micData * 3) + micData)/4;
2022-08-07 22:04:26 +02:00
# 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 ;
2022-08-14 13:58:07 +02:00
static float lastAnalogValue = 0.0f ;
2022-08-07 22:04:26 +02:00
if ( millis ( ) - lastAnalogTime > 20 ) {
micDataReal = analogRead ( A0 ) ; // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only.
lastAnalogTime = millis ( ) ;
2022-08-14 13:58:07 +02:00
lastAnalogValue = micDataReal ;
yield ( ) ;
} else micDataReal = lastAnalogValue ;
2022-08-08 10:51:46 +02:00
micIn = int ( micDataReal ) ;
2022-08-07 22:04:26 +02:00
# endif
2022-06-17 16:24:25 +02:00
# endif
micLev = ( ( micLev * 8191.0f ) + micDataReal ) / 8192.0f ; // takes a few seconds to "catch up" with the Mic Input
if ( micIn < micLev ) micLev = ( ( micLev * 31.0f ) + micDataReal ) / 32.0f ; // align MicLev to lowest input signal
micIn - = micLev ; // Let's center it to 0 now
// Using an exponential filter to smooth out the signal. We'll add controls for this in a future release.
float micInNoDC = fabs ( micDataReal - micLev ) ;
expAdjF = ( weighting * micInNoDC + ( 1.0 - weighting ) * expAdjF ) ;
expAdjF = ( expAdjF < = soundSquelch ) ? 0 : expAdjF ; // simple noise gate
2022-08-14 13:58:07 +02:00
if ( ( soundSquelch = = 0 ) & & ( expAdjF < 0.25f ) ) expAdjF = 0 ; // do something meaningfull when "squelch = 0"
2022-06-17 16:24:25 +02:00
2022-07-03 23:00:32 +02:00
expAdjF = fabsf ( expAdjF ) ; // Now (!) take the absolute value
2022-06-17 16:24:25 +02:00
tmpSample = expAdjF ;
2022-07-03 23:00:32 +02:00
micIn = abs ( micIn ) ; // And get the absolute value of each sample
2022-06-17 16:24:25 +02:00
2022-07-03 23:00:32 +02:00
sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel / 128.0f + tmpSample / 16.0f ; // Adjust the gain. with inputLevel adjustment
2022-06-17 16:24:25 +02:00
sampleReal = tmpSample ;
sampleAdj = fmax ( fmin ( sampleAdj , 255 ) , 0 ) ; // Question: why are we limiting the value to 8 bits ???
2022-06-19 19:15:34 +02:00
sampleRaw = ( int16_t ) sampleAdj ; // ONLY update sample ONCE!!!!
2022-06-17 16:24:25 +02:00
// 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
} else {
if ( ( multAgc * sampleMax > agcZoneStop [ AGC_preset ] ) & & ( soundAgc > 0 ) )
sampleMax + = 0.5f * ( sampleReal - sampleMax ) ; // over AGC Zone - get back quickly
else
sampleMax * = agcSampleDecay [ AGC_preset ] ; // signal to zero --> 5-8sec
}
if ( sampleMax < 0.5f ) sampleMax = 0.0f ;
sampleAvg = ( ( sampleAvg * 15.0f ) + sampleAdj ) / 16.0f ; // Smooth it out over the last 16 samples.
// Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC
uint16_t MinShowDelay = strip . getMinShowDelay ( ) ;
if ( millis ( ) - timeOfPeak > MinShowDelay ) { // Auto-reset of samplePeak after a complete frame has passed.
2022-07-03 23:00:32 +02:00
samplePeak = false ;
udpSamplePeak = false ;
2022-06-17 16:24:25 +02:00
}
2022-06-19 19:15:34 +02:00
//if (userVar1 == 0) samplePeak = 0;
2022-08-08 13:53:46 +02:00
2022-06-17 16:24:25 +02:00
// Poor man's beat detection by seeing if sample > Average + some value.
// if (sample > (sampleAvg + maxVol) && millis() > (timeOfPeak + 200)) {
2022-08-10 18:14:28 +02:00
if ( ( maxVol > 0 ) & & ( binNum > 1 ) & & ( fftBin [ binNum ] > maxVol ) & & ( millis ( ) > ( timeOfPeak + 100 ) ) ) { // This goes through ALL of the 255 bins - but ignores stupid settings
2022-06-17 16:24:25 +02:00
// 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.
2022-07-03 23:00:32 +02:00
samplePeak = true ;
2022-06-17 16:24:25 +02:00
timeOfPeak = millis ( ) ;
2022-07-03 23:00:32 +02:00
udpSamplePeak = true ;
2022-06-17 16:24:25 +02:00
//userVar1 = samplePeak;
}
} // getSample()
2022-08-06 17:53:35 +02:00
/* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc).
2022-08-14 13:58:07 +02:00
* does not affect FFTResult [ ] or volumeRaw ( = sample or rawSampleAgc )
2022-08-06 17:53:35 +02:00
*/
// effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly)
void limitSampleDynamics ( void ) {
# ifdef SOUND_DYNAMICS_LIMITER
2022-08-14 14:38:27 +02:00
const float bigChange = 196 ; // just a representative number - a large, expected sample value
2022-08-06 17:53:35 +02:00
static unsigned long last_time = 0 ;
static float last_volumeSmth = 0.0f ;
2022-08-14 14:38:27 +02:00
long delta_time = millis ( ) - last_time ;
delta_time = constrain ( delta_time , 1 , 1000 ) ; // below 1ms -> 1ms; above 1sec -> sily lil hick-up
float deltaSample = volumeSmth - last_volumeSmth ;
2022-08-06 17:53:35 +02:00
2022-08-14 14:38:27 +02:00
if ( attackTime > 0 ) { // user has defined attack time > 0
float maxAttack = bigChange * float ( delta_time ) / float ( attackTime ) ;
2022-08-06 17:53:35 +02:00
if ( deltaSample > maxAttack ) deltaSample = maxAttack ;
2022-08-14 14:38:27 +02:00
}
if ( decayTime > 0 ) { // user has defined decay time > 0
float maxDecay = - bigChange * float ( delta_time ) / float ( decayTime ) ;
2022-08-06 17:53:35 +02:00
if ( deltaSample < maxDecay ) deltaSample = maxDecay ;
}
2022-08-14 14:38:27 +02:00
volumeSmth = last_volumeSmth + deltaSample ;
2022-08-06 17:53:35 +02:00
last_volumeSmth = volumeSmth ;
last_time = millis ( ) ;
# endif
}
2022-06-17 16:24:25 +02:00
void transmitAudioData ( )
{
if ( ! udpSyncConnected ) return ;
//DEBUGSR_PRINTLN("Transmitting UDP Mic Packet");
audioSyncPacket transmitData ;
strncpy_P ( transmitData . header , PSTR ( UDP_SYNC_HEADER ) , 6 ) ;
2022-08-14 13:58:07 +02:00
//transmitData.sampleRaw = volumeRaw;
//transmitData.sampleSmth = volumeSmth;
// transmit samples that were not modified by limitSampleDynamics()
transmitData . sampleRaw = ( soundAgc ) ? rawSampleAgc : sampleRaw ;
transmitData . sampleSmth = ( soundAgc ) ? sampleAgc : sampleAvg ;
2022-08-06 16:48:26 +02:00
transmitData . samplePeak = udpSamplePeak ? 1 : 0 ;
udpSamplePeak = false ; // Reset udpSamplePeak after we've transmitted it
transmitData . reserved1 = 0 ;
2022-06-17 16:24:25 +02:00
for ( int i = 0 ; i < 16 ; i + + ) {
transmitData . fftResult [ i ] = ( uint8_t ) constrain ( fftResult [ i ] , 0 , 254 ) ;
}
2022-08-06 16:48:26 +02:00
transmitData . FFT_Magnitude = my_magnitude ;
2022-06-17 16:24:25 +02:00
transmitData . FFT_MajorPeak = FFT_MajorPeak ;
fftUdp . beginMulticastPacket ( ) ;
fftUdp . write ( reinterpret_cast < uint8_t * > ( & transmitData ) , sizeof ( transmitData ) ) ;
fftUdp . endPacket ( ) ;
return ;
} // transmitAudioData()
2022-08-16 12:02:22 +02:00
static bool isValidUdpSyncVersion ( const char * header ) {
2022-06-17 16:24:25 +02:00
return strncmp_P ( header , PSTR ( UDP_SYNC_HEADER ) , 6 ) = = 0 ;
}
2022-08-10 17:18:43 +02:00
bool receiveAudioData ( ) // check & process new data. return TRUE in case that new audio data was received.
2022-06-17 16:24:25 +02:00
{
2022-08-10 17:18:43 +02:00
if ( ! udpSyncConnected ) return false ;
2022-06-17 16:24:25 +02:00
//DEBUGSR_PRINTLN("Checking for UDP Microphone Packet");
2022-08-10 17:18:43 +02:00
bool haveFreshData = false ;
2022-06-17 16:24:25 +02:00
size_t packetSize = fftUdp . parsePacket ( ) ;
2022-08-06 16:48:26 +02:00
if ( packetSize > 5 ) {
2022-06-17 16:24:25 +02:00
//DEBUGSR_PRINTLN("Received UDP Sync Packet");
uint8_t fftBuff [ packetSize ] ;
fftUdp . read ( fftBuff , packetSize ) ;
// VERIFY THAT THIS IS A COMPATIBLE PACKET
2022-08-16 12:02:22 +02:00
if ( packetSize = = sizeof ( audioSyncPacket ) & & ( isValidUdpSyncVersion ( ( const char * ) fftBuff ) ) ) {
2022-06-17 16:24:25 +02:00
audioSyncPacket * receivedPacket = reinterpret_cast < audioSyncPacket * > ( fftBuff ) ;
2022-08-14 13:58:07 +02:00
// update samples for effects
2022-08-16 12:02:22 +02:00
volumeSmth = fmaxf ( receivedPacket - > sampleSmth , 0.0f ) ;
volumeRaw = fmaxf ( receivedPacket - > sampleRaw , 0.0f ) ;
2022-06-17 16:24:25 +02:00
2022-08-14 13:58:07 +02:00
// update internal samples
2022-08-06 16:48:26 +02:00
sampleRaw = volumeRaw ;
sampleAvg = volumeSmth ;
rawSampleAgc = volumeRaw ;
sampleAgc = volumeSmth ;
multAgc = 1.0f ;
// auto-reset sample peak. Need to do it here, because getSample() is not running
uint16_t MinShowDelay = strip . getMinShowDelay ( ) ;
if ( millis ( ) - timeOfPeak > MinShowDelay ) { // Auto-reset of samplePeak after a complete frame has passed.
samplePeak = false ;
udpSamplePeak = false ;
}
//if (userVar1 == 0) samplePeak = 0;
2022-06-17 16:24:25 +02:00
// Only change samplePeak IF it's currently false.
// If it's true already, then the animation still needs to respond.
2022-08-06 16:48:26 +02:00
if ( ! samplePeak ) {
samplePeak = receivedPacket - > samplePeak > 0 ? true : false ;
if ( samplePeak ) timeOfPeak = millis ( ) ;
//userVar1 = samplePeak;
}
2022-06-17 16:24:25 +02:00
//These values are only available on the ESP32
for ( int i = 0 ; i < 16 ; i + + ) fftResult [ i ] = receivedPacket - > fftResult [ i ] ;
2022-08-16 12:02:22 +02:00
my_magnitude = fmaxf ( receivedPacket - > FFT_Magnitude , 0.0f ) ;
2022-08-06 16:48:26 +02:00
FFT_Magnitude = my_magnitude ;
2022-08-16 12:02:22 +02:00
FFT_MajorPeak = fmaxf ( receivedPacket - > FFT_MajorPeak , 0.0f ) ;
2022-06-17 16:24:25 +02:00
//DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet");
2022-08-10 17:18:43 +02:00
haveFreshData = true ;
2022-06-17 16:24:25 +02:00
}
}
2022-08-10 17:18:43 +02:00
return haveFreshData ;
2022-06-17 16:24:25 +02:00
}
public :
2022-06-19 19:15:34 +02:00
//Functions called by WLED or other usermods
2022-06-17 16:24:25 +02:00
/*
* setup ( ) is called once at boot . WiFi is not yet connected at this point .
* You can use it to initialize variables , sensors or similar .
* It is called * AFTER * readFromConfig ( )
*/
void setup ( )
{
2022-07-10 22:47:42 +02:00
disableSoundProcessing = true ; // just to be sure
2022-06-17 16:24:25 +02:00
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 ;
2022-07-29 15:50:09 +02:00
um_data - > u_size = 8 ;
2022-06-17 16:24:25 +02:00
um_data - > u_type = new um_types_t [ um_data - > u_size ] ;
um_data - > u_data = new void * [ um_data - > u_size ] ;
Refactor um_data variables for audio reactive
- change sample to sampleRaw
- add volumeSmth, volumeRaw, my_magnitude and calculate in agcAvg
- remove sampleAvg, soundAgc, sampleAgc, sampleRaw, rawSampleAgc, FFT_Magnitude, multAgc, sampleReal, sampleGain, (myVals), soundSquelch from um_data interface
- refactor all effects using above variables
2022-07-29 15:24:04 +02:00
um_data - > u_data [ 0 ] = & volumeSmth ; //*used (New)
um_data - > u_type [ 0 ] = UMT_FLOAT ;
um_data - > u_data [ 1 ] = & volumeRaw ; // used (New)
um_data - > u_type [ 1 ] = UMT_UINT16 ;
um_data - > u_data [ 2 ] = fftResult ; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi)
um_data - > u_type [ 2 ] = UMT_BYTE_ARR ;
um_data - > u_data [ 3 ] = & samplePeak ; //*used (Puddlepeak, Ripplepeak, Waterfall)
um_data - > u_type [ 3 ] = UMT_BYTE ;
um_data - > u_data [ 4 ] = & FFT_MajorPeak ; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall)
um_data - > u_type [ 4 ] = UMT_FLOAT ;
um_data - > u_data [ 5 ] = & my_magnitude ; // used (New)
um_data - > u_type [ 5 ] = UMT_FLOAT ;
um_data - > u_data [ 6 ] = & maxVol ; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)
um_data - > u_type [ 6 ] = UMT_BYTE ;
um_data - > u_data [ 7 ] = & binNum ; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)
um_data - > u_type [ 7 ] = UMT_BYTE ;
2022-06-17 16:24:25 +02:00
}
// Reset I2S peripheral for good measure
i2s_driver_uninstall ( I2S_NUM_0 ) ;
periph_module_reset ( PERIPH_I2S0_MODULE ) ;
delay ( 100 ) ; // Give that poor microphone some time to setup.
switch ( dmType ) {
case 1 :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINT ( F ( " AR: Generic I2S Microphone - " ) ) ; DEBUGSR_PRINTLN ( F ( I2S_MIC_CHANNEL_TEXT ) ) ;
audioSource = new I2SSource ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
if ( audioSource ) audioSource - > initialize ( i2swsPin , i2ssdPin , i2sckPin ) ;
break ;
case 2 :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINTLN ( F ( " AR: ES7243 Microphone (right channel only). " ) ) ;
audioSource = new ES7243 ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
if ( audioSource ) audioSource - > initialize ( sdaPin , sclPin , i2swsPin , i2ssdPin , i2sckPin , mclkPin ) ;
break ;
case 3 :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINT ( F ( " AR: SPH0645 Microphone - " ) ) ; DEBUGSR_PRINTLN ( F ( I2S_MIC_CHANNEL_TEXT ) ) ;
audioSource = new SPH0654 ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
audioSource - > initialize ( i2swsPin , i2ssdPin , i2sckPin ) ;
break ;
case 4 :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINT ( F ( " AR: Generic I2S Microphone with Master Clock - " ) ) ; DEBUGSR_PRINTLN ( F ( I2S_MIC_CHANNEL_TEXT ) ) ;
audioSource = new I2SSource ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
if ( audioSource ) audioSource - > initialize ( i2swsPin , i2ssdPin , i2sckPin , mclkPin ) ;
break ;
case 5 :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINT ( F ( " AR: I2S PDM Microphone - " ) ) ; DEBUGSR_PRINTLN ( F ( I2S_MIC_CHANNEL_TEXT ) ) ;
audioSource = new I2SSource ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
if ( audioSource ) audioSource - > initialize ( i2swsPin , i2ssdPin ) ;
break ;
case 0 :
default :
2022-08-10 17:18:43 +02:00
DEBUGSR_PRINTLN ( F ( " AR: Analog Microphone (left channel only). " ) ) ;
audioSource = new I2SAdcSource ( SAMPLE_RATE , BLOCK_SIZE ) ;
2022-06-17 16:24:25 +02:00
delay ( 100 ) ;
if ( audioSource ) audioSource - > initialize ( audioPin ) ;
break ;
}
2022-08-10 17:18:43 +02:00
delay ( 250 ) ; // give microphone enough time to initialise
if ( ! audioSource ) enabled = false ; // audio failed to initialise
if ( enabled ) onUpdateBegin ( false ) ; // create FFT task
if ( FFT_Task = = nullptr ) enabled = false ; // FFT task creation failed
if ( enabled ) disableSoundProcessing = false ; // all good - enable audio processing
2022-06-17 16:24:25 +02:00
2022-08-10 17:18:43 +02:00
if ( ( ! audioSource ) | | ( ! audioSource - > isInitialized ( ) ) ) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync
DEBUGSR_PRINTLN ( F ( " AR: Failed to initialize sound input driver. Please check input PIN settings. " ) ) ;
disableSoundProcessing = true ;
}
2022-06-17 16:24:25 +02:00
initDone = true ;
}
/*
* connected ( ) is called every time the WiFi is ( re ) connected
* Use it to initialize network interfaces
*/
void connected ( )
{
if ( audioSyncPort > 0 | | ( audioSyncEnabled & 0x03 ) ) {
# ifndef ESP8266
udpSyncConnected = fftUdp . beginMulticast ( IPAddress ( 239 , 0 , 0 , 1 ) , audioSyncPort ) ;
# else
udpSyncConnected = fftUdp . beginMulticast ( WiFi . localIP ( ) , IPAddress ( 239 , 0 , 0 , 1 ) , audioSyncPort ) ;
# endif
}
}
/*
* loop ( ) is called continuously . Here you can check for events , read sensors , etc .
*
* Tips :
* 1. You can use " if (WLED_CONNECTED) " to check for a successful network connection .
* Additionally , " if (WLED_MQTT_CONNECTED) " is available to check for a connection to an MQTT broker .
*
* 2. Try to avoid using the delay ( ) function . NEVER use delays longer than 10 milliseconds .
* Instead , use a timer check as shown here .
*/
void loop ( )
{
2022-07-10 22:47:42 +02:00
static unsigned long lastUMRun = millis ( ) ;
if ( ! enabled ) {
disableSoundProcessing = true ; // keep processing suspended (FFT task)
lastUMRun = millis ( ) ; // update time keeping
return ;
}
// We cannot wait indefinitely before processing audio data
2022-08-06 16:48:26 +02:00
if ( strip . isUpdating ( ) & & ( millis ( ) - lastUMRun < 2 ) ) return ; // be nice, but not too nice
2022-07-10 22:47:42 +02:00
// suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET)
if ( ( realtimeOverride = = REALTIME_OVERRIDE_NONE ) // please odd other orrides here if needed
& & ( ( realtimeMode = = REALTIME_MODE_GENERIC )
| | ( realtimeMode = = REALTIME_MODE_E131 )
| | ( realtimeMode = = REALTIME_MODE_UDP )
| | ( realtimeMode = = REALTIME_MODE_ADALIGHT )
| | ( realtimeMode = = REALTIME_MODE_ARTNET ) ) ) // please add other modes here if needed
{
# ifdef WLED_DEBUG
if ( ( disableSoundProcessing = = false ) & & ( audioSyncEnabled = = 0 ) ) { // 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 ;
} else {
# ifdef WLED_DEBUG
if ( ( disableSoundProcessing = = true ) & & ( audioSyncEnabled = = 0 ) ) { // we just switched to "disabled"
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
disableSoundProcessing = false ;
}
2022-06-17 16:24:25 +02:00
2022-07-10 22:47:42 +02:00
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
2022-08-17 00:15:06 +02:00
if ( ! audioSource - > isInitialized ( ) ) disableSoundProcessing = true ; // no audio source
2022-07-10 22:47:42 +02:00
2022-08-14 13:58:07 +02:00
2022-08-06 18:17:45 +02:00
// Only run the sampling code IF we're not in Receive mode or realtime mode
if ( ! ( audioSyncEnabled & 0x02 ) & & ! disableSoundProcessing ) {
2022-06-19 19:15:34 +02:00
bool agcEffect = false ;
2022-06-17 16:24:25 +02:00
if ( soundAgc > AGC_NUM_PRESETS ) soundAgc = 0 ; // make sure that AGC preset is valid (to avoid array bounds violation)
2022-06-19 19:15:34 +02:00
2022-08-06 18:17:45 +02:00
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.
2022-07-10 22:47:42 +02:00
2022-08-06 18:17:45 +02:00
# 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.
if ( ( userloopDelay > 23 ) & & ! disableSoundProcessing & & ( audioSyncEnabled = = 0 ) ) {
DEBUG_PRINTF ( " [AR userLoop] hickup detected -> was inactive for last %d millis! \n " , userloopDelay ) ;
}
# endif
2022-06-17 16:24:25 +02:00
2022-08-06 18:17:45 +02:00
// run filters, and repeat in case of loop delays (hick-up compensation)
if ( userloopDelay < 2 ) userloopDelay = 0 ; // minor glitch, no problem
if ( userloopDelay > 200 ) userloopDelay = 200 ; // limit number of filter re-runs
do {
getSample ( ) ; // run microphone sampling filters
agcAvg ( t_now - userloopDelay ) ; // Calculated the PI adjusted value as sampleAvg
userloopDelay - = 2 ; // advance "simulated time" by 2ms
} while ( userloopDelay > 0 ) ;
lastUMRun = t_now ; // update time keeping
2022-08-06 16:48:26 +02:00
// update samples for effects (raw, smooth)
volumeSmth = ( soundAgc ) ? sampleAgc : sampleAvg ;
volumeRaw = ( soundAgc ) ? rawSampleAgc : sampleRaw ;
// update FFTMagnitude, taking into account AGC amplification
my_magnitude = FFT_Magnitude ; // / 16.0f, 8.0f, 4.0f done in effects
if ( soundAgc ) my_magnitude * = multAgc ;
if ( volumeSmth < 1 ) my_magnitude = 0.001f ; // noise gate closed - mute
2022-08-06 17:53:35 +02:00
limitSampleDynamics ( ) ; // optional - makes volumeSmth very smooth and fluent
2022-08-06 16:48:26 +02:00
2022-08-14 13:58:07 +02:00
// update WebServer UI
2022-06-19 19:15:34 +02:00
uint8_t knownMode = strip . getFirstSelectedSeg ( ) . mode ; // 1st selected segment is more appropriate than main segment
2022-06-17 16:24:25 +02:00
if ( lastMode ! = knownMode ) { // only execute if mode changes
2022-06-19 19:15:34 +02:00
char lineBuffer [ 4 ] ;
extractModeName ( knownMode , JSON_mode_names , lineBuffer , 3 ) ; // use of JSON_mode_names is deprecated, use nullptr
2022-06-17 16:24:25 +02:00
agcEffect = ( lineBuffer [ 1 ] = = 226 & & lineBuffer [ 2 ] = = 153 ) ; // && (lineBuffer[3] == 170 || lineBuffer[3] == 171 ) encoding of ♪ or ♫
// agcEffect = (lineBuffer[4] == 240 && lineBuffer[5] == 159 && lineBuffer[6] == 142 && lineBuffer[7] == 154 ); //encoding of 🎚 No clue why as not found here https://www.iemoji.com/view/emoji/918/objects/level-slider
2022-06-19 19:15:34 +02:00
lastMode = knownMode ;
2022-06-17 16:24:25 +02:00
}
// update inputLevel Slider based on current AGC gain
if ( ( soundAgc > 0 ) & & agcEffect ) {
unsigned long now_time = millis ( ) ;
// "user kick" feature - if user has moved the slider by at least 32 units, we "kick" AGC gain by 30% (up or down)
// only once in 3.5 seconds
if ( ( lastMode = = knownMode )
& & ( abs ( last_user_inputLevel - inputLevel ) > 31 )
& & ( now_time - last_kick_time > 3500 ) ) {
if ( last_user_inputLevel > inputLevel ) multAgc * = 0.60 ; // down -> reduce gain
if ( last_user_inputLevel < inputLevel ) multAgc * = 1.50 ; // up -> increase gain
last_kick_time = now_time ;
}
int new_user_inputLevel = 128.0f * multAgc ; // scale AGC multiplier so that "1" is at 128
if ( multAgc > 1.0f ) new_user_inputLevel = 128.0f * ( ( ( multAgc - 1.0f ) / 4.0f ) + 1.0f ) ; // compress range so we can show values up to 4
new_user_inputLevel = MIN ( MAX ( new_user_inputLevel , 0 ) , 255 ) ;
// update user interfaces - restrict frequency to avoid flooding UI's with small changes
2022-07-10 22:47:42 +02:00
if ( ( ( ( now_time - last_update_time > 3500 ) & & ( abs ( new_user_inputLevel - inputLevel ) > 2 ) ) // small change - every 3.5 sec (max)
| | ( ( now_time - last_update_time > 2200 ) & & ( abs ( new_user_inputLevel - inputLevel ) > 15 ) ) // medium change
| | ( ( now_time - last_update_time > 1200 ) & & ( abs ( new_user_inputLevel - inputLevel ) > 31 ) ) ) // BIG change - every second
& & ! strip . isUpdating ( ) ) // don't interfere while strip is updating
2022-06-17 16:24:25 +02:00
{
inputLevel = new_user_inputLevel ; // change of least 3 units -> update user variable
updateInterfaces ( CALL_MODE_WS_SEND ) ; // is this the correct way to notify UIs ? Yes says blazoncek
last_update_time = now_time ;
last_user_inputLevel = new_user_inputLevel ;
}
}
}
2022-08-14 13:58:07 +02:00
// UDP Microphone Sync - receive mode
if ( ( audioSyncEnabled & 0x02 ) & & udpSyncConnected ) {
// Only run the audio listener code if we're in Receive mode
static float syncVolumeSmth = 0 ;
bool have_new_sample = false ;
if ( millis ( ) - lastTime > delayMs ) {
have_new_sample = receiveAudioData ( ) ;
2022-08-17 00:15:06 +02:00
if ( have_new_sample ) last_UDPTime = millis ( ) ;
2022-08-14 13:58:07 +02:00
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
2022-08-06 16:48:26 +02:00
}
2022-06-17 16:24:25 +02:00
2022-08-06 16:48:26 +02:00
# if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG)
EVERY_N_MILLIS ( 20 ) {
logAudio ( ) ;
}
# endif
2022-08-17 00:15:06 +02:00
// peak sample from last 5 seconds
if ( ( millis ( ) - sampleMaxTimer ) > CYCLE_SAMPLEMAX ) {
sampleMaxTimer = millis ( ) ;
maxSample5sec = ( 0.25 * maxSample5sec ) + 0.75 * ( ( soundAgc ) ? sampleAgc : sampleAvg ) ; // reset, with some smoothing
if ( sampleAvg < 1 ) maxSample5sec = 0 ; // noise gate
} else {
maxSample5sec = fmaxf ( maxSample5sec , ( soundAgc ) ? sampleAgc : sampleAvg ) ; // follow maximum
}
2022-08-14 13:58:07 +02:00
//UDP Microphone Sync - transmit mode
2022-08-16 12:02:22 +02:00
if ( ( audioSyncEnabled & 0x01 ) & & ( millis ( ) - lastTime > 20 ) ) {
2022-08-14 13:58:07 +02:00
// Only run the transmit code IF we're in Transmit mode
2022-08-06 16:48:26 +02:00
transmitAudioData ( ) ;
2022-06-17 16:24:25 +02:00
lastTime = millis ( ) ;
}
2022-08-06 17:53:35 +02:00
2022-06-17 16:24:25 +02:00
}
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 ;
}
void onUpdateBegin ( bool init )
{
2022-07-03 23:00:32 +02:00
# ifdef WLED_DEBUG
fftTime = sampleTime = 0 ;
# endif
2022-08-17 00:15:06 +02:00
// gracefully suspend FFT task (if running)
2022-07-10 22:47:42 +02:00
disableSoundProcessing = true ;
2022-08-17 00:15:06 +02:00
// reset sound data
micDataReal = 0.0f ;
volumeRaw = 0 ; volumeSmth = 0 ;
sampleAgc = 0 ; sampleAvg = 0 ;
sampleRaw = 0 ; rawSampleAgc = 0 ;
my_magnitude = 0 ; FFT_Magnitude = 0 ; FFT_MajorPeak = 0 ;
multAgc = 1 ;
2022-07-06 19:46:32 +02:00
if ( init & & FFT_Task ) {
vTaskSuspend ( FFT_Task ) ; // update is about to begin, disable task to prevent crash
} else {
// update has failed or create task requested
if ( FFT_Task )
vTaskResume ( FFT_Task ) ;
else
2022-08-07 22:19:38 +02:00
// xTaskCreatePinnedToCore(
xTaskCreate ( // no need to "pin" this task to core #0
2022-07-06 19:46:32 +02:00
FFTcode , // Function to implement the task
" FFT " , // Name of the task
5000 , // Stack size in words
NULL , // Task input parameter
1 , // Priority of the task
2022-08-07 22:19:38 +02:00
& FFT_Task // Task handle
// , 0 // Core where the task should run
2022-07-06 19:46:32 +02:00
) ;
2022-06-17 16:24:25 +02:00
}
2022-08-17 00:15:06 +02:00
micDataReal = 0.0f ; // just to ber sure
2022-07-10 22:47:42 +02:00
if ( enabled ) disableSoundProcessing = false ;
2022-06-17 16:24:25 +02:00
}
2022-06-29 14:12:07 +02:00
/**
* handleButton ( ) can be used to override default button behaviour . Returning true
* will prevent button working in a default way .
*/
bool handleButton ( uint8_t b ) {
yield ( ) ;
// crude way of determining if audio input is analog
// better would be for AudioSource to implement getType()
if ( enabled
& & dmType = = 0 & & audioPin > = 0
& & ( buttonType [ b ] = = BTN_TYPE_ANALOG | | buttonType [ b ] = = BTN_TYPE_ANALOG_INVERTED )
) {
return true ;
}
return false ;
}
2022-06-17 16:24:25 +02:00
/*
* addToJsonInfo ( ) can be used to add custom entries to the / json / info part of the JSON API .
* Creating an " u " object allows you to add custom key / value pairs to the Info section of the WLED web UI .
* Below it is shown how this could be used for e . g . a light sensor
*/
void addToJsonInfo ( JsonObject & root )
{
2022-08-17 00:15:06 +02:00
char myStringBuffer [ 16 ] ; // buffer for snprintf()
2022-06-17 16:24:25 +02:00
JsonObject user = root [ " u " ] ;
if ( user . isNull ( ) ) user = root . createNestedObject ( " u " ) ;
2022-06-20 22:17:01 +02:00
JsonArray infoArr = user . createNestedArray ( FPSTR ( _name ) ) ;
String uiDomString = F ( " <button class= \" btn btn-xs \" onclick= \" requestJson({ " ) ;
2022-06-17 16:24:25 +02:00
uiDomString + = FPSTR ( _name ) ;
uiDomString + = F ( " :{ " ) ;
uiDomString + = FPSTR ( _enabled ) ;
2022-06-20 22:17:01 +02:00
uiDomString + = enabled ? F ( " :false}}); \" > " ) : F ( " :true}}); \" > " ) ;
uiDomString + = F ( " <i class= \" icons " ) ;
uiDomString + = enabled ? F ( " on " ) : F ( " off " ) ;
uiDomString + = F ( " \" ></i> " ) ;
2022-06-17 16:24:25 +02:00
uiDomString + = F ( " </button> " ) ;
2022-06-20 22:17:01 +02:00
infoArr . add ( uiDomString ) ;
2022-07-03 23:00:32 +02:00
2022-07-20 21:22:23 +02:00
if ( enabled ) {
infoArr = user . createNestedArray ( F ( " Input level " ) ) ;
uiDomString = F ( " <div class= \" slider \" ><div class= \" sliderwrap il \" ><input class= \" noslide \" onchange= \" requestJson({ " ) ;
uiDomString + = FPSTR ( _name ) ;
uiDomString + = F ( " :{ " ) ;
uiDomString + = FPSTR ( _inputLvl ) ;
uiDomString + = F ( " :parseInt(this.value)}}); \" oninput= \" updateTrail(this); \" max=255 min=0 type= \" range \" value= " ) ;
uiDomString + = inputLevel ;
uiDomString + = F ( " /><div class= \" sliderdisplay \" ></div></div></div> " ) ; //<output class=\"sliderbubble\"></output>
infoArr . add ( uiDomString ) ;
2022-07-03 23:00:32 +02:00
2022-08-17 00:15:06 +02:00
// current Audio input
infoArr = user . createNestedArray ( F ( " Audio Source " ) ) ;
if ( audioSyncEnabled & 0x02 ) {
// UDP sound sync - receive mode
infoArr . add ( " UDP sound sync " ) ;
if ( udpSyncConnected ) {
if ( millis ( ) - last_UDPTime < 2500 )
infoArr . add ( " - receiving " ) ;
else
infoArr . add ( " - idle " ) ;
} else {
infoArr . add ( " - no network " ) ;
}
} else {
// Analog or I2S digital input
if ( audioSource & & ( audioSource - > isInitialized ( ) ) ) {
// audio source sucessfully configured
if ( audioSource - > getType ( ) = = AudioSource : : Type_I2SAdc ) {
infoArr . add ( " ADC analog " ) ;
} else {
infoArr . add ( " I2S digital " ) ;
}
// input level or "silence"
if ( maxSample5sec > 1.0 ) {
float my_usage = 100.0f * ( maxSample5sec / 255.0f ) ;
snprintf ( myStringBuffer , 15 , " - peak %3d%% " , int ( my_usage ) ) ;
infoArr . add ( myStringBuffer ) ;
} else {
infoArr . add ( " - quiet " ) ;
}
} else {
// error during audio source setup
infoArr . add ( " not initialized " ) ;
infoArr . add ( " - check GPIO config " ) ;
}
}
// Sound processing (FFT and input filters)
infoArr = user . createNestedArray ( F ( " Sound Processing " ) ) ;
if ( audioSource & & ( disableSoundProcessing = = false ) ) {
infoArr . add ( " running " ) ;
} else {
infoArr . add ( " suspended " ) ;
}
// AGC or manual Gain
if ( ( soundAgc = = 0 ) & & ( disableSoundProcessing = = false ) & & ! ( audioSyncEnabled & 0x02 ) ) {
infoArr = user . createNestedArray ( F ( " Manual Gain " ) ) ;
float myGain = ( ( float ) sampleGain / 40.0f * ( float ) inputLevel / 128.0f ) + 1.0f / 16.0f ; // non-AGC gain from presets
infoArr . add ( roundf ( myGain * 100.0f ) / 100.0f ) ;
infoArr . add ( " x " ) ;
}
if ( soundAgc & & ( disableSoundProcessing = = false ) & & ! ( audioSyncEnabled & 0x02 ) ) {
infoArr = user . createNestedArray ( F ( " AGC Gain " ) ) ;
infoArr . add ( roundf ( multAgc * 100.0f ) / 100.0f ) ;
infoArr . add ( " x " ) ;
}
// UDP Sound Sync status
infoArr = user . createNestedArray ( F ( " UDP Sound Sync " ) ) ;
if ( audioSyncEnabled ) {
if ( audioSyncEnabled & 0x01 ) {
infoArr . add ( " send mode " ) ;
} else if ( audioSyncEnabled & 0x02 ) {
infoArr . add ( " receive mode " ) ;
}
} else
infoArr . add ( " off " ) ;
2022-07-20 21:22:23 +02:00
# ifdef WLED_DEBUG
infoArr = user . createNestedArray ( F ( " Sampling time " ) ) ;
infoArr . add ( sampleTime ) ;
infoArr . add ( " ms " ) ;
infoArr = user . createNestedArray ( F ( " FFT time " ) ) ;
infoArr . add ( fftTime - sampleTime ) ;
infoArr . add ( " ms " ) ;
# endif
}
2022-06-17 16:24:25 +02:00
}
/*
* 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 )
{
2022-07-20 21:22:23 +02:00
if ( ! initDone ) return ; // prevent crash on boot applyPreset()
JsonObject usermod = root [ FPSTR ( _name ) ] ;
if ( usermod . isNull ( ) ) {
usermod = root . createNestedObject ( FPSTR ( _name ) ) ;
}
usermod [ " on " ] = enabled ;
2022-06-17 16:24:25 +02:00
}
/*
* 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 )
{
if ( ! initDone ) return ; // prevent crash on boot applyPreset()
bool prevEnabled = enabled ;
JsonObject usermod = root [ FPSTR ( _name ) ] ;
if ( ! usermod . isNull ( ) ) {
if ( usermod [ FPSTR ( _enabled ) ] . is < bool > ( ) ) {
enabled = usermod [ FPSTR ( _enabled ) ] . as < bool > ( ) ;
if ( prevEnabled ! = enabled ) onUpdateBegin ( ! enabled ) ;
}
2022-07-03 23:00:32 +02:00
if ( usermod [ FPSTR ( _inputLvl ) ] . is < int > ( ) ) {
inputLevel = min ( 255 , max ( 0 , usermod [ FPSTR ( _inputLvl ) ] . as < int > ( ) ) ) ;
}
2022-06-17 16:24:25 +02:00
}
}
/*
* addToConfig ( ) can be used to add custom persistent settings to the cfg . json file in the " um " ( usermod ) object .
* It will be called by WLED when settings are actually saved ( for example , LED settings are saved )
* If you want to force saving the current state , use serializeConfig ( ) in your loop ( ) .
*
* CAUTION : serializeConfig ( ) will initiate a filesystem write operation .
* It might cause the LEDs to stutter and will cause flash wear if called too often .
* Use it sparingly and always in the loop , never in network callbacks !
*
* addToConfig ( ) will make your settings editable through the Usermod Settings page automatically .
*
* Usermod Settings Overview :
* - Numeric values are treated as floats in the browser .
* - If the numeric value entered into the browser contains a decimal point , it will be parsed as a C float
* before being returned to the Usermod . The float data type has only 6 - 7 decimal digits of precision , and
* doubles are not supported , numbers will be rounded to the nearest float value when being parsed .
* The range accepted by the input field is + / - 1.175494351e-38 to + / - 3.402823466e+38 .
* - If the numeric value entered into the browser doesn ' t contain a decimal point , it will be parsed as a
* C int32_t ( range : - 2147483648 to 2147483647 ) before being returned to the usermod .
* Overflows or underflows are truncated to the max / min value for an int32_t , and again truncated to the type
* used in the Usermod when reading the value from ArduinoJson .
* - Pin values can be treated differently from an integer value by using the key name " pin "
* - " pin " can contain a single or array of integer values
* - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins
* - Red color indicates a conflict . Yellow color indicates a pin with a warning ( e . g . an input - only pin )
* - Tip : use int8_t to store the pin value in the Usermod , so a - 1 value ( pin not set ) can be used
*
* See usermod_v2_auto_save . h for an example that saves Flash space by reusing ArduinoJson key name strings
*
* If you need a dedicated settings page with custom layout for your Usermod , that takes a lot more work .
* You will have to add the setting to the HTML , xml . cpp and set . cpp manually .
* See the WLED Soundreactive fork ( code and wiki ) for reference . https : //github.com/atuline/WLED
*
* I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings !
*/
void addToConfig ( JsonObject & root )
{
JsonObject top = root . createNestedObject ( FPSTR ( _name ) ) ;
top [ FPSTR ( _enabled ) ] = enabled ;
JsonObject amic = top . createNestedObject ( FPSTR ( _analogmic ) ) ;
amic [ " pin " ] = audioPin ;
JsonObject dmic = top . createNestedObject ( FPSTR ( _digitalmic ) ) ;
dmic [ F ( " type " ) ] = dmType ;
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 ( " cfg " ) ;
cfg [ F ( " squelch " ) ] = soundSquelch ;
cfg [ F ( " gain " ) ] = sampleGain ;
cfg [ F ( " AGC " ) ] = soundAgc ;
JsonObject sync = top . createNestedObject ( " sync " ) ;
sync [ F ( " port " ) ] = audioSyncPort ;
2022-07-07 23:07:20 +02:00
sync [ F ( " mode " ) ] = audioSyncEnabled ;
2022-06-17 16:24:25 +02:00
}
/*
* readFromConfig ( ) can be used to read back the custom settings you added with addToConfig ( ) .
* This is called by WLED when settings are loaded ( currently this only happens immediately after boot , or after saving on the Usermod Settings page )
*
* readFromConfig ( ) is called BEFORE setup ( ) . This means you can use your persistent values in setup ( ) ( e . g . pin assignments , buffer sizes ) ,
* but also that if you want to write persistent values to a dynamic buffer , you ' d need to allocate it here instead of in setup .
* If you don ' t know what that is , don ' t fret . It most likely doesn ' t affect your use case : )
*
* Return true in case the config values returned from Usermod Settings were complete , or false if you ' d like WLED to save your defaults to disk ( so any missing values are editable in Usermod Settings )
*
* getJsonValue ( ) returns false if the value is missing , or copies the value into the variable provided and returns true if the value is present
* The configComplete variable is true only if the " exampleUsermod " object and all values are present . If any values are missing , WLED will know to call addToConfig ( ) to save them
*
* This function is guaranteed to be called on boot , but could also be called every time settings are updated
*/
bool readFromConfig ( JsonObject & root )
{
JsonObject top = root [ FPSTR ( _name ) ] ;
bool configComplete = ! top . isNull ( ) ;
configComplete & = getJsonValue ( top [ FPSTR ( _enabled ) ] , enabled ) ;
configComplete & = getJsonValue ( top [ FPSTR ( _analogmic ) ] [ " pin " ] , audioPin ) ;
configComplete & = getJsonValue ( top [ FPSTR ( _digitalmic ) ] [ " type " ] , dmType ) ;
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 [ " cfg " ] [ F ( " squelch " ) ] , soundSquelch ) ;
configComplete & = getJsonValue ( top [ " cfg " ] [ F ( " gain " ) ] , sampleGain ) ;
configComplete & = getJsonValue ( top [ " cfg " ] [ F ( " AGC " ) ] , soundAgc ) ;
2022-07-07 23:07:20 +02:00
configComplete & = getJsonValue ( top [ " sync " ] [ F ( " port " ) ] , audioSyncPort ) ;
configComplete & = getJsonValue ( top [ " sync " ] [ F ( " mode " ) ] , audioSyncEnabled ) ;
2022-06-17 16:24:25 +02:00
return configComplete ;
}
void appendConfigData ( )
{
oappend ( SET_F ( " dd=addDropdown('AudioReactive','digitalmic:type'); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Generic Analog',0); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Generic I2S',1); " ) ) ;
oappend ( SET_F ( " addOption(dd,'ES7243',2); " ) ) ;
oappend ( SET_F ( " addOption(dd,'SPH0654',3); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Generic I2S with Mclk',4); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Generic I2S PDM',5); " ) ) ;
oappend ( SET_F ( " dd=addDropdown('AudioReactive','cfg: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); " ) ) ;
2022-07-07 23:07:20 +02:00
oappend ( SET_F ( " dd=addDropdown('AudioReactive','sync:mode'); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Off',0); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Send',1); " ) ) ;
oappend ( SET_F ( " addOption(dd,'Receive',2); " ) ) ;
2022-06-21 22:49:45 +02:00
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:type',1,'<i>requires reboot!</i>'); " ) ) ; // 0 is field type, 1 is actual field
2022-06-17 16:24:25 +02:00
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',0,'I2S SD'); " ) ) ;
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',1,'I2S WS'); " ) ) ;
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',2,'I2S SCK'); " ) ) ;
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',3,'I2S Master CLK'); " ) ) ;
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',4,'I2C SDA'); " ) ) ;
oappend ( SET_F ( " addInfo('AudioReactive:digitalmic:pin[]',5,'I2C SCL'); " ) ) ;
}
/*
* handleOverlayDraw ( ) is called just before every show ( ) ( LED strip update frame ) after effects have set the colors .
* Use this to blank out some LEDs or set them to a different color regardless of the set effect mode .
* Commonly used for custom clocks ( Cronixie , 7 segment )
*/
//void handleOverlayDraw()
//{
//strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black
//}
/*
* getId ( ) allows you to optionally give your V2 usermod an unique ID ( please define it in const . h ! ) .
* This could be used in the future for the system to determine whether your usermod is installed .
*/
uint16_t getId ( )
{
return USERMOD_ID_AUDIOREACTIVE ;
}
} ;
// strings to reduce flash memory usage (used more than twice)
const char AudioReactive : : _name [ ] PROGMEM = " AudioReactive " ;
const char AudioReactive : : _enabled [ ] PROGMEM = " enabled " ;
2022-07-03 23:00:32 +02:00
const char AudioReactive : : _inputLvl [ ] PROGMEM = " inputLevel " ;
2022-06-17 16:24:25 +02:00
const char AudioReactive : : _analogmic [ ] PROGMEM = " analogmic " ;
const char AudioReactive : : _digitalmic [ ] PROGMEM = " digitalmic " ;
2022-08-06 16:48:26 +02:00
const char AudioReactive : : UDP_SYNC_HEADER [ ] PROGMEM = " 00002 " ; // new sync header version, as format no longer compatible with previous structure
const char AudioReactive : : UDP_SYNC_HEADER_v1 [ ] PROGMEM = " 00001 " ; // old sync header version - need to add backwards-compatibility feature