feat(i2s): extend the mic recorder example to support both DMIC and AMIC

This commit is contained in:
morris
2026-07-31 22:14:16 +08:00
parent c6e80a336a
commit 66c2ce88b3
24 changed files with 630 additions and 404 deletions

View File

@@ -253,14 +253,10 @@ examples/peripherals/i2s/i2s_codec/i2s_es8311:
- esp_hal_i2s
- soc
examples/peripherals/i2s/i2s_recorder:
examples/peripherals/i2s/mic_recorder:
disable:
- if: SOC_SDMMC_HOST_SUPPORTED != 1 or SOC_I2S_SUPPORTED != 1
enable:
- if: SOC_I2S_SUPPORTS_PDM_RX > 0
- if: SOC_I2S_SUPPORTED != 1
depends_components:
- esp_driver_dma
- esp_driver_spi
- esp_driver_i2s
- esp_hal_i2s
- soc

View File

@@ -6,8 +6,3 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(i2s_usb)
# TODO IDF-15784: remove
# esp_codec_dev upstream sets locals without reading them; silence -Wunused-but-set-variable
idf_component_get_property(esp_codec_dev_lib espressif__esp_codec_dev COMPONENT_LIB)
target_compile_options(${esp_codec_dev_lib} PRIVATE -Wno-unused-but-set-variable)

View File

@@ -1,3 +1,3 @@
dependencies:
espressif/usb_device_uac: ^1.1.0
espressif/esp_codec_dev: ^1.3.4
espressif/esp_codec_dev: ^1.6.0

View File

@@ -7,8 +7,3 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
project(es7210_tdm_record_example)
# TODO IDF-15784: remove
# esp_codec_dev upstream sets locals without reading them; silence -Wunused-but-set-variable
idf_component_get_property(esp_codec_dev_lib espressif__esp_codec_dev COMPONENT_LIB)
target_compile_options(${esp_codec_dev_lib} PRIVATE -Wno-unused-but-set-variable)

View File

@@ -1,5 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_codec_dev: ">=1.5.0"
espressif/esp_codec_dev: "^1.6.0"
i2s_examples_common:
path: ${IDF_PATH}/examples/peripherals/i2s/i2s_examples_common

View File

@@ -7,8 +7,3 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
project(i2s-es8311-example)
# TODO IDF-15784: remove
# esp_codec_dev upstream sets locals without reading them; silence -Wunused-but-set-variable
idf_component_get_property(esp_codec_dev_lib espressif__esp_codec_dev COMPONENT_LIB)
target_compile_options(${esp_codec_dev_lib} PRIVATE -Wno-unused-but-set-variable)

View File

@@ -1,5 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_codec_dev: ^1.3.4
espressif/esp_codec_dev: ^1.6.0
i2s_examples_common:
path: ${IDF_PATH}/examples/peripherals/i2s/i2s_examples_common

View File

@@ -1,102 +0,0 @@
| Supported Targets | ESP32 | ESP32-P4 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | --------- |
# I2S Digital Microphone Recording Example
(See the README.md file in the upper level 'examples' directory for more information about examples.)
In this example, we record a sample audio file captured from the digital MEMS microphone on the I2S peripheral using PDM data format.
The audio is recorded into the SDCard using WAVE file format.
| Audio Setting | Value |
|:---:|:---:|
| Sample Rate |44100 Hz|
| Bits per Sample |16 bits|
## How to Use Example
### Hardware Required
* A development board with ESP32 or ESP32S3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.)
* A USB cable for power supply and programming
* A digital microphone (SPK0838HT4H PDM output was used in this example)
The digital PDM microphone is connected on the I2S interface `I2S_NUM_0`.
The default GPIO configuration is the following:
|Mic | GPIO |
|:---------:|:------:|
| PDM Clock | GPIO4 |
| PDM Data | GPIO5 |
The SDCard is connected using SPI peripheral.
| SPI | SDCard | GPIO |
|:----:|:------:|:------:|
| MISO | DAT0 | GPIO17 |
| MOSI | CMD | GPIO16 |
| SCLK | CLK | GPIO18 |
| CS | CD | GPIO19 |
To change the GPIO configuration, see the `Example Configuration` from the menuconfig.
### Configure the Project
```
idf.py menuconfig
```
In the `Example Configuration` menu:
* Use `SDCard Configuration` to assign the SPI peripheral GPIOs.
* Use `I2S MEMS MIC Configuration` to assign the I2S peripheral GPIOs and audio settings.
Optional: If you need, change the other options according to your requirements.
### Build and Flash
Build the project and flash it to the board, then run monitor tool to view serial output:
```
idf.py -p PORT flash monitor
```
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
* [ESP-IDF Getting Started Guide on ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html)
## Example Output
Running this example, you will see the Bits per Sample changes every 5 seconds after you have run this example. You can use `i2s_set_clk` to change the Bits per Sample and the Sample Rate. The output log can be seen below:
```
I (361) pdm_rec_example: PDM microphone recording Example start
I (371) I2S: DMA Malloc info, datalen=blocksize=2048, dma_desc_num=64
I (401) I2S: APLL: Req RATE: 44100, real rate: 88199.977, BITS: 16, CLKM: 1, BCK_M: 8, MCLK: 22579194.000, SCLK: 2822399.250000, diva: 1, divb: 0
I (431) I2S: APLL: Req RATE: 44100, real rate: 88199.977, BITS: 16, CLKM: 1, BCK_M: 8, MCLK: 22579194.000, SCLK: 2822399.250000, diva: 1, divb: 0
I (431) pdm_rec_example: Initializing SD card
I (431) pdm_rec_example: Using SDMMC peripheral
I (441) gpio: GPIO[13]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
Name: USD
Type: SDHC/SDXC
Speed: 20 MHz
Size: 3813MB
I (481) pdm_rec_example: Starting recording for 60 seconds!
I (481) pdm_rec_example: Opening file
I (60451) pdm_rec_example: Recording done!
I (60471) pdm_rec_example: File written on SDCard
I (60471) pdm_rec_example: Card unmounted
```
## Troubleshooting
* Program upload failure
* Hardware connection is not correct: run `idf.py -p PORT monitor`, and reboot your board to see if there are any output logs.
* The baud rate for downloading is too high: lower your baud rate in the `menuconfig` menu, and try again.
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@@ -1,3 +0,0 @@
idf_component_register(SRCS "i2s_recorder_main.c"
PRIV_REQUIRES esp_driver_i2s fatfs i2s_examples_common
INCLUDE_DIRS)

