Как стать автором
Обновить

Обработка аудио на ESP32

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров6.7K

В этой статье я хочу поделиться своим опытом портирования проекта распознавания музыкальных жанров аудиозаписей на ESP32-C3. Исходный проект взят из репозитория книги TinyML-Cookbook_2E.
При анализе речи или других звуков важно выделить такие характеристики, которые отражают строение сигнала, но при этом не зависят от конкретных слов, громкости и других мешающих факторов. Для этого используют cepstrum, mel-cepstrum и MFCC - это шаги преобразования, которые переводят звук в удобную для анализа форму.

Краткое описание алгоритма

Мы сначала превращаем звуковой сигнал в спектр, потом берём логарифм спектра, а затем применяем ещё одно преобразование Фурье. Получившийся результат и называется cepstrum.

На практике:

  1. Берём короткий фрагмент звука (например, 20–40 мс).

  2. Строим его спектр (FFT).

  3. Берём логарифм амплитуды спектра — логарифм усиливает слабые составляющие и делает спектр более «ровным».

  4. Применяем снова FFT → результат — это cepstrum.

Логарифм превращает умножение (а спектр речи можно представить как произведение голоса и вокального тракта) в сложение. А затем второе FFT помогает разложить это сложение и извлечь структуру.

Когда человек говорит, звук речи можно условно представить как две части:

  1. Источник звука (excitation) — вибрация голосовых связок. Это периодический сигнал с основной частотой (например, 100 Гц).

  2. Формирующий тракт (vocal tract) — рот, язык, губы и т.п., которые формируют звук, усиливая/подавляя определённые частоты.

Когда мы применяем IFFT, в обратном временном представлении (quefrency) эти части попадают в разные диапазоны:

  • Низкие quefrency (0–10 мс) → форма голосового тракта (огибающая).

  • Высокие quefrency (10–20 мс) → возбуждение, периодичность, F0.

Quefrency — это шуточное название (перестановка букв от frequency, частота), которое обозначает ось в cepstrum. Но физически это время, выраженное в миллисекундах. То есть:

  • Низкие значения quefrency (в начале графика) отражают гладкую форму спектра — это так называемая огибающая.

  • Высокие значения могут содержать периодические пики, связанные с основным тоном (F0).

Форманты — это устойчивые пики в спектре речи. Они возникают из-за резонанса в голосовом тракте (ротовой полости, гортани и т.д.).
Например, когда мы произносим разные гласные, положение языка и губ изменяет резонансные частоты — именно они и называются формантами.

Каждый гласный звук имеет свой «узор» формант, и именно по ним мы отличаем одни гласные от других.

Если в cepstrum оставить только низкие значения quefrency (обрезать всё остальное), а затем применить обратное преобразование, мы получим гладкую огибающую спектра. Это похоже на сглаживание: мы убираем детали и оставляем основную форму.

Mel-шкала и фильтры

Человеческое ухо слышит не линейно: мы лучше различаем низкие частоты, чем высокие.

Mel-шкала — это шкала, которая приближает частотное восприятие к человеческому.

Например:

  • Разница между 500 и 1000 Гц кажется нам большой.

  • А разница между 5000 и 5500 Гц — почти незаметна.

Mel-фильтры — это набор треугольных фильтров, равномерно размещённых по mel-шкале. Они позволяют выделить энергию в частотных диапазонах, важных для слуха.

MFCC (Mel-Frequency Cepstral Coefficients) — это компактное числовое представление звука, которое используется в машинном обучении.

Шаги получения MFCC:

  1. Делим сигнал на окна.

  2. Для каждого окна:

    • Строим спектр.

    • Пропускаем его через mel-фильтры.

    • Берём логарифм результата.

    • Применяем DCT (дискретное косинус-преобразование) — это как ещё одно сглаживание и декорреляция.

На выходе получаем небольшой набор коэффициентов, которые:

  • описывают форму спектра;

  • устойчивы к шуму и изменениям громкости;

  • хорошо подходят для классификации и распознавания речи.

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

Реализация ML-модели

