Skip to content

Commit e4fa40b

Browse files
committed
Adding new audio source: DMA ADC: analog input support for C3, S2, S3
1 parent af3476c commit e4fa40b

2 files changed

Lines changed: 200 additions & 24 deletions

File tree

usermods/audioreactive/audio_reactive.h

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
#define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3)
6767
#endif
6868

69+
// Analog mic with DMA ADC sampling is only supported on ESP32-S2, ESP32-C3 and ESP32-S3 on IDF >= 4.4.0
70+
#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)))
71+
#define AR_DMA_ADC_SAMPLING
72+
#endif
73+
6974
// Comment/Uncomment to toggle usb serial debugging
7075
// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter)
7176
// #define FFT_SAMPLING_LOG // FFT result debugging
@@ -910,7 +915,7 @@ void FFTcode(void * parameter)
910915
haveNewFFTResult = true;
911916

912917
#if !defined(I2S_GRAB_ADC1_COMPLETELY)
913-
if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC
918+
if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_ADC)) // the "delay trick" does not help for analog ADC
914919
#endif
915920
{
916921
#ifdef FFT_USE_SLIDING_WINDOW
@@ -1236,7 +1241,7 @@ class AudioReactive : public Usermod {
12361241
static const char _name[];
12371242
static const char _enabled[];
12381243
static const char _inputLvl[];
1239-
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
1244+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
12401245
static const char _analogmic[];
12411246
#endif
12421247
static const char _digitalmic[];
@@ -1964,14 +1969,14 @@ class AudioReactive : public Usermod {
19641969

19651970
useInputFilter = 2; // default: DC blocker
19661971
switch (dmType) {
1967-
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)
1968-
// stub cases for not-yet-supported I2S modes on other ESP32 chips
1972+
#if !defined(CONFIG_IDF_TARGET_ESP32) && !defined(AR_DMA_ADC_SAMPLING)
1973+
// stub cases for ADC analog on unsupported targets
19691974
case 0: //ADC analog
1975+
#endif
19701976
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
19711977
case 5: //PDM Microphone
19721978
case 51: //legacy PDM Microphone
19731979
#endif
1974-
#endif
19751980
case 1:
19761981
DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));
19771982
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE);
@@ -2085,14 +2090,21 @@ class AudioReactive : public Usermod {
20852090
audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode
20862091
break;
20872092

2088-
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
2089-
// ADC over I2S is only possible on "classic" ESP32
2093+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
2094+
// ADC over I2S is only possible on "classic" ESP32 and on ESP32S2, ESP32C3, ESP32S3 with IDF >= 4.4.0
20902095
case 0:
20912096
default:
2092-
DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only)."));
20932097
useInputFilter = 1; // PDM bandpass filter seems to work well for analog, too
2098+
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
2099+
// ADC over I2S is only possible on "classic" ESP32
2100+
DEBUGSR_PRINTLN(F("AR: Analog Microphone (I2S ADC)"));
20942101
audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);
20952102
delay(100);
2103+
#else
2104+
// use ADC DMA on ESP32S2, ESP32C3, ESP32S3
2105+
DEBUGSR_PRINTLN(F("AR: Analog Microphone (DMA ADC)"));
2106+
audioSource = new DMAadcSource(SAMPLE_RATE, samplesFFT);
2107+
#endif
20962108
if (audioSource) audioSource->initialize(audioPin);
20972109
break;
20982110
#endif
@@ -2614,7 +2626,7 @@ class AudioReactive : public Usermod {
26142626
// Analog or I2S digital input
26152627
if (audioSource && (audioSource->isInitialized())) {
26162628
// audio source successfully configured
2617-
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
2629+
if (audioSource->getType() == AudioSource::Type_ADC) {
26182630
infoArr.add(F("ADC analog"));
26192631
} else {
26202632
if (dmType != 51)
@@ -2798,7 +2810,7 @@ class AudioReactive : public Usermod {
27982810
JsonObject top = root.createNestedObject(FPSTR(_name));
27992811
top[FPSTR(_enabled)] = enabled;
28002812
#ifdef ARDUINO_ARCH_ESP32
2801-
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
2813+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
28022814
JsonObject amic = top.createNestedObject(FPSTR(_analogmic));
28032815
amic["pin"] = audioPin;
28042816
#endif
@@ -2881,19 +2893,19 @@ class AudioReactive : public Usermod {
28812893

28822894
configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
28832895
#ifdef ARDUINO_ARCH_ESP32
2884-
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
2896+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
28852897
configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin);
28862898
#else
28872899
audioPin = -1; // MCU does not support analog mic
28882900
#endif
28892901

28902902
configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType);
2891-
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)
2903+
#if !defined(CONFIG_IDF_TARGET_ESP32) && !defined(AR_DMA_ADC_SAMPLING)
28922904
if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog
2893-
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
2905+
#endif
2906+
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
28942907
if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM
28952908
if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM
2896-
#endif
28972909
#else
28982910
if (dmType == 5) useInputFilter = 1; // enable filter for PDM
28992911
if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM
@@ -2951,9 +2963,9 @@ class AudioReactive : public Usermod {
29512963
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
29522964
oappend(SET_F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[]
29532965
oappend(SET_F("addInfo(ux+':help',0,'<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/soundreactive/Sound-Settings&quot;\" type=\"button\">?</button>');"));
2954-
#ifdef ARDUINO_ARCH_ESP32
2966+
#ifdef ARDUINO_ARCH_ESP32
29552967
//WLEDMM: add defaults
2956-
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -S3/-S2/-C3 don't support analog audio
2968+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
29572969
#ifdef AUDIOPIN
29582970
oappend(SET_F("xOpt(ux+':analogmic:pin',1,' ⎌',")); oappendi(AUDIOPIN); oappend(");");
29592971
#endif
@@ -2966,7 +2978,7 @@ class AudioReactive : public Usermod {
29662978
#else
29672979
oappend(SET_F("addOption(dd,'None - network receive only',254);"));
29682980
#endif
2969-
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
2981+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING)
29702982
#if SR_DMTYPE==0
29712983
oappend(SET_F("addOption(dd,'Generic Analog (⎌)',0);"));
29722984
#else
@@ -3234,7 +3246,7 @@ class AudioReactive : public Usermod {
32343246
const char AudioReactive::_name[] PROGMEM = "AudioReactive";
32353247
const char AudioReactive::_enabled[] PROGMEM = "enabled";
32363248
const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel";
3237-
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
3249+
#if defined(ARDUINO_ARCH_ESP32) && (defined(CONFIG_IDF_TARGET_ESP32) || defined(AR_DMA_ADC_SAMPLING))
32383250
const char AudioReactive::_analogmic[] PROGMEM = "analogmic";
32393251
#endif
32403252
const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic";

usermods/audioreactive/audio_source.h

Lines changed: 170 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class AudioSource {
164164
virtual bool isInitialized(void) {return(_initialized);}
165165

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

170170
protected:
@@ -947,10 +947,8 @@ class AC101Source : public I2SSource {
947947

948948
};
949949

950-
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
951-
#if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC)
952-
#warning this MCU does not support analog sound input
953-
#endif
950+
#if !defined(CONFIG_IDF_TARGET_ESP32) || !defined(AR_DMA_ADC_SAMPLING)
951+
#warning this MCU does not support analog sound input on IDF versions < 4.4.0
954952
#endif
955953

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

987985
/* identify Audiosource type - I2S-ADC*/
988-
AudioSourceType getType(void) {return(Type_I2SAdc);}
986+
AudioSourceType getType(void) {return(Type_ADC);}
989987

990988
void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) {
991989
DEBUGSR_PRINTLN("I2SAdcSource:: initialize().");
@@ -1144,6 +1142,172 @@ class I2SAdcSource : public I2SSource {
11441142
int8_t _audioPin;
11451143
int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined"
11461144
};
1145+
#elif defined(AR_DMA_ADC_SAMPLING)
1146+
1147+
/* ADC sampling with DMA
1148+
This microphone is an ADC pin sampled via ADC1 in continuous mode
1149+
This allows to sample in the background with high sample rates and minimal CPU load
1150+
note: only ADC1 channels can be used (ADC2 is used for WiFi)
1151+
ESP32 is not implemented as it supports I2S for ADC sampling (see above)
1152+
*/
1153+
1154+
#include "driver/adc.h"
1155+
#include "hal/adc_types.h"
1156+
#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)
1157+
#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
1158+
#ifdef CONFIG_IDF_TARGET_ESP32C3
1159+
#define MAX_ADC1_CHANNEL 4 // C3 has 5 channels (0-4)
1160+
#else
1161+
#define MAX_ADC1_CHANNEL 9 // ESP32, S2, S3 have 10 channels (0-9)
1162+
#endif
1163+
1164+
class DMAadcSource : public AudioSource {
1165+
public:
1166+
DMAadcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :
1167+
AudioSource(sampleRate, blockSize, sampleScale, false) {
1168+
// ADC continuous mode configuration
1169+
adc_dma_config = {
1170+
.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
1171+
.conv_num_each_intr = (unsigned)blockSize * ADC_RESULT_BYTE, // number of bytes per interrupt (or per frame, one sample contains 12bit of sample data)
1172+
.adc1_chan_mask = 0, // ADC1 channel mask (set to correct channel in initialize())
1173+
.adc2_chan_mask = 0, // dont use adc2 (used for wifi)
1174+
};
1175+
1176+
adcpattern = {
1177+
.atten = ADC_ATTEN_DB_11, // approx. 0-2.5V input range
1178+
.channel = 0, // channel mask (set to correct channel in initialize())
1179+
.unit = 0, // use ADC1
1180+
.bit_width = SOC_ADC_DIGI_MAX_BITWIDTH, // set to 12bit
1181+
};
1182+
1183+
dig_cfg = {
1184+
.conv_limit_en = 0, // disable limit (does not work right if enabled)
1185+
.conv_limit_num = 255, // set to max just in case
1186+
.pattern_num = 1, // single channel sampling
1187+
.adc_pattern = &adcpattern, // Pattern configuration
1188+
.sample_freq_hz = sampleRate, // sample frequency in Hz
1189+
.conv_mode = ADC_CONV_SINGLE_UNIT_1, // use ADC1 only
1190+
#ifdef CONFIG_IDF_TARGET_ESP32S2
1191+
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE1, // S2 and ESP32 use TYPE1 (there is an error about this for S2 in IDF example)
1192+
#else
1193+
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE2, // C3 and S3 use TYPE2 format
1194+
#endif
1195+
};
1196+
}
1197+
1198+
/* identify Audiosource type - ADC*/
1199+
AudioSourceType getType(void) {return(Type_ADC);}
1200+
1201+
void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) {
1202+
DEBUGSR_PRINTLN(F("DMAadcSource::initialize()"));
1203+
_myADCchannel = 0x0F;
1204+
if(!pinManager.allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) {
1205+
DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin);
1206+
return;
1207+
}
1208+
_audioPin = audioPin;
1209+
// Determine Analog channel. Only Channels on ADC1 are supported
1210+
int8_t channel = digitalPinToAnalogChannel(_audioPin);
1211+
if (channel > MAX_ADC1_CHANNEL) {
1212+
DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin);
1213+
return;
1214+
} else {
1215+
_myADCchannel = channel;
1216+
}
1217+
adc_dma_config.adc1_chan_mask = (1 << channel); // update mask in DMA config
1218+
adcpattern.channel = channel; // update pattern config
1219+
if (init_adc_continuous() != ESP_OK)
1220+
return;
1221+
adc_digi_start(); //start sampling
1222+
_initialized = true;
1223+
}
1224+
1225+
void getSamples(float *buffer, uint16_t num_samples) {
1226+
1227+
1228+
Serial.println(ESP_IDF_VERSION);
1229+
if (!_initialized) return;
1230+
int32_t framesize = num_samples * ADC_RESULT_BYTE; // size of one sample frame in bytes
1231+
uint8_t result[framesize]; // create a read buffer
1232+
uint32_t ret_num;
1233+
uint32_t totalbytes = 0;
1234+
uint32_t j = 0;
1235+
esp_err_t err;
1236+
do {
1237+
err = adc_digi_read_bytes(result, framesize, &ret_num, ADC_TIMEOUT); // read samples
1238+
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)
1239+
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
1240+
1241+
if (totalbytes > framesize) { // got too many bytes to fit sample buffer
1242+
ret_num -= totalbytes - framesize; // discard extra samples
1243+
}
1244+
for (int i = 0; i < ret_num; i += ADC_RESULT_BYTE) {
1245+
adc_digi_output_data_t *p = reinterpret_cast<adc_digi_output_data_t*>(&result[i]);
1246+
buffer[j++] = float((int(p->val & 0x0FFF))); // get the 12bit sample data and convert to float note: works on both format types
1247+
// 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
1248+
}
1249+
} else { // no samples or other error: usually ESP_ERR_TIMEOUT (if DMA has stopped for some reason)
1250+
reset_DMA_ADC();
1251+
DEBUGSR_PRINTF("ADC ERROR!\n");
1252+
return; // something went very wrong, just exit
1253+
}
1254+
} while (totalbytes < framesize); // read more samples if a partial frame was returned (data is still consistent in split frames)
1255+
1256+
// remove DC TODO: should really do this in int on C3 & S2... -> needs an update after PR #248 is merged
1257+
int32_t sum = 0;
1258+
for (int i = 0; i < num_samples; i++) sum += buffer[i];
1259+
int32_t mean = sum / num_samples;
1260+
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
1261+
1262+
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
1263+
DEBUGSR_PRINTF("ADC BFR OVERFLOW, RESETTING ADC\n");
1264+
reset_DMA_ADC();
1265+
}
1266+
}
1267+
1268+
void deinitialize() {
1269+
pinManager.deallocatePin(_audioPin, PinOwner::UM_Audioreactive);
1270+
_initialized = false;
1271+
_myADCchannel = 0x0F;
1272+
esp_err_t err;
1273+
adc_digi_stop();
1274+
delay(50); // just in case, give it some time
1275+
err = adc_digi_deinitialize();
1276+
if (err != ESP_OK) {
1277+
DEBUGSR_PRINTF("Failed deinit ADC: %d\n", err);
1278+
}
1279+
}
1280+
1281+
private:
1282+
adc_digi_init_config_t adc_dma_config;
1283+
adc_digi_pattern_config_t adcpattern;
1284+
adc_digi_configuration_t dig_cfg;
1285+
int8_t _audioPin;
1286+
int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined"
1287+
1288+
// Initialize ADC continuous mode with stored settings
1289+
esp_err_t init_adc_continuous() {
1290+
esp_err_t err = adc_digi_initialize(&adc_dma_config);
1291+
if (err != ESP_OK) {
1292+
DEBUGSR_PRINTF("Failed init ADC DMA: %d\n", err);
1293+
return err;
1294+
}
1295+
1296+
err = adc_digi_controller_configure(&dig_cfg);
1297+
if (err != ESP_OK) {
1298+
DEBUGSR_PRINTF("Failed init ADC sampling: %d\n", err);
1299+
}
1300+
return err;
1301+
}
1302+
1303+
void reset_DMA_ADC(void) {
1304+
adc_digi_stop();
1305+
adc_digi_deinitialize();
1306+
//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
1307+
init_adc_continuous();
1308+
adc_digi_start(); //start sampling
1309+
}
1310+
};
11471311
#endif
11481312

11491313
/* SPH0645 Microphone

0 commit comments

Comments
 (0)