Эксперимент по записи голоса с помощью модуля MAX9814 и ESP32
В этой статье мы изучим процесс записи голоса с помощью микроконтроллера ESP32 и микрофонного усилителя MAX9814.
Эксперимент по записи голоса с помощью модуля MAX9814 и ESP32
29.02.2024 в 10:45   972 0
Версия для печати

Эксперимент по записи голоса с помощью модуля MAX9814 и ESP32

logo

Введение

В этой статье мы изучим процесс записи голоса с помощью микроконтроллера ESP32 и микрофонного усилителя MAX9814. ESP32 обладает целым рядом возможностей, включая двухъядерный процессор, Wi-Fi и Bluetooth, что делает его полезным инструментом для различных проектов, включая те, что связаны с записью звука.

MAX9814 - это микрофонный усилитель, который предоставляет качественный аналоговый аудиосигнал. Когда этот сигнал преобразуется в цифровой формат, он может быть обработан устройствами, такими как ESP32, для дальнейшего использования в различных приложениях.

Мы подробно разберем процесс установки оборудования, соединения модулей друг с другом, а также написания кода на Arduino IDE для управления звуковыми данными. 

Цель этого эксперимента - сборка рабочего прототипа устройства захвата и записи аналогового звука средствами ESP32, с перекодированием "на лету" в формат wav. Управление записью, а также скачивание полученного файла будет осуществляться через веб интерфейс. Итак, приступаем.

Краткий обзор микрофонного модуля MAX9814

max9814

Микрофонный модуль состоит из электронного микрофона (20-20кГЦ) и специального усилителя на чипе MAX9814.
MAX9814 - это высокопроизводительный, низкошумящий микрофонный усилитель, идеально подходящий для проектов, требующих качественной аудиозаписи. Его основные характеристики обеспечивают универсальность и производительность, делая его полезным элементом в многих аудио-приложениях.

Главные особенности Модуля MAX9814

  • Автоматический уровень усиления (AGC): MAX9814 имеет встроенную функцию автоматического контроля усиления, которая помогает поддерживать постоянный уровень звукового сигнала, даже при значительных изменениях входного сигнала.
  • Низкий уровень шума: Модуль обеспечивает низкошумящее усиление благодаря своему особому дизайну и применяемым технологиям.
  • Возможность выбора режима усиления: MOD MAX9814 имеет 3 дискретных уровня усиления - 40dB, 50dB и 60dB, позволяя пользователю выбрать наиболее подходящий в зависимости от потребностей.
  • Простота в использовании: Модуль можно легко подключить к большинству микроконтроллеров, включая ESP32 и Arduino, что делает его идеальным для DIY проектов.
  • Экономичен: MAX9814 работает от одного источника питания от 2,7 до 5,5 В.

Назначение контактов

  • AR — регулировка время срабатывание/время восстановления
  • Gain — регулировка «Максимальное усиление»
  • Out — выход звукового сигнала.
  • Vdd и GND — питание модуля

Необходимые компоненты и схема подключения

Для создания прототипа устройства нам понадобится необходимый минимум компонентов. Это:
  • Модуль ESP-WROOM-32
  • Микрофонный модуль MAX9814
  • 3 провода для соединений
  • Usb кабель для прошивки и питания устройства 
Соединять MAX9814 и ESP32 будем по следующей схеме:
 
Пин MAX9814 Пин ESP32
VDD 3V3
GND GND
OUT GPIO34
 

После подключения модуля можно приступать к написанию программы.

Написание программы

Для начала определимся как должна работать программа и какие дополнительные библиотеки нам понадобятся.

Логика программы и выбор дополнительных библиотек

Для начала определимся как должна работать программа и какие дополнительные библиотеки нам понадобятся. Для подключения к устройству мы будем использовать WiFi. Взаимодействовать мы будем через Web интерфейс, посредством http запросов. Получать аналоговый сигнал с микрофона и преобразовывать в его в цифровой вид для записи в файл мы будем с помощью встроенного ADC преобразователя ESP32 с последующим копированием полученных данных через интерфейс I2S в файл во флеш память ESP32. Исходя из этого подключаем библиотеки:

#include "driver/i2s.h" //Работа с итерфейсом i2s
#include <SPIFFS.h>//файловая система spiffs
#include <ESPAsyncWebServer.h>//веб сервер
#include <WiFi.h>//wifi функции