Для получения исходного кода модели распознавания музыкальных жанров необходимо преобразовать обученную нейросеть в tflite, а затем в model.h. Для этого воспользуемся листингом Jupyter Notebook prepare_model.ipynb. Файл можно запустить локально, например в VScode с расширением Jupyter, или с помощью Google Colab.
В исходном файле автор выбрал три типа музыкальных жанра для распознавания: 'disco', 'jazz', 'metal' датасета GTZAN Dataset из соображений экономии ресурсов на Raspberry Pico. В нашем примере обучим модель на всех десяти жанрах датасета. На практике это занимает не намного больше ресурсов, и ESP32 с этим вполне хорошо справляется.
Для предсказания временных рядов используется Lstm (рекуррентный) слой ML-модели.

x = norm_layer(input)
x = layers.LSTM(32, return_sequences=True)(x)
x = layers.Dropout(0.5)(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)
x = layers.Dense(len(LIST_GENRES), activation='softmax')(x)

По умолчанию в Python устанавливается Tensorflow последней версии, который содержит Keras 3 версии. И это влечет несколько проблем. В файле prepare_model.ipynb используется Keras 2 синтаксис конвертации модели в формат .tflite. Рефакторинг на keras 3 решается достаточно легко если погуглить, или обратиться к "всемогущему" Чату. Но со второй проблемой вы столкнетесь только когда запустите микроконтроллер. В консоли последовательного порта будет сообщение, что в модели отсутствуют необходимые OPS слои. Возможно это частный случай компонента espressif/esp-tflite-micro.

Установим необходимые зависимости. Расширим первую ячейку prepare_model.ipynb, добавив к существующим зависимостям tensorflow с выбранной версией 2.12.0.

!pip install numpy==1.23.5
!pip install cmsisdsp==1.9.6
!pip install tensorflow==2.12.0

Далее необходимо скачать и распаковать GTZAN Dataset. В Jupyter указан каталог для образцов mgr_dataset. Для работы в Colab нужно создать каталог с таким же именем в корне Google drive и туда расположить образцы мелодий из genres_original, или в самом Jupyter-файле изменить имя каталога.

Подключаем Google drive к Colab.

from google.colab import drive
drive.mount('/content/drive')

После выполнения блока кода всплывет диалог авторизации Google, а затем диалог выбора разрешений. Если выбрать не все разрешения, то скорее всего диск не подключиться, и не будет доступа к каталогам с образцами мелодий.

Для того, чтобы обучить модель для всех 10 типов жанра из датасета, необходимо расширить список LIST_GENRES

LIST_GENRES = ['blues', 'classical', 'country', 'disco', 'hiphop', 'jazz', 'metal', 'pop', 'reggae', 'rock']

После этого можно запустить выполнение prepare_model.ipynb и идти заваривать чай.
Если верить метрикам времени выполнения каждого блока кода в Colab, то весь процесс занял 18 минут.

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

MFCC-заголовки в Google Colab
MFCC-заголовки в Google Colab

Эти заголовки (кроме test_dst.h, test_src.h) нужно расположить в каталоге основного компонента.

Для установки зависимости Tensorflow lite в ESP-IDF проекте выполняем команду в ESP-IDF терминале.

idf.py add-dependency "esp-tflite-micro"

После чего манифест idf_component.yml будет иметь следующее содержимое

## IDF Component Manager Manifest File
dependencies:
  espressif/esp-tflite-micro: "*"
  ## Required IDF version
  idf:
    version: ">=4.1.0"

В espressif/esp-tflite-micro нужно явно подключать слои модели. Для этого загрузим файл model.tflite на ресурс https://netron.app. Данная модель имеет следующую структуру.

Tensorflow lite model
Tensorflow lite model

В статье Machine learning на ESP32 я описывал процесс обучения модели, и реализацию исходного кода для ESP32. Здесь приведу уже готовый листинг для данного проекта модели.