View File

@@ -1,67 +0,0 @@
menu "Example Configuration"
menu "SDCard Configuration"
config EXAMPLE_SPI_MISO_GPIO
int "SPI MISO GPIO"
default 15
help
Set the GPIO number used for MISO from SPI.
config EXAMPLE_SPI_MOSI_GPIO
int "SPI MOSI GPIO"
default 14
help
Set the GPIO number used for MOSI from SPI.
config EXAMPLE_SPI_SCLK_GPIO
int "SPI SCLK GPIO"
default 18
help
Set the GPIO number used for SCLK from SPI.
config EXAMPLE_SPI_CS_GPIO
int "SPI CS GPIO"
default 19
help
Set the GPIO number used for CS from SPI.
endmenu
menu "I2S MEMS MIC Configuration"
config EXAMPLE_SAMPLE_RATE
int "Audio Sample Rate"
default 44100 if SOC_I2S_SUPPORTS_PDM2PCM
default 5644800
help
Set the audio sample rate frequency. Usually 16000 or 44100 Hz if PCM data format supported.
Oversample rate usually can be 2048000 or 5644800 Hz if only raw PDM data format supported.
config EXAMPLE_BIT_SAMPLE
int "Audio Bit Sample"
default 16
help
Define the number of bits for each sample. Default 16 bits per sample.
config EXAMPLE_I2S_DATA_GPIO
int "I2S Data GPIO"
default 5
help
Set the GPIO number used for transmitting/receiving data from I2S.
config EXAMPLE_I2S_CLK_GPIO
int "I2S Clock GPIO"
default 4
help
Set the GPIO number used for the clock line from I2S.
endmenu
config EXAMPLE_REC_TIME
int "Example Recording Time in Seconds"
default 15
help
Set the time for recording audio in seconds.
endmenu

View File

