Я объединил некоторые примеры i2s-adc и sd-card из репозитория esp-idf на GitHub, чтобы создать программу, которая сэмплирует аналоговый микрофон и сохраняет запись в виде файла WAV на SD-карте. проблема в том, что мой записанный звук всегда воспроизводится на половине частоты, которая была установлена для сэмплирования.
Как показано в приведенном ниже коде, для I2S_SAMPLE_RATE
установлено значение 44 100 Гц, но если я играю, это звучит так, как если бы оно было семплировано на частоте около 88 200 Гц. Я могу думать о двух причинах этого:
- Драйвер I2s работает с более высокой частотой дискретизации.
Я уже пытался жестко кодировать частоту с помощью i2s_set_clk
, но безуспешно.
- Запись была сделана с небольшой глубиной, поэтому есть некоторые искажения.
Я знаю, что esp32 имеет 12-битный АЦП, и я устанавливаю BIT_SAMPLE
на 16, но достаточно ли этого, чтобы вызвать тип искажения, который я описал?
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <sys/unistd.h>
#include <sys/stat.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.h"
#include "driver/gpio.h"
#include "driver/spi_common.h"
#include "sdmmc_cmd.h"
#include "sdkconfig.h"
#include "esp_adc_cal.h"
#include "driver/adc.h"
#define V_REF 1100
#define I2S_COMM_MODE 0 // ADC/DAC Mode
#define I2S_SAMPLE_RATE 44100
#define I2S_SAMPLE_BITS 16
#define I2S_BUF_DEBUG 0 // enable display buffer for debug
#define I2S_READ_LEN 16 * 1024 // I2S read buffer length
#define I2S_FORMAT (I2S_CHANNEL_FMT_RIGHT_LEFT)
#define I2S_CHANNEL_NUM 0 // I2S channel number
#define I2S_ADC_UNIT ADC_UNIT_1 // I2S built-in ADC unit
#define I2S_ADC_CHANNEL ADC1_CHANNEL_0 // I2S built-in ADC channel GPIO36
#define SPI_MOSI_GPIO 23
#define SPI_MISO_GPIO 19
#define SPI_CLK_GPIO 18
#define SPI_CS_GPIO 5
static const char *TAG = "I2S_ADC_REC";
#define BIT_SAMPLE 16
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
#define NUM_CHANNELS 1 // For mono recording only!
#define SD_MOUNT_POINT "/sdcard"
#define SAMPLE_SIZE (BIT_SAMPLE * 1024)
#define BYTE_RATE (I2S_SAMPLE_RATE * (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;
static int16_t i2s_readraw_buff[SAMPLE_SIZE];
size_t bytes_read;
const int WAVE_HEADER_SIZE = 44;
/**
* @brief Initializes the slot without card detect (CD) and write protect (WP) signals.
* It formats the card if mount fails and initializes the card. After the card has been
* initialized, it print the card properties
*/
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 = SPI_MOSI_GPIO,
.miso_io_num = SPI_MISO_GPIO,
.sclk_io_num = SPI_CLK_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 = 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;
}
sdmmc_card_print_info(stdout, card); // Card has been initialized, print its properties
}
/**
* @brief Generates the header for the WAV file that is going to be stored in the SD card.
* See this for reference: http://soundfile.sapp.org/doc/WaveFormat/.
*/
void generate_wav_header(char *wav_header, uint32_t wav_size, uint32_t sample_rate)
{
uint32_t file_size = wav_size + WAVE_HEADER_SIZE - 8;
uint32_t byte_rate = BYTE_RATE;
const char set_wav_header[] = {
'R', 'I', 'F', 'F', // ChunkID
file_size, file_size >> 8, file_size >> 16, file_size >> 24, // ChunkSize
'W', 'A', 'V', 'E', // Format
'f', 'm', 't', ' ', // Subchunk1ID
0x10, 0x00, 0x00, 0x00, // Subchunk1Size (16 for PCM)
0x01, 0x00, // AudioFormat (1 for PCM)
0x01, 0x00, // NumChannels (1 channel)
sample_rate, sample_rate >> 8, sample_rate >> 16, sample_rate >> 24, // SampleRate
byte_rate, byte_rate >> 8, byte_rate >> 16, byte_rate >> 24, // ByteRate
0x02, 0x00, // BlockAlign
0x10, 0x00, // BitsPerSample (16 bits)
'd', 'a', 't', 'a', // Subchunk2ID
wav_size, wav_size >> 8, wav_size >> 16, wav_size >> 24, // Subchunk2Size
};
memcpy(wav_header, set_wav_header, sizeof(set_wav_header));
}
/**
* @brief I2S ADC mode init.
*/
void init_microphone(void)
{
int i2s_num = I2S_COMM_MODE;
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN | I2S_MODE_ADC_BUILT_IN,
.sample_rate = I2S_SAMPLE_RATE,
.bits_per_sample = I2S_SAMPLE_BITS,
.communication_format = I2S_COMM_FORMAT_STAND_MSB,
.channel_format = I2S_FORMAT,
.intr_alloc_flags = 0,
.dma_buf_count = 6,
.dma_buf_len = 256,
.use_apll = 1,
};
// Call driver installation function and adc pad.
ESP_ERROR_CHECK(i2s_driver_install(i2s_num, &i2s_config, 0, NULL));
ESP_ERROR_CHECK(i2s_set_adc_mode(I2S_ADC_UNIT, I2S_ADC_CHANNEL));
}
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");
char wav_header_fmt[WAVE_HEADER_SIZE];
uint32_t flash_rec_size = BYTE_RATE * rec_time;
generate_wav_header(wav_header_fmt, flash_rec_size, I2S_SAMPLE_RATE);
// 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_fmt, 1, WAVE_HEADER_SIZE, f);
i2s_adc_enable(I2S_CHANNEL_NUM);
// Start recording
while (flash_wr_size < flash_rec_size)
{
// TODO: The part below needs to be replaced with analog microphone reading //
// Read the RAW samples from the microphone
// Read data from I2S bus, in this case, from ADC. //
i2s_read(I2S_CHANNEL_NUM, (char *)i2s_readraw_buff, SAMPLE_SIZE, &bytes_read, 100);
// TODO: The part above needs to be replaced with analog microphone reading //
// Write the samples to the WAV file
fwrite(i2s_readraw_buff, 1, bytes_read, f);
flash_wr_size += bytes_read;
}
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 app_main(void)
{
int rec_time = 5;
ESP_LOGI(TAG, "Analog microphone recording Example start");
// Mount the SDCard for recording the audio file
mount_sdcard();
// I2S ADC mode microphone init.
init_microphone();
ESP_LOGI(TAG, "Starting recording for %d seconds!", rec_time);
// Start Recording
record_wav(rec_time);
// Stop I2S driver and destroy
ESP_ERROR_CHECK(i2s_driver_uninstall(I2S_COMM_MODE));
}
Дайте мне знать, в чем, по вашему мнению, проблема и/или есть ли у вас хорошие рекомендации для изучения материала по этому предмету.
Как только что заметил @pmacfarlane, вы пытаетесь получить стерео с монофонического входа. Попробуйте изменить настройку I2S с I2S_CHANNEL_FMT_RIGHT_LEFT
на I2S_CHANNEL_FMT_ONLY_LEFT
(или ... ПРАВИЛЬНО)... Просто догадка
Спасибо за предложение! Это действительно решило проблему, поэтому не стесняйтесь редактировать ответ, чтобы я мог принять ответ. Что для меня странно, так это то, что драйвер i2s имеет структуру i2s_channel_t
с параметром I2S_CHANNEL_MONO
и другую структуру с именем i2s_channel_fmt_t
с параметрами, которые вы предложили. Я не мог найти разницу между двумя
Вы забыли отметить @Fe2O3, так что, возможно, они этого не видели. Их комментарий - лучший ответ.
@pmacfarlane Спасибо за наводку и лесть... В наши дни трудно пытаться "опередить"... Спасибо за ваш последний комментарий здесь. :-)
Возможно, это связано с какой-то ошибкой в esp-idf github.com/espressif/esp-idf/issues/8874
Недавнее знакомство с I2S дало мне представление о его возможностях.
Когда я сканировал ваш код и думал о вашем описании («Кажется, семплируется на частоте 88200 Гц»), токен I2S_CHANNEL_FMT_RIGHT_LEFT
привлек мое внимание.
Зная, что вы будете записывать монофонический сигнал (и не углубляясь в I2S), казалось разумным предложить настроить конфигурацию только на один канал; т.е. I2S_CHANNEL_FMT_ONLY_LEFT
(или ... ПРАВИЛЬНО). (Если бы вы ранее просматривали «шестнадцатеричный дамп» записи WAV, я предполагаю, что вы бы увидели то, на что указал @pmacfarlane: половина выборочных значений либо дублируются, либо равны 0, и их можно/нужно отбросить.)
Имеет смысл, что такая мощная система (I2S) также понимает, как вы обнаружили, I2S_CHANNEL_MONO
. Я рад, что вы нашли решение.
Что касается «кажется странным», то только тогда, когда ученик глубоко учится у своих мастеров, это «странное» перерастает в понимание и признательность. Те, кто ушел раньше, не были «сутулыми».
OT: когда вы экспериментируете с выборкой и записью на SD-карту (микро SD?), имейте в виду, что запись может иметь значительные «накладные расходы». uSeconds, затрачиваемые на запись 10 000 000 000 000, составляют более 1/10 от тех, которые требуются для записи 100 000 000. Я подозреваю, что «установка» до того, как происходит письмо, является существенной. Лучше писать большими блоками, а не маленькими фрагментами, из-за которых вы можете пропустить другие события (например, I2S, заполняющий свой буфер DMA).
Удачной выборки!
Я никогда не использовал ESP32, но когда делал подобные вещи на STM32, стало очевидно, что I2S всегда имеет два канала (стерео). Возможно, вам придется выбросить половину ваших данных (т. е. один из двух каналов) при сохранении файла.