void tflu_initialization() {
    if (tensor_arena == NULL) {
        tensor_arena = (uint8_t *) heap_caps_malloc(tensor_arena_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
      }
      if (tensor_arena == NULL) {
        ESP_LOGE(TAG, "Couldn't allocate memory of %d bytes\n", tensor_arena_size);
        return;
      } 

    tflu_model = tflite::GetModel(model_tflite);
    if (tflu_model->version() != TFLITE_SCHEMA_VERSION) {
        ESP_LOGE(TAG, "Model schema version mismatch!");
        while (1);
    }

    static tflite::MicroMutableOpResolver<9>; resolver;
    resolver.AddQuantize();
    resolver.AddDequantize();
    resolver.AddSub();
    resolver.AddMul();
    resolver.AddUnidirectionalSequenceLSTM();
    resolver.AddStridedSlice();
    resolver.AddFullyConnected();
    resolver.AddSoftmax();

    static tflite::MicroInterpreter static_interpreter(
        tflu_model, resolver, tensor_arena, tensor_arena_size);

    tflu_interpreter = &static_interpreter;
    tflu_interpreter->AllocateTensors();

    tflu_i_tensor = tflu_interpreter->input(0);
    tflu_o_tensor = tflu_interpreter->output(0);

    ESP_LOGI(TAG, "TensorFlow Lite initialization completed");
}

Модуль ESP32-C3

Для проекта был выбран модуль ESP32-C3 0.42 OLED. Он скромно ждал своего момента с тех времен, когда мне захотелось познакомится с ассемблером RISK-V. Модуль имеет следующие особенности:

  • RISC-V SoC @ 160MHz with 4MB flash and 400kB RAM

  • WS2812B RGB serial LED

  • 0.42-inch OLED over I2C

  • Qwiic I2C connector

  • One pushbutton

  • Onboard ceramic chip antenna

  • On-chip USB-UART converter

Главной фишкой данного модуля является 0.42-дюймовый OLED-дисплей, в нашем случае будем на нем показывать распознанный жанр.

Модуль микрофона MAX9814

Характеристики:

  • Напряжение питания: 2,7 В - 5,5 В при токе 3 мА

  • Выход: 2Vpp при смещении 1,25В

  • Частотная характеристика: 20 Гц - 20 кГц

  • Программируемый коэффициент атаки и спада

  • Автоматическое усиление, выбирается максимум из 40дБ, 50дБ или 60дБ

  • Низкая входная плотность шума 30 нВ/Гц

  • Низкий THD: 0,04% (тип)

  • Размеры: 25х14мм , диаметр отверстия 2мм, расстояние от центра до центра отверстия 10мм

  • Чип в основе этого усилителя – MAX9814

  • Электретный микрофон

Подключение микрофона на макетной плате
Подключение микрофона на макетной плате

Вывод OUT микрофона подключается к аналоговому входу A0 ESP32-С3. Вывод Gain нужно подключить к V+, тогда усиление микрофона будет максимальным.
Проверить работу микрофона можно с помощью осциллографа. Для этого нужно подать питание на микрофон, щуп подключить к OUT, и воспроизвести аудиосигнал.

Программирование 0.42-inch OLED дисплея

Для программирования монохромных LCD-дисплеев существует библиотека u8g2. Пример исходного кода для 0.42 OLED на Arduino можно найти здесь. Из этого примера мы можем найти строчку конфигурации для дисплея 0.42.

U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);   // EastRising 0.42" OLED

Клонируем u8g2 репозиторий в каталог компонентов нашего проекта ./components. В этом проекте CMakeLists.txt уже содержит команды для ESP-IDF

if(COMMAND idf_component_register)
    idf_component_register(SRCS "${COMPONENT_SRCS}" INCLUDE_DIRS csrc)
    return()
endif()

Для упрощения работы с u8g2 в ESP-IDF можно воспользоваться hal-модулем из nkolban/esp32-snippets репозитория. Нам нужны два файла: u8g2_esp32_hal.h и u8g2_esp32_hal.c, которые копируем в каталог с основным модулем main.c.
Пример работы с hal-модулем находится в этом же репозитории test_SSD1306_i2c.c

Чтобы компилировать hal-модуль совместно с C++ классами (Tensorflow, CMCIS) нужно немного доработать u8g2_esp32_hal.h, а именно - добавить директивы extern "C".

