mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(i2s): extend the mic recorder example to support both DMIC and AMIC
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,3 +0,0 @@
|
||||
idf_component_register(SRCS "i2s_recorder_main.c"
|
||||
PRIV_REQUIRES esp_driver_i2s fatfs i2s_examples_common
|
||||
INCLUDE_DIRS)
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
dependencies:
|
||||
i2s_examples_common:
|
||||
path: ${IDF_PATH}/examples/peripherals/i2s/i2s_examples_common
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
126
examples/peripherals/i2s/mic_recorder/README.md
Normal file
126
examples/peripherals/i2s/mic_recorder/README.md
Normal 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.
|
||||
@@ -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 .)
|
||||
94
examples/peripherals/i2s/mic_recorder/main/Kconfig.projbuild
Normal file
94
examples/peripherals/i2s/mic_recorder/main/Kconfig.projbuild
Normal 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
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
163
examples/peripherals/i2s/mic_recorder/pytest_mic_record.py
Normal file
163
examples/peripherals/i2s/mic_recorder/pytest_mic_record.py
Normal 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)
|
||||
@@ -0,0 +1 @@
|
||||
CONFIG_EXAMPLE_MIC_TYPE_AMIC=y
|
||||
1
examples/peripherals/i2s/mic_recorder/sdkconfig.ci.dmic
Normal file
1
examples/peripherals/i2s/mic_recorder/sdkconfig.ci.dmic
Normal file
@@ -0,0 +1 @@
|
||||
CONFIG_EXAMPLE_MIC_TYPE_DMIC=y
|
||||
Reference in New Issue
Block a user