@@ -1,189 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* I2S Digital Microphone Recording Example */
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
#include "driver/spi_common.h"
#include "sdmmc_cmd.h"
#include "format_wav.h"
#include "esp_log.h"
static const char *TAG = "pdm_rec_example";
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
#define NUM_CHANNELS (1) // For mono recording only!
#define SD_MOUNT_POINT "/sdcard"
#define SAMPLE_SIZE (CONFIG_EXAMPLE_BIT_SAMPLE * 1024)
#define BYTE_RATE (CONFIG_EXAMPLE_SAMPLE_RATE * (CONFIG_EXAMPLE_BIT_SAMPLE / 8)) * NUM_CHANNELS
// When testing SD and SPI modes, keep in mind that once the card has been
// initialized in SPI mode, it can not be reinitialized in SD mode without
// toggling power to the card.
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
sdmmc_card_t *card;
i2s_chan_handle_t rx_handle = NULL;
static int16_t i2s_readraw_buff[SAMPLE_SIZE];
size_t bytes_read;
const int WAVE_HEADER_SIZE = 44;
void mount_sdcard(void)
{
esp_err_t ret;
// Options for mounting the filesystem.
// If format_if_mount_failed is set to true, SD card will be partitioned and
// formatted in case when mounting fails.
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 8 * 1024
};
ESP_LOGI(TAG, "Initializing SD card");
spi_bus_config_t bus_cfg = {
.mosi_io_num = CONFIG_EXAMPLE_SPI_MOSI_GPIO,
.miso_io_num = CONFIG_EXAMPLE_SPI_MISO_GPIO,
.sclk_io_num = CONFIG_EXAMPLE_SPI_SCLK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
ret = spi_bus_initialize(host.slot, &bus_cfg, SPI_DMA_CHAN);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize bus.");
return;
}
// This initializes the slot without card detect (CD) and write protect (WP) signals.
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = CONFIG_EXAMPLE_SPI_CS_GPIO;
slot_config.host_id = host.slot;
ret = esp_vfs_fat_sdspi_mount(SD_MOUNT_POINT, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
}
return;
}
// Card has been initialized, print its properties
sdmmc_card_print_info(stdout, card);
}
void record_wav(uint32_t rec_time)
{
// Use POSIX and C standard library functions to work with files.
int flash_wr_size = 0;
ESP_LOGI(TAG, "Opening file");
uint32_t flash_rec_time = BYTE_RATE * rec_time;
const wav_header_t wav_header =
WAV_HEADER_PCM_DEFAULT(flash_rec_time, 16, CONFIG_EXAMPLE_SAMPLE_RATE, 1);
// First check if file exists before creating a new file.
struct stat st;
if (stat(SD_MOUNT_POINT"/record.wav", &st) == 0) {
// Delete it if it exists
unlink(SD_MOUNT_POINT"/record.wav");
}
// Create new WAV file
FILE *f = fopen(SD_MOUNT_POINT"/record.wav", "a");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
// Write the header to the WAV file
fwrite(&wav_header, sizeof(wav_header), 1, f);
// Start recording
while (flash_wr_size < flash_rec_time) {
// Read the RAW samples from the microphone
if (i2s_channel_read(rx_handle, (char *)i2s_readraw_buff, SAMPLE_SIZE, &bytes_read, 1000) == ESP_OK) {
printf("[0] %d [1] %d [2] %d [3]%d ...\n", i2s_readraw_buff[0], i2s_readraw_buff[1], i2s_readraw_buff[2], i2s_readraw_buff[3]);
// Write the samples to the WAV file
fwrite(i2s_readraw_buff, bytes_read, 1, f);
flash_wr_size += bytes_read;
} else {
printf("Read Failed!\n");
}
}
ESP_LOGI(TAG, "Recording done!");
fclose(f);
ESP_LOGI(TAG, "File written on SDCard");
// All done, unmount partition and disable SPI peripheral
esp_vfs_fat_sdcard_unmount(SD_MOUNT_POINT, card);
ESP_LOGI(TAG, "Card unmounted");
// Deinitialize the bus after all devices are removed
spi_bus_free(host.slot);
}
void init_microphone(void)
{
#if SOC_I2S_SUPPORTS_PDM2PCM
ESP_LOGI(TAG, "Receive PDM microphone data in PCM format");
#else
ESP_LOGI(TAG, "Receive PDM microphone data in raw PDM format");
#endif // SOC_I2S_SUPPORTS_PDM2PCM
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(CONFIG_EXAMPLE_SAMPLE_RATE),
/* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
#if SOC_I2S_SUPPORTS_PDM2PCM
.slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
#else
.slot_cfg = I2S_PDM_RX_SLOT_RAW_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
#endif
.gpio_cfg = {
.clk = CONFIG_EXAMPLE_I2S_CLK_GPIO,
.din = CONFIG_EXAMPLE_I2S_DATA_GPIO,
.invert_flags = {
.clk_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
void app_main(void)
{
printf("PDM microphone recording example start\n--------------------------------------\n");
// Mount the SDCard for recording the audio file
mount_sdcard();
// Acquire a I2S PDM channel for the PDM digital microphone
init_microphone();
ESP_LOGI(TAG, "Starting recording for %d seconds!", CONFIG_EXAMPLE_REC_TIME);
// Start Recording
record_wav(CONFIG_EXAMPLE_REC_TIME);
// Stop I2S driver and destroy
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
}

View File

@@ -1,3 +0,0 @@
dependencies:
i2s_examples_common:
path: ${IDF_PATH}/examples/peripherals/i2s/i2s_examples_common

View File

@@ -1,13 +0,0 @@
# SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32', 'esp32s3', 'esp32p4', 'esp32s31'], indirect=['target'])
def test_i2s_recorder_generic(dut: Dut) -> None:
dut.expect('PDM microphone recording example start')
dut.expect('--------------------------------------')
dut.expect('I \\(([0-9]+)\\) pdm_rec_example: Initializing SD card')

View File

@@ -6,4 +6,4 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(esp32_i2s_driver_example)
project(mic_recorder_example)

View File

@@ -0,0 +1,126 @@
| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# I2S Microphone Recording Example
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This example captures 16-bit stereo PCM audio from a microphone and streams it over the console back to the host PC, where it can be reconstructed as a ``.wav`` file. It supports two kinds of microphones, selectable in menuconfig:
* **Digital microphone (PDM)** — one or two PDM digital MEMS microphones sampled through the I2S peripheral's hardware PDM-to-PCM conversion.
* **Analog microphone with ES8389 codec** — the ES8389 ADC samples the analog microphone, then transmits PCM data to the SoC over I2S STD mode. The example configures the codec through the [esp_codec_dev](https://components.espressif.com/components/espressif/esp_codec_dev) component.
The device records the PCM data in RAM, stops the I2S/codec, and prints the audio as chunked Base64 over the console. No SD card or filesystem is used. The accompanying ``pytest`` test reconstructs the WAVE file on the host PC.
| Audio Setting | Value |
|:---:|:---:|
| Sample Rate |16000 Hz|
| Bits per Sample |16 bits|
| Channels |Stereo|
| Recording Time |2 seconds|
These settings are defined as macros at the top of `main/mic_recorder_example_main.c` (see `EXAMPLE_SAMPLE_RATE`, `EXAMPLE_RECORD_TIME_SECONDS`, etc.). The sample rate is shared by both the digital and the analog path.
## How to Use Example
### Hardware Required
* A development board with a SoC that supports the chosen microphone type:
* Digital microphone: hardware PDM-to-PCM conversion (ESP32, ESP32-S3, ESP32-P4, ESP32-S31, etc.).
* Analog microphone: I2S STD mode plus I2C to drive the ES8389 codec (for example the ESP32-S31-Korvo-1 board).
* A USB cable for power supply and programming.
* The microphone(s) matching the selected type.
For stereo PDM capture, two microphones share one data line. Connect the left microphone's `select` pin to GND and the right microphone's `select` pin to VDD. Configure every GPIO according to your board schematic.
### Configure the Project
```
idf.py menuconfig
```
In the `Example Configuration` menu:
* Use `Microphone type` to select between the digital (PDM) and the analog (ES8389) microphone.
* The configuration menu shown below the mic type is updated automatically:
* `PDM MIC Configuration` — assign the PDM clock and data GPIOs (digital microphone).
* `ES8389 Codec Configuration` — assign the I2C and I2S GPIOs, plus the mic gain (analog microphone).
The recording duration is defined by `EXAMPLE_RECORD_TIME_SECONDS` in `main/mic_recorder_example_main.c`. The default 2-second stereo recording occupies 128 KB of RAM. Increasing the recording duration or sample rate increases RAM use proportionally.
### Build and Flash
Build the project and flash it to the board, then run monitor tool to view serial output:
```
idf.py -p PORT flash monitor
```
(To exit the serial monitor, type ``Ctrl-]``.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
* [ESP-IDF Getting Started Guide on ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html)
## Console Output and WAVE Artifact
After recording completes, the device emits an `AUDIO_META` line followed by Base64 chunks bracketed by `AUDIO_BASE64_BEGIN` and `AUDIO_BASE64_END`. The chunks are intentionally short so that the serial stream can be parsed reliably.
A digital-microphone run looks like:
```
PDM MIC recording example start
--------------------------------------
I (...) mic_rec_example: Starting recording for 2 seconds!
I (...) mic_rec_example: Recording done, sending PCM data over console
AUDIO_META sample_rate=16000 bits_per_sample=16 channels=2 data_size=128000 encoding=base64
AUDIO_BASE64_BEGIN
AUDIO_BASE64 <Base64-encoded PCM chunk>
...
AUDIO_BASE64_END
mic recorder example done
```
An analog-microphone run looks the same, except the start line reads `ES8389 MIC recording example start`.
At the default 115200 baud console speed, sending the complete Base64 payload takes roughly 15--20 seconds for the default recording. This happens only after the I2S/codec has stopped, so console throughput cannot cause microphone DMA overflows.
### Getting the Recorded Audio on the Host PC
The Base64 payload printed over the console carries the full PCM recording.
### Run pytest and Save a WAVE File
With the board connected, run the test for the microphone type from the example directory:
Digital PDM microphone:
```
pytest --target esp32s3 --port /dev/ttyUSB0 pytest_mic_record.py::test_mic_recorder_dmic_example
```
Analog ES8389 microphone on ESP32-S31-Korvo-1:
```
pytest --target esp32s31 --port /dev/ttyUSB0 pytest_mic_record.py::test_mic_recorder_amic_example
```
Replace `/dev/ttyUSB0` with your board's serial port. Each test selects its matching `sdkconfig.ci.*` configuration, decodes the serial output, and writes the recording to:
```
<logdir>/mic_recording.wav
```
(`<logdir>` is printed at the start of the pytest run; it is also reported by the ``Saved microphone recording artifact to ...`` log line.) The test both validates the console protocol and saves the captured microphone data as a WAVE artifact.
The resulting `mic_recording.wav` is 16-bit PCM: 16000 Hz sample rate, 2 channels, ~2 seconds long.
Open it with [Audacity](https://www.audacityteam.org/) to listen to the recording or inspect its waveform.
## Troubleshooting
* Analog microphone produces silence or a very low signal
* Check the I2C wiring (SDA/SCL) and that `ES8389 Codec Configuration` matches your board.
* Increase `Mic gain (dB)` in the `Example Configuration` menu.
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "mic_recorder_example_main.c"
PRIV_REQUIRES esp_driver_i2s esp_driver_i2c esp_driver_gpio mbedtls
INCLUDE_DIRS .)

View File

@@ -0,0 +1,94 @@
menu "Example Configuration"
choice EXAMPLE_MIC_TYPE
prompt "Microphone type"
default EXAMPLE_MIC_TYPE_DMIC if SOC_I2S_SUPPORTS_PDM2PCM
default EXAMPLE_MIC_TYPE_AMIC
help
Select the microphone type connected to the board. The example configures the
I2S peripheral accordingly:
- Digital microphone (PDM): the I2S PDM RX mode samples two PDM MEMS
microphones and performs the PDM-to-PCM conversion in hardware.
Only available on chips that support hardware PDM-to-PCM conversion.
- Analog microphone (ES8389 codec): the ES8389 ADC samples the analog
microphone signal, then I2S STD mode receives the codec's PCM stream.
config EXAMPLE_MIC_TYPE_DMIC
bool "Digital microphone (PDM)"
depends on SOC_I2S_SUPPORTS_PDM2PCM
config EXAMPLE_MIC_TYPE_AMIC
bool "Analog microphone (ES8389 codec)"
# defined by the esp_codec_dev component
select CODEC_ES8389_SUPPORT
endchoice
menu "PDM MIC Configuration"
depends on EXAMPLE_MIC_TYPE_DMIC
config EXAMPLE_PDM_CLK_GPIO
int "PDM Clock GPIO"
default 4
help
Set the GPIO number used for the PDM clock line driven by I2S.
config EXAMPLE_PDM_DATA_GPIO
int "PDM Data GPIO"
default 5
help
Set the GPIO number used for receiving PDM data from the microphone.
The left and right PDM microphones share this data pin:
connect the left microphone's select pin to GND
and the right microphone's select pin to VDD.
endmenu
menu "ES8389 Codec Configuration (Analog Microphone)"
depends on EXAMPLE_MIC_TYPE_AMIC
config EXAMPLE_I2C_SDA_IO
int "I2C SDA GPIO"
default 0
help
Set the GPIO number used for the I2C SDA signal towards the ES8389 codec.
config EXAMPLE_I2C_SCL_IO
int "I2C SCL GPIO"
default 1
help
Set the GPIO number used for the I2C SCL signal towards the ES8389 codec.
config EXAMPLE_I2S_MCLK_IO
int "I2S MCLK GPIO (set -1 if the codec does not use an external MCLK)"
default 2
help
Set the GPIO number used for the I2S MCLK signal. When set to -1, no MCLK
is generated and the ES8389 derives its clock from BCLK.
config EXAMPLE_I2S_BCLK_IO
int "I2S BCLK GPIO"
default 3
help
Set the GPIO number used for the I2S BCLK signal towards the ES8389 codec.
config EXAMPLE_I2S_WS_IO
int "I2S WS/LRCK GPIO"
default 4
help
Set the GPIO number used for the I2S WS (LRCK) signal towards the ES8389 codec.
config EXAMPLE_I2S_DIN_IO
int "I2S DIN GPIO"
default 6
help
Set the GPIO number used for receiving the ADC data from the ES8389 codec.
config EXAMPLE_MIC_GAIN
int "Mic gain (dB)"
default 27
range 0 36
help
Set the ES8389 analog microphone input gain in dB.
endmenu
endmenu

View File

@@ -0,0 +1,4 @@
## IDF Component Manager Manifest File
dependencies:
# ESP codec device driver, only used when an analog microphone is selected.
espressif/esp_codec_dev: "^1.6.0"

View File

@@ -0,0 +1,228 @@
/*
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include "sdkconfig.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/base64.h"
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "driver/i2s_std.h"
#include "esp_codec_dev.h"
#include "esp_codec_dev_defaults.h"
static const char *TAG = "mic_rec_example";
/* Both microphone paths produce this PCM format. */
#define EXAMPLE_SAMPLE_RATE 16000
#define EXAMPLE_CHANNEL_COUNT 2
#define EXAMPLE_BITS_PER_SAMPLE 16
#define EXAMPLE_RECORD_TIME_SECONDS 2
#define EXAMPLE_PCM_SIZE (EXAMPLE_SAMPLE_RATE * EXAMPLE_RECORD_TIME_SECONDS * EXAMPLE_CHANNEL_COUNT * EXAMPLE_BITS_PER_SAMPLE / 8)
#define EXAMPLE_DMA_READ_SIZE 1024
#define EXAMPLE_I2C_CLOCK_HZ 100000
#define EXAMPLE_MCLK_MULTIPLE 256 /* Codec MCLK is 256 times the sample rate. */
#define EXAMPLE_STEREO_CHANNEL_MASK 0x03 /* Capture both left and right channels. */
/* A multiple of three produces fixed-size Base64 lines without padding. */
#define EXAMPLE_BASE64_CHUNK_BYTES 72
#define EXAMPLE_BASE64_BUFFER_SIZE (4 * ((EXAMPLE_BASE64_CHUNK_BYTES + 2) / 3) + 1)
static void capture_pcm(i2s_chan_handle_t rx_handle, uint8_t *pcm_data, size_t pcm_size)
{
size_t bytes_recorded = 0;
/* Continue until the RAM buffer contains the requested recording duration. */
while (bytes_recorded < pcm_size) {
size_t bytes_to_read = pcm_size - bytes_recorded;
bytes_to_read = MIN(bytes_to_read, EXAMPLE_DMA_READ_SIZE);
size_t bytes_read = 0;
ESP_ERROR_CHECK(i2s_channel_read(rx_handle, pcm_data + bytes_recorded, bytes_to_read, &bytes_read, 1000));
bytes_recorded += bytes_read;
}
}
static void send_pcm_as_base64(const uint8_t *pcm_data, size_t pcm_size)
{
unsigned char encoded_chunk[EXAMPLE_BASE64_BUFFER_SIZE];
size_t chunks_sent = 0;
/* Framed chunks let the host extract audio while ignoring other console logs. */
printf("AUDIO_BASE64_BEGIN\n");
for (size_t offset = 0; offset < pcm_size; offset += EXAMPLE_BASE64_CHUNK_BYTES) {
size_t chunk_size = pcm_size - offset;
chunk_size = MIN(chunk_size, EXAMPLE_BASE64_CHUNK_BYTES);
size_t encoded_size = 0;
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded_chunk, sizeof(encoded_chunk), &encoded_size,
pcm_data + offset, chunk_size) == 0 ? ESP_OK : ESP_FAIL);
printf("AUDIO_BASE64 %.*s\n", (int)encoded_size, encoded_chunk);
if (++chunks_sent % 16 == 0) {
/* UART output is slow; allow the idle task to run periodically. */
vTaskDelay(1);
}
}
printf("AUDIO_BASE64_END\n");
}
#if CONFIG_EXAMPLE_MIC_TYPE_DMIC
static void record_from_pdm_microphone(uint8_t *pcm_data, size_t pcm_size)
{
/* PDM RX mode drives the microphone clock and converts its PDM stream to PCM. */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
/* In stereo mode, the left slot (select pin pulled down) and the right slot (select pin
* pulled up) are both sampled on the shared data pin, producing interleaved L/R channels. */
.slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(EXAMPLE_BITS_PER_SAMPLE, EXAMPLE_CHANNEL_COUNT),
.gpio_cfg = {
.clk = CONFIG_EXAMPLE_PDM_CLK_GPIO,
.dins[0] = CONFIG_EXAMPLE_PDM_DATA_GPIO,
},
};
i2s_chan_handle_t rx_handle = NULL;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
ESP_LOGI(TAG, "Starting PDM recording for %d seconds!", EXAMPLE_RECORD_TIME_SECONDS);
capture_pcm(rx_handle, pcm_data, pcm_size);
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
}
#endif
#if CONFIG_EXAMPLE_MIC_TYPE_AMIC
static void record_from_es8389_microphone(uint8_t *pcm_data, size_t pcm_size)
{
/* The analog signal is sampled by the ES8389 ADC. I2S RX receives its digital samples
* and, as bus master, generates the BCLK/WS (and optional MCLK) required by the codec. */
i2c_master_bus_handle_t i2c_bus_handle = NULL;
i2c_master_bus_config_t i2c_bus_cfg = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = -1, /* Let the driver select a free I2C port */
.sda_io_num = CONFIG_EXAMPLE_I2C_SDA_IO,
.scl_io_num = CONFIG_EXAMPLE_I2C_SCL_IO,
.glitch_ignore_cnt = 7,
/* Enable the internal pull-ups in case there is no external pull-up on the board.
* External pull-ups are recommended to make the bus more robust. */
.flags.enable_internal_pullup = true,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_handle));
/* I2C configures codec registers; PCM samples use I2S. */
audio_codec_i2c_cfg_t i2c_cfg = {
.bus_handle = i2c_bus_handle,
.addr = ES8389_CODEC_DEFAULT_ADDR,
.clock_speed_hz = EXAMPLE_I2C_CLOCK_HZ,
};
const audio_codec_ctrl_if_t *ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
ESP_ERROR_CHECK(ctrl_if ? ESP_OK : ESP_FAIL);
es8389_codec_cfg_t es8389_cfg = {
.ctrl_if = ctrl_if,
.codec_mode = ESP_CODEC_DEV_WORK_MODE_ADC,
.pa_pin = GPIO_NUM_NC,
/* When MCLK is unused, the ES8389 derives its clock from BCLK. */
.use_mclk = CONFIG_EXAMPLE_I2S_MCLK_IO >= 0,
.mclk_div = EXAMPLE_MCLK_MULTIPLE,
};
const audio_codec_if_t *codec_if = es8389_codec_new(&es8389_cfg);
ESP_ERROR_CHECK(codec_if ? ESP_OK : ESP_FAIL);
/* Recording needs only an RX channel. In master mode, RX also drives BCLK/WS. */
i2s_chan_handle_t rx_handle = NULL;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(EXAMPLE_BITS_PER_SAMPLE, EXAMPLE_CHANNEL_COUNT),
.gpio_cfg = {
.mclk = CONFIG_EXAMPLE_I2S_MCLK_IO,
.bclk = CONFIG_EXAMPLE_I2S_BCLK_IO,
.ws = CONFIG_EXAMPLE_I2S_WS_IO,
.dout = GPIO_NUM_NC,
.din = CONFIG_EXAMPLE_I2S_DIN_IO,
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
/* Pass the already-configured RX channel to the codec data interface. */
audio_codec_i2s_cfg_t i2s_cfg = {
.rx_handle = rx_handle,
};
const audio_codec_data_if_t *data_if = audio_codec_new_i2s_data(&i2s_cfg);
ESP_ERROR_CHECK(data_if ? ESP_OK : ESP_FAIL);
esp_codec_dev_cfg_t dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = codec_if,
.data_if = data_if,
};
esp_codec_dev_handle_t codec_handle = esp_codec_dev_new(&dev_cfg);
ESP_ERROR_CHECK(codec_handle ? ESP_OK : ESP_FAIL);
esp_codec_dev_sample_info_t sample_cfg = {
.bits_per_sample = EXAMPLE_BITS_PER_SAMPLE,
.channel = EXAMPLE_CHANNEL_COUNT,
.channel_mask = EXAMPLE_STEREO_CHANNEL_MASK,
.sample_rate = EXAMPLE_SAMPLE_RATE,
.mclk_multiple = EXAMPLE_MCLK_MULTIPLE,
};
ESP_ERROR_CHECK(esp_codec_dev_open(codec_handle, &sample_cfg));
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(codec_handle, CONFIG_EXAMPLE_MIC_GAIN));
ESP_LOGI(TAG, "ES8389 codec initialized");
ESP_LOGI(TAG, "Starting ES8389 recording for %d seconds!", EXAMPLE_RECORD_TIME_SECONDS);
capture_pcm(rx_handle, pcm_data, pcm_size);
/* Closing the input device disables RX before its channel is deleted. */
ESP_ERROR_CHECK(esp_codec_dev_close(codec_handle));
esp_codec_dev_delete(codec_handle);
ESP_ERROR_CHECK(audio_codec_delete_codec_if(codec_if));
ESP_ERROR_CHECK(audio_codec_delete_data_if(data_if));
ESP_ERROR_CHECK(audio_codec_delete_ctrl_if(ctrl_if));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
ESP_ERROR_CHECK(i2c_del_master_bus(i2c_bus_handle));
}
#endif
void app_main(void)
{
/* Capture to RAM first so slow UART output cannot overflow the I2S DMA buffers. */
uint8_t *pcm_data = calloc(1, EXAMPLE_PCM_SIZE);
ESP_ERROR_CHECK(pcm_data ? ESP_OK : ESP_FAIL);
#if CONFIG_EXAMPLE_MIC_TYPE_DMIC
printf("PDM MIC recording example start\n--------------------------------------\n");
record_from_pdm_microphone(pcm_data, EXAMPLE_PCM_SIZE);
#else
printf("ES8389 MIC recording example start\n--------------------------------------------\n");
record_from_es8389_microphone(pcm_data, EXAMPLE_PCM_SIZE);
#endif
ESP_LOGI(TAG, "Recording done, sending PCM data over console");
printf("AUDIO_META sample_rate=%d bits_per_sample=%d channels=%d data_size=%d encoding=base64\n",
EXAMPLE_SAMPLE_RATE, EXAMPLE_BITS_PER_SAMPLE, EXAMPLE_CHANNEL_COUNT, EXAMPLE_PCM_SIZE);
send_pcm_as_base64(pcm_data, EXAMPLE_PCM_SIZE);
printf("mic recorder example done\n");
free(pcm_data);
}

