Найти тему
Project A.L.T.

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

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

Введение

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

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

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

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

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

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 будем по следующей схеме:

Схема соединения
Схема соединения

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

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

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

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

Для начала определимся как должна работать программа и какие дополнительные библиотеки нам понадобятся. Для подключения к устройству мы будем использовать 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 кнопками.

Веб интерфейс
Веб интерфейс

Пока нажатия на эти кнопки тоже приводят к ошибке 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, руководствуясь следующей схемой:

Схема выводов esp32
Схема выводов esp32

Далее напишем функцию блочного чтения байт данных получаемых по 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 репозитории.

Материал также доступен на моем сайте https://projectalt.ru.