Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 30 additions & 18 deletions usermods/audioreactive/audio_reactive.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
#define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3)
#endif

// Analog mic with DMA ADC sampling is only supported on ESP32-S2, ESP32-C3 and ESP32-S3 on IDF >= 4.4.0
#if !defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)))
#define AR_DMA_ADC_SAMPLING
#endif

// Comment/Uncomment to toggle usb serial debugging
// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter)
// #define FFT_SAMPLING_LOG // FFT result debugging
Expand Down Expand Up @@ -910,7 +915,7 @@ void FFTcode(void * parameter)
haveNewFFTResult = true;

#if !defined(I2S_GRAB_ADC1_COMPLETELY)
if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC
if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_ADC)) // the "delay trick" does not help for analog ADC
#endif
{
#ifdef FFT_USE_SLIDING_WINDOW
Expand Down Expand Up @@ -1236,7 +1241,7 @@ class AudioReactive : public Usermod {
static const char _name[];
static const char _enabled[];
static const char _inputLvl[];
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
static const char _analogmic[];
#endif
static const char _digitalmic[];
Expand Down Expand Up @@ -1964,14 +1969,14 @@ class AudioReactive : public Usermod {

useInputFilter = 2; // default: DC blocker
switch (dmType) {
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)
// stub cases for not-yet-supported I2S modes on other ESP32 chips
#if !defined(CONFIG_IDF_TARGET_ESP32) && !defined(AR_DMA_ADC_SAMPLING)
// stub cases for ADC analog on unsupported targets
case 0: //ADC analog
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
case 5: //PDM Microphone
case 51: //legacy PDM Microphone
#endif
#endif
case 1:
DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE);
Expand Down Expand Up @@ -2085,14 +2090,21 @@ class AudioReactive : public Usermod {
audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode
break;

#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
// ADC over I2S is only possible on "classic" ESP32
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
// ADC over I2S is only possible on "classic" ESP32 and on ESP32S2, ESP32C3, ESP32S3 with IDF >= 4.4.0
case 0:
default:
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
useInputFilter = 1; // PDM bandpass filter seems to work well for analog, too
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
// ADC over I2S is only possible on "classic" ESP32
DEBUGSR_PRINTLN(F("AR: Analog Microphone (I2S ADC)"));
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
delay(100);
#else
// use ADC DMA on ESP32S2, ESP32C3, ESP32S3
DEBUGSR_PRINTLN(F("AR: Analog Microphone (DMA ADC)"));
audioSource = new DMAadcSource(SAMPLE_RATE, samplesFFT);
#endif
if (audioSource) audioSource->initialize(audioPin);
break;
#endif
Expand Down Expand Up @@ -2614,7 +2626,7 @@ class AudioReactive : public Usermod {
// Analog or I2S digital input
if (audioSource && (audioSource->isInitialized())) {
// audio source successfully configured
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
if (audioSource->getType() == AudioSource::Type_ADC) {
infoArr.add(F("ADC analog"));
} else {
if (dmType != 51)
Expand Down Expand Up @@ -2798,7 +2810,7 @@ class AudioReactive : public Usermod {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
#ifdef ARDUINO_ARCH_ESP32
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
JsonObject amic = top.createNestedObject(FPSTR(_analogmic));
amic["pin"] = audioPin;
#endif
Expand Down Expand Up @@ -2881,19 +2893,19 @@ class AudioReactive : public Usermod {

configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
#ifdef ARDUINO_ARCH_ESP32
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
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 !defined(CONFIG_IDF_TARGET_ESP32) && !defined(AR_DMA_ADC_SAMPLING)
if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM
if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM
#endif
#else
if (dmType == 5) useInputFilter = 1; // enable filter for PDM
if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM
Expand Down Expand Up @@ -2951,9 +2963,9 @@ class AudioReactive : public Usermod {
oappend(SET_F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style
oappend(SET_F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[]
oappend(SET_F("addInfo(ux+':help',0,'<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/soundreactive/Sound-Settings&quot;\" type=\"button\">?</button>');"));
#ifdef ARDUINO_ARCH_ESP32
#ifdef ARDUINO_ARCH_ESP32
//WLEDMM: add defaults
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -S3/-S2/-C3 don't support analog audio
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
#ifdef AUDIOPIN
oappend(SET_F("xOpt(ux+':analogmic:pin',1,' ⎌',")); oappendi(AUDIOPIN); oappend(");");
#endif
Expand All @@ -2966,7 +2978,7 @@ class AudioReactive : public Usermod {
#else
oappend(SET_F("addOption(dd,'None - network receive only',254);"));
#endif
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
#if SR_DMTYPE==0
oappend(SET_F("addOption(dd,'Generic Analog (⎌)',0);"));
#else
Expand Down Expand Up @@ -3234,7 +3246,7 @@ class AudioReactive : public Usermod {
const char AudioReactive::_name[] PROGMEM = "AudioReactive";
const char AudioReactive::_enabled[] PROGMEM = "enabled";
const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel";
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(ARDUINO_ARCH_ESP32) && (defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING))
const char AudioReactive::_analogmic[] PROGMEM = "analogmic";
#endif
const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic";
Expand Down
176 changes: 170 additions & 6 deletions usermods/audioreactive/audio_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class AudioSource {
virtual bool isInitialized(void) {return(_initialized);}

/* identify Audiosource type - I2S-ADC or I2S-digital */
typedef enum{Type_unknown=0, Type_I2SAdc=1, Type_I2SDigital=2} AudioSourceType;
typedef enum{Type_unknown=0, Type_ADC=1, Type_I2SDigital=2} AudioSourceType;
virtual AudioSourceType getType(void) {return(Type_I2SDigital);} // default is "I2S digital source" - ADC type overrides this method

protected:
Expand Down Expand Up @@ -947,10 +947,8 @@ class AC101Source : public I2SSource {

};

#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
#if !defined(CONFIG_IDF_TARGET_ESP32) || !defined(AR_DMA_ADC_SAMPLING)
#warning this MCU does not support analog sound input on IDF versions < 4.4.0
#endif

#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
Expand Down Expand Up @@ -985,7 +983,7 @@ class I2SAdcSource : public I2SSource {
}

/* identify Audiosource type - I2S-ADC*/
AudioSourceType getType(void) {return(Type_I2SAdc);}
AudioSourceType getType(void) {return(Type_ADC);}

void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) {
DEBUGSR_PRINTLN("I2SAdcSource:: initialize().");
Expand Down Expand Up @@ -1144,6 +1142,172 @@ class I2SAdcSource : public I2SSource {
int8_t _audioPin;
int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined"
};
#elif defined(AR_DMA_ADC_SAMPLING)

/* ADC sampling with DMA
This microphone is an ADC pin sampled via ADC1 in continuous mode
This allows to sample in the background with high sample rates and minimal CPU load
note: only ADC1 channels can be used (ADC2 is used for WiFi)
ESP32 is not implemented as it supports I2S for ADC sampling (see above)
*/

#include "driver/adc.h"
#include "hal/adc_types.h"
#define ADC_TIMEOUT 30 // Timout for one full frame of samples in ms (TODO: could use (FFT_MIN_CYCLE + 5) but need to move the ifdefs before the include in the cpp file)
#define ADC_RESULT_BYTE SOC_ADC_DIGI_RESULT_BYTES //for C3 & S3 this is 4 bytes, S2 is 2 bytes, first 12bits is ADC result, see adc_digi_output_data_t
#ifdef CONFIG_IDF_TARGET_ESP32C3
#define MAX_ADC1_CHANNEL 4 // C3 has 5 channels (0-4)
#else
#define MAX_ADC1_CHANNEL 9 // ESP32, S2, S3 have 10 channels (0-9)
#endif

class DMAadcSource : public AudioSource {
public:
DMAadcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :
AudioSource(sampleRate, blockSize, sampleScale, false) {
// ADC continuous mode configuration
adc_dma_config = {
.max_store_buf_size = (unsigned)blockSize * ADC_RESULT_BYTE, // internal storage of DMA driver (in bytes, one sample is 4 bytes on C3&S3, 2bytes on S2 note: using 2x buffer size would reduce overflows but can add latency
.conv_num_each_intr = (unsigned)blockSize * ADC_RESULT_BYTE, // number of bytes per interrupt (or per frame, one sample contains 12bit of sample data)
.adc1_chan_mask = 0, // ADC1 channel mask (set to correct channel in initialize())
.adc2_chan_mask = 0, // dont use adc2 (used for wifi)
};

adcpattern = {
.atten = ADC_ATTEN_DB_11, // approx. 0-2.5V input range
.channel = 0, // channel mask (set to correct channel in initialize())
.unit = 0, // use ADC1
.bit_width = SOC_ADC_DIGI_MAX_BITWIDTH, // set to 12bit
};

dig_cfg = {
.conv_limit_en = 0, // disable limit (does not work right if enabled)
.conv_limit_num = 255, // set to max just in case
.pattern_num = 1, // single channel sampling
.adc_pattern = &adcpattern, // Pattern configuration
.sample_freq_hz = sampleRate, // sample frequency in Hz
.conv_mode = ADC_CONV_SINGLE_UNIT_1, // use ADC1 only
#ifdef CONFIG_IDF_TARGET_ESP32S2
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE1, // S2 and ESP32 use TYPE1 (there is an error about this for S2 in IDF example)
#else
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2, // C3 and S3 use TYPE2 format
#endif
};
}

/* identify Audiosource type - ADC*/
AudioSourceType getType(void) {return(Type_ADC);}

void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) {
DEBUGSR_PRINTLN(F("DMAadcSource::initialize()"));
_myADCchannel = 0x0F;
if(!pinManager.allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) {
DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin);
return;
}
_audioPin = audioPin;
// Determine Analog channel. Only Channels on ADC1 are supported
int8_t channel = digitalPinToAnalogChannel(_audioPin);
if (channel > MAX_ADC1_CHANNEL) {
DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin);
return;
} else {
_myADCchannel = channel;
}
adc_dma_config.adc1_chan_mask = (1 << channel); // update mask in DMA config
adcpattern.channel = channel; // update pattern config
if (init_adc_continuous() != ESP_OK)
return;
adc_digi_start(); //start sampling
_initialized = true;
}

void getSamples(float *buffer, uint16_t num_samples) {


Serial.println(ESP_IDF_VERSION);
if (!_initialized) return;
int32_t framesize = num_samples * ADC_RESULT_BYTE; // size of one sample frame in bytes
uint8_t result[framesize]; // create a read buffer
uint32_t ret_num;
uint32_t totalbytes = 0;
uint32_t j = 0;
esp_err_t err;
do {
err = adc_digi_read_bytes(result, framesize, &ret_num, ADC_TIMEOUT); // read samples
if ((err == ESP_OK || err == ESP_ERR_INVALID_STATE) && ret_num > 0) { // in invalid sate (internal buffer overrun), still read the last valid sample, then reset the ADC DMA afterwards (better than not having samples at all)
totalbytes += ret_num; // after an error, DMA buffer can be misaligned, returning partial frames. Found no solution to re-align or flush the buffers, seems to be yet another IDF4 bug

if (totalbytes > framesize) { // got too many bytes to fit sample buffer
ret_num -= totalbytes - framesize; // discard extra samples
}
for (int i = 0; i < ret_num; i += ADC_RESULT_BYTE) {
adc_digi_output_data_t *p = reinterpret_cast<adc_digi_output_data_t*>(&result[i]);
buffer[j++] = float((int(p->val & 0x0FFF))); // get the 12bit sample data and convert to float note: works on both format types
// TODO: for integer math: when scaling up to 16bit: compared to I2S mic the scaling seems about the same when not shifting at all, so need to divide by 16 after FFT if scaling up to 16bit
}
} else { // no samples or other error: usually ESP_ERR_TIMEOUT (if DMA has stopped for some reason)
reset_DMA_ADC();
DEBUGSR_PRINTF("ADC ERROR!\n");
return; // something went very wrong, just exit
}
} while (totalbytes < framesize); // read more samples if a partial frame was returned (data is still consistent in split frames)

// remove DC TODO: should really do this in int on C3 & S2... -> needs an update after PR #248 is merged
int32_t sum = 0;
for (int i = 0; i < num_samples; i++) sum += buffer[i];
int32_t mean = sum / num_samples;
for (int i = 0; i < num_samples; i++) buffer[i] -= mean; //uses static mean, as it should not change too much over time, deducted above

if (err == ESP_ERR_INVALID_STATE) { // error reading data, error means buffer overrun, need to fully reset the DMA ADC to make it work again
DEBUGSR_PRINTF("ADC BFR OVERFLOW, RESETTING ADC\n");
reset_DMA_ADC();
}
}

void deinitialize() {
pinManager.deallocatePin(_audioPin, PinOwner::UM_Audioreactive);
_initialized = false;
_myADCchannel = 0x0F;
esp_err_t err;
adc_digi_stop();
delay(50); // just in case, give it some time
err = adc_digi_deinitialize();
if (err != ESP_OK) {
DEBUGSR_PRINTF("Failed deinit ADC: %d\n", err);
}
}

private:
adc_digi_init_config_t adc_dma_config;
adc_digi_pattern_config_t adcpattern;
adc_digi_configuration_t dig_cfg;
int8_t _audioPin;
int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined"

// Initialize ADC continuous mode with stored settings
esp_err_t init_adc_continuous() {
esp_err_t err = adc_digi_initialize(&adc_dma_config);
if (err != ESP_OK) {
DEBUGSR_PRINTF("Failed init ADC DMA: %d\n", err);
return err;
}

err = adc_digi_controller_configure(&dig_cfg);
if (err != ESP_OK) {
DEBUGSR_PRINTF("Failed init ADC sampling: %d\n", err);
}
return err;
}

void reset_DMA_ADC(void) {
adc_digi_stop();
adc_digi_deinitialize();
//delay(1); // TODO: need any delay? seems to work fine without it and this code can be invoked at any time, so do not waste time here
init_adc_continuous();
adc_digi_start(); //start sampling
}
};
#endif

/* SPH0645 Microphone
Expand Down
Loading