#ifndef U8G2_ESP32_HAL_H_
#define U8G2_ESP32_HAL_H_

#ifdef __cplusplus
extern "C" {
#endif

//исходный код

#ifdef __cplusplus
}
#endif

#endif /* U8G2_ESP32_HAL_H_ */

В основном модуле main.c подключаем заголовок #include "u8g2_esp32_hal.h"

В main-функции инициализируем hal-модуль и OLED, как в примере из test_SSD1306_i2c.c. Так как дисплей подключается к ESP32 I2C-шине, нам нужно указать выводы PIN_SDA и PIN_SDA. В случае ESP32-C3 - это пины 5 и 6 соответственно. I2C адрес дисплея - 0x78.

#include "u8g2_esp32_hal.h"

#define PIN_SDA GPIO_NUM_5
#define PIN_SCL GPIO_NUM_6

u8g2_t u8g2;
// initialize the u8g2 hal
u8g2_esp32_hal_t u8g2_esp32_hal = U8G2_ESP32_HAL_DEFAULT;
u8g2_esp32_hal.sda = PIN_SDA;
u8g2_esp32_hal.scl = PIN_SCL;
u8g2_esp32_hal_init(u8g2_esp32_hal);

// initialize the u8g2 library
u8g2_Setup_ssd1306_i2c_72x40_er_f(
	&u8g2,
	U8G2_R0,
	u8g2_esp32_i2c_byte_cb,
	u8g2_esp32_gpio_and_delay_cb);

// set the display address
u8x8_SetI2CAddress(&u8g2.u8x8, 0x78);

// initialize the display
u8g2_InitDisplay(&u8g2);

// wake up the display
u8g2_SetPowerSave(&u8g2, 0);

Чтобы вывести текст на дисплей необходимо вызывать функцию u8g2_DrawStr. Перед этим очищаем буфер u8g2_ClearBuffer, устанавливаем шрифт u8g2_SetFont, а после отсылаем буфер u8g2_SendBuffer.

u8g2_ClearBuffer(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr);
u8g2_DrawStr(&u8g2, 0,15, "Hello world");
u8g2_SendBuffer(&u8g2);

где,

  • &u8g2 - указатель на структуру u8g2_t u8g2,

  • 0,15 - координаты x, y в пикселях с верхнего левого края.

Подключение компонента CMSIS

В исходном проекте для вычисления MFCC-коэффициентов в микроконтроллере используется CMSIS-библиотека. Автор уже скомпилировал этот проект, и предоставил ссылку на zip-архив. Этот архив затем подключается, как библиотека к Arduino IDE.
Для ESP-IDF проекта нужно подключить CMSIS-библиотеку как компонент. Для этого нужно склонировать CMSIS-DSP репозиторий в каталог /components. А также нам нужны CMSIS Core заголовки, которые берем отсюда https://github.com/ARM-software/CMSIS_5/tree/develop/CMSIS/Core.
После этого нужно составить make-файлы для компонентов CMSIS.

CMakefile.txt для CMSIS Core.

idf_component_register(
    INCLUDE_DIRS
        "Include"  # Path to the CMSIS-Core include directory
)

CMakefile.txt CMSIS-DSP

# Define the path to the CMSIS-DSP library
set(CMSISDSP "${CMAKE_CURRENT_LIST_DIR}")

# Recursively gather all source files from the CMSIS-DSP Source directory
file(GLOB_RECURSE CMSIS_DSP_SRCS "${CMSISDSP}/Source/*.c")

# Exclude NEON-specific files if NEON is not supported
list(FILTER CMSIS_DSP_SRCS EXCLUDE REGEX "_arm_mat_mult_neon.c")

idf_component_register(
    SRCS ${CMSIS_DSP_SRCS}
    INCLUDE_DIRS
        "${CMSISDSP}/Include"
        "${CMSISDSP}/PrivateInclude"
        "${CMAKE_CURRENT_LIST_DIR}/../CMSIS-Core/Include"  # Add CMSIS-Core include path
        "."
)