View File

@@ -0,0 +1,163 @@
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import base64
import logging
import os
import re
import struct
import wave
from dataclasses import dataclass
from pathlib import Path
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
I2S_TARGETS = soc_filtered_targets('SOC_I2S_SUPPORTED == 1')
PDM2PCM_TARGETS = soc_filtered_targets('SOC_I2S_SUPPORTS_PDM2PCM == 1')
MIC_RECORDING_OUTPUT_NAME = 'mic_recording.wav'
# Keep these values in sync with the PCM format constants in the firmware example.
EXPECTED_SAMPLE_RATE_HZ = 16000
EXPECTED_BITS_PER_SAMPLE = 16
EXPECTED_CHANNELS = 2
AUDIO_META_PATTERN = (
r'AUDIO_META sample_rate=(?P<sample_rate>\d+) bits_per_sample=(?P<bits_per_sample>\d+) '
r'channels=(?P<channels>\d+) data_size=(?P<data_size>\d+) encoding=(?P<encoding>\w+)'
)
AUDIO_META_RE = re.compile(AUDIO_META_PATTERN)
AUDIO_CHUNK_PATTERN = r'AUDIO_BASE64 (?P<payload>[A-Za-z0-9+/=]+)'
AUDIO_CHUNK_RE = re.compile(AUDIO_CHUNK_PATTERN)
# The example prints a mic-type specific banner before starting a recording.
DMIC_START_MARKER = 'PDM MIC recording example start'
AMIC_START_MARKER = 'ES8389 MIC recording example start'
DONE_MARKER = 'mic recorder example done'
@dataclass(frozen=True)
class AudioMetadata:
sample_rate: int
bits_per_sample: int
channels: int
data_size: int
encoding: str
@property
def bytes_per_sample(self) -> int:
if self.bits_per_sample % 8:
raise ValueError(f'Unsupported sample size: {self.bits_per_sample} bits')
return self.bits_per_sample // 8
@property
def frame_size(self) -> int:
return self.channels * self.bytes_per_sample
def _parse_audio_metadata(meta_line: str) -> AudioMetadata:
match = AUDIO_META_RE.fullmatch(meta_line)
if not match:
raise ValueError(f'Invalid audio metadata line: {meta_line}')
return AudioMetadata(
sample_rate=int(match.group('sample_rate')),
bits_per_sample=int(match.group('bits_per_sample')),
channels=int(match.group('channels')),
data_size=int(match.group('data_size')),
encoding=match.group('encoding'),
)
def _collect_base64_payload(dut: Dut) -> list[str]:
payload_chunks: list[str] = []
while True:
# Accept only framed audio lines, ignoring unrelated console output.
match = dut.expect(rf'(?P<line>AUDIO_BASE64_END|{AUDIO_CHUNK_PATTERN}\r?\n)', timeout=40)
line = match.group('line').decode('utf-8').strip()
if line == 'AUDIO_BASE64_END':
return payload_chunks
chunk_match = AUDIO_CHUNK_RE.fullmatch(line)
assert chunk_match is not None
payload_chunks.append(chunk_match.group('payload'))
def _decode_pcm_base64(metadata: AudioMetadata, base64_chunks: list[str]) -> bytes:
"""Decode the console payload and verify its declared PCM layout."""
if metadata.encoding != 'base64':
raise ValueError(f'Unsupported payload encoding: {metadata.encoding}')
if metadata.sample_rate <= 0 or metadata.channels not in (1, 2) or metadata.bits_per_sample != 16:
raise ValueError(f'Unsupported PCM format: {metadata}')
pcm_data = base64.b64decode(''.join(base64_chunks), validate=True)
if len(pcm_data) != metadata.data_size:
raise ValueError(f'Expected {metadata.data_size} PCM bytes, got {len(pcm_data)}')
if len(pcm_data) % metadata.frame_size:
raise ValueError(f'PCM data size is not aligned to {metadata.frame_size}-byte frames')
return pcm_data
def _save_wav_artifact(metadata: AudioMetadata, pcm_data: bytes, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
with wave.open(str(output_path), 'wb') as wav_file:
wav_file.setnchannels(metadata.channels)
wav_file.setsampwidth(metadata.bytes_per_sample)
wav_file.setframerate(metadata.sample_rate)
wav_file.writeframes(pcm_data)
except (OSError, wave.Error):
logging.exception('Failed to save microphone recording artifact to %s', output_path)
raise
logging.info('Saved microphone recording artifact to %s', output_path)
def _log_pcm_stats(pcm_data: bytes, metadata: AudioMetadata) -> None:
"""Report the measured peak amplitude. Informational only: a peak check would be
flaky on a board with no audio playing, where the recorded signal is near silence."""
interleaved_sample_count = len(pcm_data) // metadata.bytes_per_sample
peak = max((abs(v) for v in struct.unpack(f'<{interleaved_sample_count}h', pcm_data)), default=0)
duration = len(pcm_data) / metadata.frame_size / metadata.sample_rate
logging.info('PCM statistics: peak amplitude %d/32767, duration %.2f s', peak, duration)
def _collect_recording(dut: Dut) -> AudioMetadata:
"""Collect one recording, save its WAV artifact, and verify the console protocol."""
metadata_line = dut.expect(AUDIO_META_PATTERN, timeout=10).group(0).decode('utf-8')
metadata = _parse_audio_metadata(metadata_line)
assert metadata.sample_rate == EXPECTED_SAMPLE_RATE_HZ
assert metadata.bits_per_sample == EXPECTED_BITS_PER_SAMPLE
assert metadata.channels == EXPECTED_CHANNELS
dut.expect_exact('AUDIO_BASE64_BEGIN', timeout=5)
pcm_data = _decode_pcm_base64(metadata, _collect_base64_payload(dut))
_log_pcm_stats(pcm_data, metadata)
_save_wav_artifact(metadata, pcm_data, Path(dut.logdir) / MIC_RECORDING_OUTPUT_NAME)
dut.expect_exact(DONE_MARKER, timeout=5)
return metadata
@pytest.mark.generic
@pytest.mark.parametrize('config', ['dmic'], indirect=True)
@idf_parametrize(
'target',
PDM2PCM_TARGETS,
indirect=['target'],
)
def test_mic_recorder_dmic_example(dut: Dut) -> None:
dut.expect_exact(DMIC_START_MARKER, timeout=10)
_collect_recording(dut)
@pytest.mark.generic
@pytest.mark.parametrize('config', ['amic_es8389'], indirect=True)
@pytest.mark.skipif(os.getenv('CI_JOB_ID'), reason='no CI runner with an ES8389 codec')
@idf_parametrize(
'target',
I2S_TARGETS,
indirect=['target'],
)
def test_mic_recorder_amic_example(dut: Dut) -> None:
dut.expect_exact(AMIC_START_MARKER, timeout=10)
_collect_recording(dut)

View File

@@ -0,0 +1 @@
CONFIG_EXAMPLE_MIC_TYPE_AMIC=y

View File

@@ -0,0 +1 @@
CONFIG_EXAMPLE_MIC_TYPE_DMIC=y