Подключение к wifi и веб интерфейс

Зададим необходимые параметры для подключения к wifi

const char* ssid = "SSID"; //точка доступа wifi
const char* password = "PASS"; //пароль

Добавим в функцию Setup() вызов функции подключения к wifi с последующим запуском веб сервера

 

void setup() {
Serial.begin(115200); //Запускаем серийный порт
 WiFi.begin(ssid, password); //Подключаемся к WiFi
 while (WiFi.status() != WL_CONNECTED) {
 delay(1000);
 Serial.println("Connecting to WiFi...");
 }
 Serial.println(WiFi.localIP());

// Запускаем сервер
 server.begin();
 Serial.println("Server started");
}

Если на данном этапе вы прошьете устройство и введете его IP адрес в браузере, то получите ошибку 404, так веб сервер еще не знает как обрабатывать данный запрос. Исправим это добавив его обработчик в функцию Setup().

//Формируем главную страницу веб интерфейса
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
 String html = "<html><body>";
 html += "<h1>ESP32 Audio Recorder</h1>";
 html += "<button onclick=\"location.href='/record_begin'\" type='button'>Start Recording</button>";
 html += "<button onclick=\"location.href='/list'\" type='button'>Files list</button>";
 html += "<button onclick=\"location.href='/audio.wav'\" type='button'>Download WAV</button>";
 html += "</body></html>";
 request->send(200, "text/html", html);
});

Теперь при обращении серверу по IP адресу в браузере, он динамически создает веб страницу с 3 кнопками.

webmain

Пока нажатия на эти кнопки тоже приводят к ошибке 404, так как их обработку мы еще не написали, но этим займемся чуточку позже.

Работа с файловой системой

Сохранять полученный файл звукозаписи мы будем во флеш-память ESP32, с файловой системой SPIFFS. Возможно, это далеко не лучший выбор, из-за достаточно ограниченного свободного места ии некоторых других ограничений её использования. Гораздо правильнее будет подключить внешний модуль SD карты по SPI и работать уже с ним, но в рамках эксперимента сгодится и внутренняя флеш-память ESP32.

Добавим несколько строк для инициализации SPIFFS.

File file;
SPIFFS.begin(true); //Формат файловой системы в случае ошибки инициализации

Также допишем обработчики событий для кнопок веб интерфейса связанных с файловой системой:

//Выводим содержимое файловой системы в веб интерфейс
server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request){
 String html = "<html><body>";
 html += "<h1>SPIFFS Files:</h1>";
 html += "<ul>";
 File root = SPIFFS.open("/");
 File file = root.openNextFile();
 while(file){
 html += "<li>";
 html += file.name();
 html += " (";
 html += file.size();
 html += " bytes)";
 html += "</li>";
 file = root.openNextFile();
 }
 html += "</ul>";
 html += "</body></html>";
 request->send(200, "text/html", html);
});

//Формируем запрос серверу для скачивания созданного файла
server.on("/audio.wav", HTTP_GET, [](AsyncWebServerRequest *request){
 if(!SPIFFS.exists("/audio.wav")){
 return request->send(404, "text/plain", "File not found");
 }
 File file = SPIFFS.open("/audio.wav","r");
 if (file) {
 Serial.print("File size: ");
 Serial.println(file.size());
 request->send(file, "/audio.wav", "audio/wav", true);
 }else {
 request->send(500, "text/plain", "File does not exist");
 }

});

Теперь при нажатии на кнопку Files list в веб интерфейсе откроется новое окно со списком файлов хранящихся в файловой системе ESP32. А при нажатии Download WAV будет произведена попытка скачивания файла audio.wav если он пристутствует в файловой системе, иначе будет выведено сообщение об ошибке.

Запись звука с микрофона и кодирование в WAV

Для начала давайте разберемся, что вообще из себя представляет файл в формате wav.
WAV - это формат файла, используемый для хранения аудиоданных в несжатом виде. Структура файла  включает в себя "header" (заголовок) и "data" (данные). Заголовок содержит информацию о содержащихся в файле аудиоданных, такую как формат кодирования аудио (PCM, ADPCM, MP3 и т.д.), число каналов (моно, стерео, 5.1 и т.д.), частоту дискретизации (44.1кГц, 48кГц и т.д.) и битовую глубину (16 бит, 24 бита и т.д.). Секция данных содержит сам аудиопоток, кодированный в выбранном формате.
Таким образом, нам необходимо создать файл, сгенерировать для него блок header и записать в начало. Далее получить аналоговый сигнал с микрофона, преобразовать его в набор цифровых данных и записать его после блока header. После чего сохранить полученный файл.