В CMSIS-DSP я исключил модуль armmat_mult_neon.c, т.к. он требовал дополнительных зависимостей, а в проекте он не используется.

Вся логика обработки MFCC вынесена в отдельный модуль MFCC_Q15.h, MFCC_Q15.c. Подробное описание исходного кода MFCC можно посмотреть в книге TinyML-Cookbook_2E. Для нас важно то, что вызывается метод void MFCC_Q15::run(const q15_t* src, float* dst). В метод передаем указатель на буфер с аудиосемплами и указатель на входные данные тензора. Затем вычисляются MFCC-коэффициенты, которые передаются в тензор.

 mfccs.run((const q15_t*)audio_buffer, tflu_i_tensor->data.f);

Далее производится инференс модели.

tflu_interpreter->Invoke();

Обработка аналогового сигнала на ESP32

Согласно руководству ESP-IDF v4.4 - Analog to Digital Converter ESP32-C3 содержит два ADC с поддержкой 6 каналов измерения (аналоговых входов). В ESP-IDF v5.4 ADC API будет отличаться, но архитектура и конфигурация имеет тот же смысл.

Поддерживаемые каналы:

ADC1:
5 каналов: GPIO0 – GPIO4

ADC2:
1 канал: GPIO5

Аттенюация ADC
Vref — это опорное напряжение, используемое внутри ESP32-C3 для измерения входного сигнала. ADC способен измерять напряжения от 0 В до Vref. Среднее значение Vref составляет 1.1 В, но оно может варьироваться между чипами. Для измерения напряжений выше Vref применяется аттенюация (ослабление сигнала). Доступны четыре уровня аттенюации:

Аттенюация

Диапазон измеряемого напряжения

ADC_ATTEN_DB_0

0 мВ - 750 мВ

ADC_ATTEN_DB_2_5

0 мВ - 1050 мВ

ADC_ATTEN_DB_6

0 мВ - 1300 мВ

ADC_ATTEN_DB_11

0 мВ - 2500 мВ

Преобразование ADC
Преобразование аналогового сигнала в цифровой представляет собой получение необработанного значения. Разрешение ADC в режиме одиночного чтения — 12 бит. Основные функции:

  • adc1_get_raw()

  • adc2_get_raw()

  • adc_digi_read_bytes()

Для расчета напряжения на основе необработанных данных используется формула:

Vout = Dout * Vmax / Dmax

где:

  • Vout — измеренное напряжение

  • Dout — цифровое значение (raw)

  • Vmax — максимальное измеряемое напряжение (см. таблицу аттенюации)

  • Dmax — максимальное значение цифрового выхода, 4095 для 12 бит

Если на плате поддерживается калибровка ADC через eFuse, можно использовать функцию esp_adc_cal_raw_to_voltage(), которая возвращает откалиброванное значение напряжения (в мВ). В этом случае формула выше не требуется. Если калибровка используется на платах без eFuse, будут выводиться предупреждения.

Ограничения ADC

  • Модуль ADC2 также используется Wi-Fi, поэтому чтение через adc2_get_raw() может завершиться неудачей между вызовами esp_wifi_start() и esp_wifi_stop().

  • Один модуль ADC может работать только в одном режиме: либо непрерывном, либо одиночном.

  • ADC1 и ADC2 не могут одновременно работать в режиме одиночного чтения. Один будет заблокирован до завершения работы другого.

  • В непрерывном режиме частота дискретизации должна находиться в пределах SOC_ADC_SAMPLE_FREQ_THRES_LOW и SOC_ADC_SAMPLE_FREQ_THRES_HIGH.

Использование драйвера ADC
Доступны два режима работы:

  • одиночное чтение (single read) — подходит для низкочастотных измерений

  • непрерывное чтение (DMA) — для высокочастотной выборки

Если пин не подключён к источнику сигнала, результаты ADC будут случайными.

Режим непрерывного чтения (DMA)

