В этой статье я хочу поделиться своим опытом портирования проекта распознавания музыкальных жанров аудиозаписей на ESP32-C3. Исходный проект взят из репозитория книги TinyML-Cookbook_2E.
При анализе речи или других звуков важно выделить такие характеристики, которые отражают строение сигнала, но при этом не зависят от конкретных слов, громкости и других мешающих факторов. Для этого используют cepstrum, mel-cepstrum и MFCC - это шаги преобразования, которые переводят звук в удобную для анализа форму.
Краткое описание алгоритма
Мы сначала превращаем звуковой сигнал в спектр, потом берём логарифм спектра, а затем применяем ещё одно преобразование Фурье. Получившийся результат и называется cepstrum.
На практике:
Берём короткий фрагмент звука (например, 20–40 мс).
Строим его спектр (FFT).
Берём логарифм амплитуды спектра — логарифм усиливает слабые составляющие и делает спектр более «ровным».
Применяем снова FFT → результат — это cepstrum.
Логарифм превращает умножение (а спектр речи можно представить как произведение голоса и вокального тракта) в сложение. А затем второе FFT помогает разложить это сложение и извлечь структуру.
Когда человек говорит, звук речи можно условно представить как две части:
Источник звука (excitation) — вибрация голосовых связок. Это периодический сигнал с основной частотой (например, 100 Гц).
Формирующий тракт (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:
Делим сигнал на окна.
Для каждого окна:
Строим спектр.
Пропускаем его через 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-коэффициенты.

Эти заголовки (кроме 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. Данная модель имеет следующую структуру.

В статье 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)
Порядок использования:
Инициализация драйвера: adc_digi_initialize()
Настройка контроллера: adc_digi_controller_config()
Запуск чтения: adc_digi_start()
Чтение данных: adc_digi_read_bytes()
Остановка чтения: adc_digi_stop()
Деинициализация драйвера: 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
Состоит из аппаратной и программной части.
Аппаратная калибровка
Производится автоматически драйвером и включает:
Настройку опорного напряжения (bandgap)
Коррекцию смещения в характеристике Vin–Dout (обычно: f(x) = A * x + B, где B — смещение)
Результаты — это всё ещё "сырые" данные, требующие дальнейшего преобразования по формуле или через программную калибровку.
Программная калибровка
Проверка поддержки калибровки через eFuse: esp_adc_cal_check_efuse()
Расчёт калибровочных характеристик: esp_adc_cal_characterize(). Они зависят от модуля и уровня аттенюации. Например, ADC1 канал 0 и канал 2 при 11 дБ будут иметь одинаковые характеристики, а ADC1 канал 0 при 6 дБ уже будет отличаться.
Получение напряжения: 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 производится вычитание смещения согласно формуле
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 < 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 < 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 = &timer_callback,
.name = "audio_timer"
};
esp_timer_handle_t timer;
esp_timer_create(&timer_args, &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 < 10; i++) {
if (tflu_o_tensor->data.f[i] > max_value) {
max_index = i;
max_value = tflu_o_tensor->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, а в качестве входных данных предоставлять аудиофайл из тренировочного датасета, то в большинстве случаев модель справляется с определением жанра. Но если предоставить рандомный музыкальный файл, то результат может быть не предсказуемый. Опытным путем определено, что модель хорошо определяет жанры с выраженными музыкальными композициями, например где присутствуют гитарные солло-партии в металл-группах. Практическая ценность конкретного проекта скорее всего не велика. Проект хорошо подходит в качестве знакомства с технологиями обработки и распознаванием звука.