Генерация блока header

Основная структура заголовка WAV-файла представлена в следующем виде:
  • ChunkID (4 байта): "RIFF" в ASCII.
  • ChunkSize (4 байта): размер оставшейся части файла после этого поля, включая данные.
  • Format (4 байта): "WAVE" в ASCII.
  • Subchunk1ID (4 байта): "fmt " в ASCII.
  • Subchunk1Size (4 байта): размер оставшейся части под-блока, начиная от следующего поля. Для формата PCM это 16.
  • AudioFormat (2 байта): формат аудио данных, PCM = 1 (линейное квантование). Значения, отличные от 1, указывают на какую-то форму сжатия.
  • NumChannels (2 байта): число каналов, моно = 1, стерео = 2 и т. д.
  • SampleRate (4 байта): частота дискретизации, 44100 Гц, 48000 Гц и т. д.
  • ByteRate (4 байта): для несжатых аудиоданных - это число байт, обрабатываемых за одну секунду воспроизведения.
  • BlockAlign (2 байта): число байт на выборку, включая все каналы.
  • BitsPerSample (2 байты): количество бит в выборке. Так называемая глубина звука, 8 бит, 16 бит и т. д.
  • Subchunk2ID (4 байта): "data" в ASCII.
  • Subchunk2Size (4 байта): Количество байт в области данных. По сути, количество полезных «пользовательских» данных.

Все числовые значения записаны в виде little-endian (младшим байтом вперёд), что является стандартным форматом для файлов WAV.

Исходя из этого, напишем функцию - генератор заголовка wav файла.

//Объявляем несколько констант для подсчета длины блоков data и header
const int record_time = 5; // время записи в секундах
const int headerSize = 44;
const int waveDataSize = record_time * 88000;

byte header[headerSize]; //Объявляем байтовый массив для хранения заголовка

//Создаем заголовок wav файла
void CreateWavHeader(byte* header, int waveDataSize){
 // ChunkID
 header[0] = 'R';
 header[1] = 'I';
 header[2] = 'F';
 header[3] = 'F';
 // ChunkSize
 unsigned int fileSizeMinus8 = waveDataSize + 44 - 8;
 header[4] = (byte)(fileSizeMinus8 & 0xFF);
 header[5] = (byte)((fileSizeMinus8 >> 8) & 0xFF);
 header[6] = (byte)((fileSizeMinus8 >> 16) & 0xFF);
 header[7] = (byte)((fileSizeMinus8 >> 24) & 0xFF);
 // Format
 header[8] = 'W';
 header[9] = 'A';
 header[10] = 'V';
 header[11] = 'E';
 //Subchunk1ID
 header[12] = 'f'; 
 header[13] = 'm';
 header[14] = 't';
 header[15] = ' ';
 // Subchunk1Size 16 байт для формата PCM
 header[16] = 0x10; 
 header[17] = 0x00;
 header[18] = 0x00;
 header[19] = 0x00;
 // AudioFormat 
 header[20] = 0x01; //linear PCM
 header[21] = 0x00;
 //NumChannels
 header[22] = 0x01; // mono
 header[23] = 0x00;
 //SampleRate
 header[24] = 0x44; // sampling rate 44100 (hex=ac44)
 header[25] = 0xAC;
 header[26] = 0x00;
 header[27] = 0x00;
 //ByteRate
 header[28] = 0x88; // Byte/sec = 44100x2x1 = 88200
 header[29] = 0x58;
 header[30] = 0x01;
 header[31] = 0x00;
 //BlockAlign
 header[32] = 0x02; // 16bit mono
 header[33] = 0x00;
 //BitsPerSample
 header[34] = 0x10; // 16bit
 header[35] = 0x00;
 // Subchunk2ID 
 header[36] = 'd';
 header[37] = 'a';
 header[38] = 't';
 header[39] = 'a';
 //Subchunk2Size
 header[40] = (byte)(waveDataSize & 0xFF);
 header[41] = (byte)((waveDataSize >> 8) & 0xFF);
 header[42] = (byte)((waveDataSize >> 16) & 0xFF);
 header[43] = (byte)((waveDataSize >> 24) & 0xFF);
}