Порядок использования:

  1. Инициализация драйвера: adc_digi_initialize()

  2. Настройка контроллера: adc_digi_controller_config()

  3. Запуск чтения: adc_digi_start()

  4. Чтение данных: adc_digi_read_bytes()

  5. Остановка чтения: adc_digi_stop()

  6. Деинициализация драйвера: adc_digi_deinitialize()

Пример кода находится в examples/peripherals/adc/dma_read в ESP-IDF.

Режим одиночного чтения

Перед чтением необходимо настроить параметры:

  • Для ADC1: adc1_config_width() и adc1_config_channel_atten()

  • Для ADC2: adc2_config_channel_atten(); ширина чтения задаётся при вызове adc2_get_raw()

Аттенюация настраивается для каждого канала отдельно. Функции чтения:

  • adc1_get_raw()

  • adc2_get_raw()

Пример — examples/peripherals/adc/single_read.

Минимизация шума
ADC чувствителен к шуму. Рекомендуется подключать байпасный керамический конденсатор (например, 100 нФ) к активному пину, а также использовать многократную выборку (multisampling) для уменьшения влияния шумов.

Калибровка ADC
Состоит из аппаратной и программной части.

Аппаратная калибровка

Производится автоматически драйвером и включает:

  1. Настройку опорного напряжения (bandgap)

  2. Коррекцию смещения в характеристике Vin–Dout (обычно: f(x) = A * x + B, где B — смещение)

Результаты — это всё ещё "сырые" данные, требующие дальнейшего преобразования по формуле или через программную калибровку.

Программная калибровка

  1. Проверка поддержки калибровки через eFuse: esp_adc_cal_check_efuse()

  2. Расчёт калибровочных характеристик: esp_adc_cal_characterize(). Они зависят от модуля и уровня аттенюации. Например, ADC1 канал 0 и канал 2 при 11 дБ будут иметь одинаковые характеристики, а ADC1 канал 0 при 6 дБ уже будет отличаться.

  3. Получение напряжения: esp_adc_cal_raw_to_voltage()

Результаты — откалиброванные значения напряжения. Пример кода — в examples/peripherals/adc/single_read.

Для использования ADC необходимо подключить заголовки

#include "driver/adc.h"
#include "esp_adc_cal.h"

Далее необходимо инициализировать ADC.

esp_adc_cal_characteristics_t* adc_chars;

// Initialize ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
    adc_chars = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
    if (adc_chars == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for ADC characteristics");
        return;
    }
    esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);

ADC_WIDTH_BIT_12 - используем 12-битный ADC;
ADC_ATTEN_DB_11 - диапазон напряжения 0 мВ - 2500 мВ, т. к. опорное напряжение на микрофоне с подтянутым усиление = 1.25 В;
ADC1_CHANNEL_0 - используем вывод GPIO0 на ESP32-C3.

Согласно документации с примерами имеет смысл сделать калибровку ADC с помощью вызова функции esp_adc_cal_characterize, перед этим не забываем выделить память для adc_chars.

В исходном примере 11_music_genre_classification.ino после считывания ADC производится вычитание смещения согласно формуле

bias = \frac{V_{ref} \cdot N_{bit}}{V_{cc}}
int BIAS_MIC  =  1552; // (1.25V * 4095) / 3.3

На практике более оптимальным оказалось динамическое вычисление среднего смещения во время запуска ESP32. Т. е., считываем значение ADC, суммируем в каждом цикле, а затем вычисляем среднее за 1000 итераций.

int measure_bias_offset()
{
    int sum = 0;
    const int samples = 1000;
    for (int i = 0; i &lt; samples; i++) {
        sum += adc1_get_raw(ADC1_CHANNEL_0);
        ets_delay_us(1000); 
    }
    return sum / samples;
}

Как и в исходном примере обработка аудиосемпла выполняется с помощью вызова коллбэка таймера. Можно попробовать выполнить обработку и в Freertos-задаче, но в моем случае определение жанра работало плохо, и я решил оставить таймер.

