diff --git a/docs/en/api-reference/peripherals/i2s.rst b/docs/en/api-reference/peripherals/i2s.rst index 423dbf3260e..f4345cf301d 100644 --- a/docs/en/api-reference/peripherals/i2s.rst +++ b/docs/en/api-reference/peripherals/i2s.rst @@ -445,6 +445,7 @@ Standard TX/RX Usage - :example:`peripherals/i2s/i2s_codec/i2s_es8311` demonstrates how to use the I2S ES8311 audio codec with {IDF_TARGET_NAME} to play music or echo sounds, featuring high performance and low power multi-bit delta-sigma audio ADC and DAC, with options to customize music and adjust mic gain and volume. - :example:`peripherals/i2s/i2s_basic/i2s_std` demonstrates how to use the I2S standard mode in either simplex or full-duplex mode on {IDF_TARGET_NAME}. +- :example:`peripherals/i2s/mic_recorder` demonstrates how to record audio from an analog microphone connected to an ES8389 codec through the I2S STD interface. Different slot communication formats can be generated by the following helper macros for standard mode. As described above, there are three formats in standard mode, and their helper macros are: @@ -804,7 +805,7 @@ Here is the table of the data received in the buffer with different :cpp:member: PDM RX Usage ^^^^^^^^^^^^ - - :example:`peripherals/i2s/i2s_recorder` demonstrates how to record audio from a digital MEMS microphone using the I2S peripheral in PDM data format and save it to an SD card in ``.wav`` file format on {IDF_TARGET_NAME} development boards. + - :example:`peripherals/i2s/mic_recorder` demonstrates how to record audio from either PDM digital MEMS microphones (using the I2S PDM RX mode) or an analog microphone through an ES8389 codec, and streams the recorded PCM data over the console so that the ``.wav`` file can be reconstructed on the host PC. - :example:`peripherals/i2s/i2s_basic/i2s_pdm` demonstrates how to use the PDM RX mode on {IDF_TARGET_NAME}, including the necessary hardware setup and configuration. For PDM mode in RX channel, the slot configuration helper macro are: diff --git a/docs/zh_CN/api-reference/peripherals/i2s.rst b/docs/zh_CN/api-reference/peripherals/i2s.rst index 67530d56930..8b3b7ffeac2 100644 --- a/docs/zh_CN/api-reference/peripherals/i2s.rst +++ b/docs/zh_CN/api-reference/peripherals/i2s.rst @@ -445,6 +445,7 @@ I2S 驱动例程请参考 :example:`peripherals/i2s` 目录。以下为每种模 - :example:`peripherals/i2s/i2s_codec/i2s_es8311` 演示了如何在 {IDF_TARGET_NAME} 上使用 I2S ES8311 音频编解码器来播放音乐或回声,具有高性能和低功耗的多位 delta-sigma 音频 ADC 和 DAC,提供自定义音乐、调整麦克风增益和音量的选项。 - :example:`peripherals/i2s/i2s_basic/i2s_std` 演示了如何在 {IDF_TARGET_NAME} 上以单工或全双工模式使用 I2S 标准模式。 +- :example:`peripherals/i2s/mic_recorder` 演示了如何通过 I2S STD 接口,使用 ES8389 音频编解码器录制模拟麦克风的音频。 不同声道的通信格式可通过以下标准模式的辅助宏来生成。如上所述,在标准模式下有三种格式,辅助宏分别为: @@ -804,7 +805,7 @@ STD RX 模式 PDM RX 模式的应用 ^^^^^^^^^^^^^^^^^^ - - :example:`peripherals/i2s/i2s_recorder` 演示了如何通过 I2S 外设以 PDM 数据格式用数字 MEMS 麦克风录制音频,并将其以 ``.wav`` 文件格式保存到 {IDF_TARGET_NAME} 开发板上的 SD 卡中。 + - :example:`peripherals/i2s/mic_recorder` 演示了如何录制音频:既可以使用 PDM 数字 MEMS 麦克风(通过 I2S PDM RX 模式),也可以使用连接 ES8389 编解码器的模拟麦克风,并将采集到的 PCM 数据通过控制台串流输出,以便在主机 PC 上重建 ``.wav`` 文件。 - :example:`peripherals/i2s/i2s_basic/i2s_pdm` 演示了如何在 {IDF_TARGET_NAME} 上使用 PDM RX 模式,包括必要的硬件设置和配置。 针对 RX 通道的 PDM 模式,声道配置的辅助宏为: diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 1c586c47717..b2e43a2c063 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -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 diff --git a/examples/peripherals/i2s/i2s_advance/i2s_usb/CMakeLists.txt b/examples/peripherals/i2s/i2s_advance/i2s_usb/CMakeLists.txt index 4225e2f044a..168b2243674 100644 --- a/examples/peripherals/i2s/i2s_advance/i2s_usb/CMakeLists.txt +++ b/examples/peripherals/i2s/i2s_advance/i2s_usb/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/i2s/i2s_advance/i2s_usb/main/idf_component.yml b/examples/peripherals/i2s/i2s_advance/i2s_usb/main/idf_component.yml index 810e60b5cd6..795b3053e7f 100644 --- a/examples/peripherals/i2s/i2s_advance/i2s_usb/main/idf_component.yml +++ b/examples/peripherals/i2s/i2s_advance/i2s_usb/main/idf_component.yml @@ -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 diff --git a/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/CMakeLists.txt b/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/CMakeLists.txt index a9a06a99885..ec14014c06f 100644 --- a/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/CMakeLists.txt +++ b/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/main/idf_component.yml b/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/main/idf_component.yml index af877bcec30..e56221746b8 100644 --- a/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/main/idf_component.yml +++ b/examples/peripherals/i2s/i2s_codec/i2s_es7210_tdm/main/idf_component.yml @@ -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 diff --git a/examples/peripherals/i2s/i2s_codec/i2s_es8311/CMakeLists.txt b/examples/peripherals/i2s/i2s_codec/i2s_es8311/CMakeLists.txt index 08e8ee05369..bee278af491 100644 --- a/examples/peripherals/i2s/i2s_codec/i2s_es8311/CMakeLists.txt +++ b/examples/peripherals/i2s/i2s_codec/i2s_es8311/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/i2s/i2s_codec/i2s_es8311/main/idf_component.yml b/examples/peripherals/i2s/i2s_codec/i2s_es8311/main/idf_component.yml index 4f217693059..3dfafda15bd 100644 --- a/examples/peripherals/i2s/i2s_codec/i2s_es8311/main/idf_component.yml +++ b/examples/peripherals/i2s/i2s_codec/i2s_es8311/main/idf_component.yml @@ -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 diff --git a/examples/peripherals/i2s/i2s_recorder/README.md b/examples/peripherals/i2s/i2s_recorder/README.md deleted file mode 100644 index 0b5c6e4ae40..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/README.md +++ /dev/null @@ -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. diff --git a/examples/peripherals/i2s/i2s_recorder/main/CMakeLists.txt b/examples/peripherals/i2s/i2s_recorder/main/CMakeLists.txt deleted file mode 100644 index 0776d102b50..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/main/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "i2s_recorder_main.c" - PRIV_REQUIRES esp_driver_i2s fatfs i2s_examples_common - INCLUDE_DIRS) diff --git a/examples/peripherals/i2s/i2s_recorder/main/Kconfig.projbuild b/examples/peripherals/i2s/i2s_recorder/main/Kconfig.projbuild deleted file mode 100644 index c5c70f09486..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/main/Kconfig.projbuild +++ /dev/null @@ -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 diff --git a/examples/peripherals/i2s/i2s_recorder/main/i2s_recorder_main.c b/examples/peripherals/i2s/i2s_recorder/main/i2s_recorder_main.c deleted file mode 100644 index 04d9fb8f50e..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/main/i2s_recorder_main.c +++ /dev/null @@ -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 -#include -#include -#include -#include -#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)); -} diff --git a/examples/peripherals/i2s/i2s_recorder/main/idf_component.yml b/examples/peripherals/i2s/i2s_recorder/main/idf_component.yml deleted file mode 100644 index 804fe8e427e..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/main/idf_component.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - i2s_examples_common: - path: ${IDF_PATH}/examples/peripherals/i2s/i2s_examples_common diff --git a/examples/peripherals/i2s/i2s_recorder/pytest_i2s_record.py b/examples/peripherals/i2s/i2s_recorder/pytest_i2s_record.py deleted file mode 100644 index 874a9427c25..00000000000 --- a/examples/peripherals/i2s/i2s_recorder/pytest_i2s_record.py +++ /dev/null @@ -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') diff --git a/examples/peripherals/i2s/i2s_recorder/CMakeLists.txt b/examples/peripherals/i2s/mic_recorder/CMakeLists.txt similarity index 91% rename from examples/peripherals/i2s/i2s_recorder/CMakeLists.txt rename to examples/peripherals/i2s/mic_recorder/CMakeLists.txt index e1cb971b7eb..520631053bd 100644 --- a/examples/peripherals/i2s/i2s_recorder/CMakeLists.txt +++ b/examples/peripherals/i2s/mic_recorder/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/i2s/mic_recorder/README.md b/examples/peripherals/i2s/mic_recorder/README.md new file mode 100644 index 00000000000..de261b1d4d6 --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/README.md @@ -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 +... +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: + +``` +/mic_recording.wav +``` + +(`` 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. diff --git a/examples/peripherals/i2s/mic_recorder/main/CMakeLists.txt b/examples/peripherals/i2s/mic_recorder/main/CMakeLists.txt new file mode 100644 index 00000000000..cdcc476106c --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/main/CMakeLists.txt @@ -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 .) diff --git a/examples/peripherals/i2s/mic_recorder/main/Kconfig.projbuild b/examples/peripherals/i2s/mic_recorder/main/Kconfig.projbuild new file mode 100644 index 00000000000..ea64a9192c5 --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/main/Kconfig.projbuild @@ -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 diff --git a/examples/peripherals/i2s/mic_recorder/main/idf_component.yml b/examples/peripherals/i2s/mic_recorder/main/idf_component.yml new file mode 100644 index 00000000000..d135f239a51 --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/main/idf_component.yml @@ -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" diff --git a/examples/peripherals/i2s/mic_recorder/main/mic_recorder_example_main.c b/examples/peripherals/i2s/mic_recorder/main/mic_recorder_example_main.c new file mode 100644 index 00000000000..8cf02b759dd --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/main/mic_recorder_example_main.c @@ -0,0 +1,228 @@ +/* + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#include +#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); +} diff --git a/examples/peripherals/i2s/mic_recorder/pytest_mic_record.py b/examples/peripherals/i2s/mic_recorder/pytest_mic_record.py new file mode 100644 index 00000000000..76e4ce5cbfd --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/pytest_mic_record.py @@ -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\d+) bits_per_sample=(?P\d+) ' + r'channels=(?P\d+) data_size=(?P\d+) encoding=(?P\w+)' +) +AUDIO_META_RE = re.compile(AUDIO_META_PATTERN) +AUDIO_CHUNK_PATTERN = r'AUDIO_BASE64 (?P[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'(?PAUDIO_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) diff --git a/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.amic_es8389 b/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.amic_es8389 new file mode 100644 index 00000000000..48ec54697e2 --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.amic_es8389 @@ -0,0 +1 @@ +CONFIG_EXAMPLE_MIC_TYPE_AMIC=y diff --git a/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.dmic b/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.dmic new file mode 100644 index 00000000000..e9d8c776e96 --- /dev/null +++ b/examples/peripherals/i2s/mic_recorder/sdkconfig.ci.dmic @@ -0,0 +1 @@ +CONFIG_EXAMPLE_MIC_TYPE_DMIC=y