Получение аудиопотока

После генерации блока header, настало время заняться содержимым блока data. Данные в нем будут хранится в формате PCM. А для преобразования аналогового сигнала с микрофона в нужный нам вид воспользуемся ADC конвертером и I2S интерфейсом ESP32.

Сначала напишем функцию инициализации I2S.

void I2S_Init(i2s_mode_t MODE, i2s_bits_per_sample_t BPS)
{
 i2s_config_t i2s_config = {
 .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN | I2S_MODE_ADC_BUILT_IN),
 .sample_rate = (44100),
 .bits_per_sample = BPS,
 .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
 .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
 .intr_alloc_flags = 0,
 .dma_buf_count = 16,
 .dma_buf_len = 60
 };
 Serial.println("using ADC_builtin");
 i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
 i2s_set_adc_mode(ADC_UNIT_1, ADC1_CHANNEL_6); 
 
}

В ней производится настройка основных параметров протокола. Частота дискретизации, количество звуковых каналов, источник сигнала и прочее. Более подробно об этом можно узнать в документации к библиотеке.

Обратите внимание на этот параметр:

 i2s_set_adc_mode(ADC_UNIT_1, ADC1_CHANNEL_6); 

Здесь указывается пин на который приходит аналоговый аудиосигнал. Если он отличается от GPIO34, то измените значение ADC1_CHANNEL_6, руководствуясь следующей схемой.

esp_pinout

Далее напишем функцию блочного чтения байт данных получаемых по i2S.

int I2S_Read(char *data, int numData)
{
 return i2s_read_bytes(I2S_NUM_0, (char *)data, numData, portMAX_DELAY);
}

На вход функция получает два параметра. Указатель на область памяти куда писать данные и размер блока. Затем читает данные с помощью i2s_read_bytes() и записывает по указанному адресу. Обратите внимание, что функция i2s_read_bytes() в ядре ESP32 для Arduino IDE версии 2.0 и выше была удалена, так что используем ядро 1.0.6

Далее остается записать полученный блок данных в файл и заполнить его новыми значениями.

Самое время дописать обработчик кнопки веб интерфейса для записи сигнала с микрофона.

// Отправляем команду серверу на начало записи
 server.on("/record_begin", HTTP_GET, [](AsyncWebServerRequest *request){
 file = SPIFFS.open("/audio.wav", FILE_WRITE); //Создаем файл для записи
 if(file){
 CreateWavHeader(header, waveDataSize); //Создаем заголовок для wav
 Serial.println("Recording started");
 file.write(header, headerSize);//Записываем его в файл
 
 I2S_Init(I2S_MODE_ADC_BUILT_IN, I2S_BITS_PER_SAMPLE_32BIT); //Инициализируем интерфейс I2S
 //С помощью встроенного ADC ESP32 получаем звуковой сигнал с микрофона, преобразовываем его в цифру и записываем в файл 
 for (int j = 0; j < waveDataSize/numPartWavData; ++j) {
 I2S_Read(communicationData, numCommunicationData);
 for (int i = 0; i < numCommunicationData/8; ++i) {
 partWavData[2*i] = communicationData[8*i + 2];
 partWavData[2*i + 1] = communicationData[8*i + 3];
 }
 file.write((const byte*)partWavData, numPartWavData);
 }
 file.close(); //Закрываем файл для записи
 Serial.println("finish");
 request->send(200, "text/plain", "Recording finished");
 } else { //Обрабатываем ошибку записи файла
 Serial.println("Failed to start recording");
 request->send(500, "text/plain", "Failed to start recording");
 }
 });

После нажатия на кнопку записи, в файловой системе ESP32 создается новый файл audio.wav. Далее генерируется header и записывается в wav. Далее инициализируется интерфейс i2s и в цикле блоками считываются данные и сразу же записываются в файл до тех пор пока выделенное место под них не закончится. После чего файл закрывается и сохраняется.

 

