Как установить частоту дискретизации звука с помощью режима АЦП/ЦАП драйвера ESP32 I2S?

Я объединил некоторые примеры 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));
}

Дайте мне знать, в чем, по вашему мнению, проблема и/или есть ли у вас хорошие рекомендации для изучения материала по этому предмету.

Я никогда не использовал ESP32, но когда делал подобные вещи на STM32, стало очевидно, что I2S всегда имеет два канала (стерео). Возможно, вам придется выбросить половину ваших данных (т. е. один из двух каналов) при сохранении файла.

pmacfarlane 15.12.2022 23:48

Как только что заметил @pmacfarlane, вы пытаетесь получить стерео с монофонического входа. Попробуйте изменить настройку I2S с I2S_CHANNEL_FMT_RIGHT_LEFT на I2S_CHANNEL_FMT_ONLY_LEFT (или ... ПРАВИЛЬНО)... Просто догадка

Fe2O3 15.12.2022 23:51

Спасибо за предложение! Это действительно решило проблему, поэтому не стесняйтесь редактировать ответ, чтобы я мог принять ответ. Что для меня странно, так это то, что драйвер i2s имеет структуру i2s_channel_t с параметром I2S_CHANNEL_MONO и другую структуру с именем i2s_channel_fmt_t с параметрами, которые вы предложили. Я не мог найти разницу между двумя

curioso 16.12.2022 09:22

Вы забыли отметить @Fe2O3, так что, возможно, они этого не видели. Их комментарий - лучший ответ.

pmacfarlane 16.12.2022 13:52

@pmacfarlane Спасибо за наводку и лесть... В наши дни трудно пытаться "опередить"... Спасибо за ваш последний комментарий здесь. :-)

Fe2O3 17.12.2022 00:00

Возможно, это связано с какой-то ошибкой в ​​esp-idf github.com/espressif/esp-idf/issues/8874

LudvigH 14.01.2023 16:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
252
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Недавнее знакомство с 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).

Удачной выборки!

Другие вопросы по теме