esp32-a2dp-xq
ESP32 Bluetooth A2DP source with SBC-XQ: Dual Channel bitpool 38, 452 kbps to headphones, fallback to standard SBC, AVRCP volume and buttons
C0пуш
README из репозиторияesp32-a2dp-xq
Библиотека для ESP32, которая передаёт звук в Bluetooth-наушники по A2DP с SBC-XQ: SBC в режиме Dual Channel с битпулом 38, 452 кбит/с при 44,1 кГц. Если наушники XQ не принимают, библиотека переходит на Joint Stereo с битпулом 53 (328 кбит/с).
English · Справочник API · История изменений
Состояние
Версию 0.1.0 я ещё не запускал на железе. Первую проверку с наушниками я запишу в историю изменений вместе с моделями наушников.
Что проверено:
- В тестах на компьютере проходят 1315 проверок: длина кадра и битрейт SBC, подбор битпула кодером Bluedroid, решение о XQ по возможностям наушников, PCM-буфер с опустошением и сбросом, шкалы громкости, файл памяти устройств и фаззинг парсера.
- CI собирает примеры для ESP-IDF под ESP32 на ESP-IDF 5.3, 5.5, 6.0 и 6.1 с включённым Bluetooth Classic, а также под ESP32-S3, где библиотека собирается без Bluetooth Classic и сообщает, что он не поддерживается. Предупреждение в коде библиотеки останавливает сборку.
- CI собирает примеры для Arduino на ядре 3.1.3 (ESP-IDF 5.3) и на последнем ядре, а также через PlatformIO.
Сравнение
| esp32-a2dp-xq | ESP32-A2DP | |
|---|---|---|
| SBC-XQ, Dual Channel, битпул 38, 452 кбит/с | Да, ESP-IDF 6.0+ | Нет |
| Обычный SBC, Joint Stereo, битпул 53 | Да | Да |
| Вход | 16 бит, или 32 бита с TPDF-дизерингом до 16 | 16 бит |
| Память XQ по устройствам и чёрный список | Да | Нет |
| ESP32 в роли Bluetooth-колонки (A2DP sink) | Нет | Да |
Stream из Arduino и AudioTools |
Нет | Да |
| Проверено на железе | Пока нет | Да |
Для Bluetooth-колонки на ESP32 или для быстрого старта с AudioTools подходит ESP32-A2DP. Эта библиотека работает только как источник звука и добавляет более высокий битрейт SBC.
Быстрый старт
#include "a2dp_xq.h"
void app_main(void) {
nvs_flash_init(); // ключи сопряжения хранятся в NVS
a2dp_xq_config_t cfg = A2DP_XQ_CONFIG_DEFAULT();
cfg.device_name = "My ESP32";
a2dp_xq_init(&cfg);
a2dp_xq_connect("aa:bb:cc:dd:ee:ff"); // или a2dp_xq_scan(true)
a2dp_xq_start();
static int16_t pcm[256 * 2]; // стерео, 44,1 кГц
for (;;) a2dp_xq_write_s16(pcm, 256, portMAX_DELAY); // ждёт, пока в буфере есть место
}
Установка
ESP-IDF 5.3 и новее:
idf.py add-dependency "olerast67/esp32-a2dp-xq^0.1.0"
PlatformIO (platformio.ini):
lib_deps = https://github.com/olerast67/esp32-a2dp-xq.git#v0.1.0
Arduino IDE с ядром ESP32 3.x: скачайте esp32-a2dp-xq-0.1.0.zip со страницы Releases и добавьте через «Скетч > Подключить библиотеку > Добавить .ZIP библиотеку». Пример с синусом занимает 1,08 МБ из 1,25 МБ раздела приложения по умолчанию. Если рядом есть другой код (esp32-audio-player, Wi-Fi), выберите «Инструменты > Partition Scheme > Huge APP».
В sdkconfig проекта на ESP-IDF нужен Bluetooth Classic с A2DP. В примерах эти строки лежат в sdkconfig.defaults:
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_A2DP_ENABLE=y
CONFIG_BT_AVRCP_ENABLED=y
Как выбирается SBC-XQ
У SBC здесь важны два стереорежима. В Joint Stereo оба канала делят один битпул, и кодер Bluedroid в ESP-IDF держит 328 кбит/с, это битпул 53. В Dual Channel у каждого канала свой битпул, и при битпуле 38 на канал поток занимает 452 кбит/с. Спецификация A2DP 1.3 (раздел 4.3.2) обязывает каждый приёмник декодировать Dual Channel. В PipeWire такая настройка называется кодеком sbc_xq.
После подключения библиотека до 2 секунд ждёт от наушников описание их возможностей SBC (ESP_A2D_REPORT_SNK_CODEC_CAPS_EVT). XQ запрашивается, если выполнены три условия:
- XQ разрешён в настройках (
allow_sbc_xq). - Наушники поддерживают 44,1 кГц, Dual Channel, 16 блоков, 8 поддиапазонов, распределение loudness, и их диапазон битпула включает 38.
- В памяти устройств для этих наушников нет отметки о сбое, и имени нет в чёрном списке.
Запрос уходит через esp_a2d_source_set_pref_mcc(): 44,1 кГц, Dual Channel, битпул 38..38. После этого кодер Bluedroid поднимает целевой битрейт шагами по 5 кбит/с, пока не получится битпул 38. Если наушники отказали, не ответили за 4 секунды или не прислали свои возможности, поток идёт в Joint Stereo с диапазоном битпула от стека.
SBC-XQ требует ESP-IDF 6.0, где появилась esp_a2d_source_set_pref_mcc(). На старых версиях, в том числе в ядре Arduino 3.x, библиотека пишет в лог «SBC-XQ needs ESP-IDF 6.0 or newer» и передаёт Joint Stereo. Функция a2dp_xq_supported() сообщает, какой из двух вариантов в сборке.
Память устройств
Результат XQ для каждых наушников хранится в файле <state_dir>/a2dp_xq_devices.txt, по строке на устройство:
aa:bb:cc:dd:ee:ff xq=ok fails=0 noxq=0 seen=12 name=WH-1000XM4
Если поток XQ держится 30 секунд, устройство получает отметку xq=ok. Обрыв связи в первые 20 секунд после старта потока XQ считается одним сбоем, после двух сбоев подряд ставится xq=fail. Если наушники приняли настройку XQ, а потом дважды отказались запускать поток, библиотека сразу ставит xq=fail и переподключается в Joint Stereo. Через a2dp_xq_set_xq_allowed() пользователь может выключить или включить XQ для одного устройства, включение стирает прошлую отметку о сбое. В файле помещается 16 устройств; при переполнении удаляется то, которым пользовались давнее всех.
Встроенный чёрный список ищет в имени устройства без учёта регистра целые слова: Soundcore 2, CMF Buds 2a, DC800, S305, PMK TWS, Phonak. Я собрал их из списка особенностей Bluetooth-устройств в PipeWire и из баг-репортов других открытых плееров.
Без state_dir память хранится в RAM и стирается при перезагрузке.
Звук и громкость
Приложение пишет стереокадры с частотой 44,1 кГц функциями a2dp_xq_write_s16() или a2dp_xq_write_q31(). Кадры попадают в буфер на 16384 кадра, который заполняется до 300 мс; память под него берётся из PSRAM, если она есть на плате. 32-битный вход переводится в 16 бит с TPDF-дизерингом, 16-битный вход на полной громкости копируется без изменений. После опустошения буфера кодер ждёт 100 мс звука и до этого отправляет тишину.
a2dp_xq_set_volume_db() передаёт громкость наушникам с абсолютной громкостью AVRCP (0..127, 127 соответствует 0 дБ, один шаг равен 60/127 дБ), и сам поток идёт на 0 дБ. Для наушников без абсолютной громкости, а также пока наушники её не подтвердили, библиотека ослабляет сигнал при переводе в 16 бит. Изменения громкости кнопками наушников приходят в remote_cb как A2DP_XQ_REMOTE_VOLUME_ABS.
Кнопки наушников (воспроизведение, пауза, стоп, следующий, предыдущий, громкость) приходят в remote_cb. Событие A2DP_XQ_REMOTE_LINK_DOWN приходит, когда звук запущен, наушники не подключены и переподключение не ожидается.
Примеры
| Пример | ESP-IDF | Arduino |
|---|---|---|
| Синус 440 Гц в наушники | sine_to_headphones | SineToHeadphones |
| Выбранный кодек, битпул, битрейт и статистика связи | codec_report | CodecReport |
| WAV-файл с SD-карты | WavFileToHeadphones |
FLAC, MP3, Vorbis и альбомы без пауз между треками с SD-карты в наушники воспроизводит пример bluetooth_headphones из esp32-audio-player. Для вывода в Bluetooth он использует эту библиотеку.
Требования
- Классический ESP32 (ESP32, ESP32-WROOM, ESP32-WROVER, ESP32-PICO). У ESP32-S3, C3 и C6 нет Bluetooth Classic: на них библиотека собирается, а
a2dp_xq_init()возвращаетESP_ERR_NOT_SUPPORTED. - ESP-IDF 5.3 и новее или ядро Arduino 3.x. Для SBC-XQ нужен ESP-IDF 6.0 и новее.
- Около 100 КБ внутренней RAM под контроллер Bluetooth и Bluedroid, пока библиотека инициализирована.
a2dp_xq_deinit()возвращает эту память.
Тесты на компьютере
Части на чистом C собираются GCC, Clang или zig cc в режиме C17:
cmake -S test -B build/test
cmake --build build/test
ctest --test-dir build/test --output-on-failure
На Windows без компилятора C выполните python -m pip install ziglang cmake ninja и добавьте -G Ninja -DCMAKE_TOOLCHAIN_FILE=<абсолютный путь>/cmake/zig-toolchain.cmake. В CI тесты идут на Linux с AddressSanitizer и UndefinedBehaviorSanitizer.
Лицензия
Apache License 2.0, текст в LICENSE. Библиотека работает поверх стека Bluedroid из ESP-IDF (Apache-2.0) и не содержит стороннего кода.
Код взят из музыкального плеера на ESP32, который я сейчас делаю. Плеер я опубликую после его собственных проверок на железе. Большую часть кода написал Claude (Anthropic) по моим заданиям, я ставил задачи и проверял результат тестами и сборками из раздела «Состояние».