Длительность записи задается константой const int record_time = 5; и по умолчанию равна 5 секундам. Больше ставить на выбранной мной конфигурации оборудования особо не имеет смысла. Файл длительностью 2 секунды занимает около 250 кб, что по меркам микроконтроллеров весьма существенно.

Тестирование

Для теста попробовал записать один семпл звука. Качество записи в целом хорошее, хотя слышны некоторые шумы и потрескивания в фоне. Скачать полученный семпл можно по ссылке.

Но всплывает одна серьезная проблема. Так как запись аудио происходит в callback вызове ESPAsyncWebServer и занимает довольно таки продолжительное время, watchdog таймер ESP32 считает, что процесс завис и руководствуясь своими алгоритмами перезагружает ESP32 спустя пару секунд после включения записи. Благо файл при этом сохраняется. Но длительность записи ограничивается всего лишь 2-3 секундами. 

Для решения этой проблемы попробовал вынести запись аудио в отдельный процесс на Core0, слегка видоизменив код:


TaskHandle_t Task1;
bool recording=false;

void Task1code( void * pvParameters ){
 Serial.print("Task1 running on core ");
 Serial.println(xPortGetCoreID());
 
 for(;;){
 if (recording)
 {
 SPIFFS.remove("/audio.wav");
 file = SPIFFS.open("/audio.wav", FILE_WRITE); //Создаем файл для записи
 if(file){
 CreateWavHeader(header, waveDataSize); //Создаем заголовок для wav
 Serial.println("Recording started");
 file.write(header, headerSize);//Записываем его в файл 
 I2S_Init(I2S_MODE_ADC_BUILT_IN, I2S_BITS_PER_SAMPLE_32BIT); //Инициализируем интерфейс I2S
 //С помощью встроенного ADC ESP32 получаем звуковой сигнал с микрофона, преобразовываем его в цифру и записываем в файл 
 for (int j = 0; j < waveDataSize/numPartWavData; ++j) {
 I2S_Read(communicationData, numCommunicationData);
 for (int i = 0; i < numCommunicationData/8; ++i) {
 partWavData[2*i] = communicationData[8*i + 2];
 partWavData[2*i + 1] = communicationData[8*i + 3];
 }
 file.write((const byte*)partWavData, numPartWavData);
 }
 file.close(); //Закрываем файл для записи
 recording=false;
 Serial.println("finish");
 }
 else { //Обрабатываем ошибку записи файла
 Serial.println("Failed to start recording");
 }
}
 delay(10);
 } 
}

void setup() {
...
// Отправляем команду серверу на начало записи
 server.on("/record_begin", HTTP_GET, [](AsyncWebServerRequest *request){
 if (recording==false)
 {
 recording=true;
 xTaskCreatePinnedToCore(
 Task1code, /* Функция для задачи */
 "Task1", /* Имя задачи */
 10000, /* Размер стека */
 NULL, /* Параметр задачи */
 0, /* Приоритет */
 &Task1, /* Выполняемая операция */
 0); /* Номер ядра, на котором она должна выполняться */
 request->send(500, "text/plain", "Record started");
 } 
 });

...
}

Перезагрузки прекратились, но итоговый аудиофайл воспроизводится теперь очень большой скорости кратной скоростью. Пример здесь. В чем проблема разобраться мне так и не удалось.
В целом есть пища для размышлений в свободное время. Да файловая система Esp32 все же не лучший выбор для потоковой записи аудио файла. Гораздо лучше будет использовать для этих целей sd карту.

Подведение итогов

В целом, эксперимент я могу назвать успешным. В рамках проекта мне удалось создать прототип устройства для записи звука управлением по WiFI на базе ESP32. На его основе можно создать множество интересных и сложных проектов. Например wifi микрофон, автоответчик, диктофон, система прослушки или распознавания голоса или музыкальных треков.
Конечно, с реализацией есть некоторые проблемы, требующие решения. Но главное, что концепция рабочая и потоковая запись аудио на ESP32 с помощью модуля MAX9814 и сохранением файла в wav вполне возможна.  Архив с исходниками проекта вы сможете скачать в моем Github репозитории.

Материал также доступен на моем канале: Яндекс Дзен и в группе ВК
Категория: Программирование | Добавил: :, (20.07.2023)
Просмотров: 972 | Теги: ESP32, запись звука, MAX9814 | Рейтинг: 0.0/0
Поделиться:
Всего комментариев: 0
avatar