void timer_callback(void* arg) {
    if (buffer_index &lt; AUDIO_LENGTH_SAMPLES) {
        int adc_value = adc1_get_raw(ADC1_CHANNEL_0); // Read ADC
        //printf("Raw ADC Value: %d\n", adc_value);
        audio_buffer[buffer_index++] = adc_value - bias_offset; // Adjust for bias
    } else {
        buffer_ready = true;
    }
}
const esp_timer_create_args_t timer_args = {
        .callback = &amp;timer_callback,
        .name = "audio_timer"
    };
    esp_timer_handle_t timer;
    esp_timer_create(&amp;timer_args, &amp;timer);
    esp_timer_start_periodic(timer, 1000000 / SAMPLE_RATE); // Sampling rate

Настраиваем периодичность таймера на 1/SAMPLE_RATE сек, где SAMPLE_RATE=22050. С этим значением частоты выборки ранее производилось вычисление MFCC-коэффициентов, а затем обучение модели, но можно также выбрать и стандартное для CD - 44100, и заново выполнить Jupyter-файл.
В коллбэке считываем значение ADC, отнимаем смещение опорного напряжение и устанавливаем значение буфера.

Аудио-жанр определяется на основании выходных данных тензора.

static const char *label[] = {"blues", "classical", "country", "disco", "hiphop", "jazz", "metal", "pop", "reggae", "rock"};

size_t max_index = 0;
        float max_value = 0;
        for (size_t i = 0; i &lt; 10; i++) {
            if (tflu_o_tensor-&gt;data.f[i] &gt; max_value) {
                max_index = i;
                max_value = tflu_o_tensor-&gt;data.f[i];
            }
        }

        ESP_LOGI(TAG, "Predicted genre: %s", label[max_index]);

В итоге структура проекта для ESP32-C3

├── main
│   ├── main.cc                # Main application logic
│   ├── u8g2_esp32_hal.h       # HAL for U8G2 library
│   ├── u8g2_esp32_hal.c       # Implementation of U8G2 HAL
│   ├── MFCC_Q15.cc            # MFCC computation logic
│   ├── model.h                # TensorFlow Lite model header
│   ├── dct_wei_mtx_q15_T.h    # DCT weight matrix for MFCC computation
│   ├── hann_lut_q15.h         # Hanning window lookup table
│   ├── log_lut_q13_3.h        # Logarithm lookup table
│   ├── mel_wei_mtx_q15_T.h    # Mel filter bank weights
│   ├── mfccs_consts.h         # Constants for MFCC computation
├── components
│   ├── esp-tflite-micro       # TensorFlow Lite Micro component
│   ├── esp-mfcc               # MFCC computation library
│   ├── u8g2                   # U8G2 library for OLED display
├── CMakeLists.txt             # Build configuration
├── README.md                  # Project documentation

Исходный код проекта выложен в репозитории genre-classification-esp32.

Определение reggae-жанра

Определение metall-жанра

Заключение

Этот проект служит практическим примером развертывания моделей машинного обучения на встраиваемых системах с ограниченными ресурсами, открывая возможности для приложений в обработке звука и распознавании речи. Удалось успешно портировать исходный код 11_music_genre_classification.ino для ESP-IDF проекта ESP32-C3. Для реализации логики системы были подключены библиотеки цифровой обработки CMSIS, а также библиотека для работы с OLED-дисплеем u8g2.
Модель обработки аудиофайлов расширена для определения 10 музыкальных жанров.
Из недостатков данного проекта следует отметить то, что логика на ESP32 не всегда хорошо справляется с определением жанров. Если сделать тестовый проект в Jupyter, а в качестве входных данных предоставлять аудиофайл из тренировочного датасета, то в большинстве случаев модель справляется с определением жанра. Но если предоставить рандомный музыкальный файл, то результат может быть не предсказуемый. Опытным путем определено, что модель хорошо определяет жанры с выраженными музыкальными композициями, например где присутствуют гитарные солло-партии в металл-группах. Практическая ценность конкретного проекта скорее всего не велика. Проект хорошо подходит в качестве знакомства с технологиями обработки и распознаванием звука.

Используемые источники

Теги:
Хабы:
+26
Комментарии5

Публикации

Работа

Программист С
34 вакансии
Data Scientist
38 вакансий

Ближайшие события