На моем шкафу несколько лет пылились две замечательные пятидесяти ватные колонки Yamaha, которым я не мог найти применение. А в ящике стола лежала плата на базе модуля ESP32 с такой же проблемой. Я решил немного скрасить свой досуг и собрать своими руками Bluetooth‑проигрыватель для этих колонок. Конечно можно было купить готовый, это было бы экономически целесообразней да и быстрее, но уже не так интересно.
Забегая вперед сразу покажу что получилось:
Играет громко для одной комнаты, несмотря на маломощный усилитель (об этом далее). Можно регулировать уровень звука и ставить на паузу с помощью ручки энкодера. На дисплее отображается визуализация частотного спектра.
Bluetooth подключается автоматически при включении проигрывателя. Управление громкостью и воспроизведением с телефона также работает.
Далее расскажу все этапы производства этого устройства своими руками.
Компоненты
Чтобы приступить к работе, мне потребовалось добыть следующие запчасти. Часть из них у меня уже была, остальное заказал на популярном маркетплейсе.
Электронные компоненты:
-
Модуль ESP32 WROOM-32
-
Модуль ЦАП (DAC) GY‑PCM5102 с интерфейсом I2S
-
Стерео усилитель звука XH‑A232
-
Модуль поворотного энкодера HW-040 (KY-040)
-
OLED дисплей 0.96″ 128×64, I2C
-
Печатная макетная плата
-
Два керамических конденсатора 0.1мкФ (маркировка 104)
-
Два резистора 100Ом
-
Два резистора 680Ом
Питание и выводы:
-
Блок питания 12V для светодиодной ленты и светильников, 5А
-
Понижающий DC‑DC преобразователь XL4015E
-
Разъем сетевой С8 на корпус с выключателем
-
Кабель питания C8
-
Колодка акустическая
Корпус:
-
Катушка PLA‑пластика
-
Крепежные втулки M2
-
Крепежные втулки M3
-
Винты M2
-
Винты M3
-
Прозрачный ПЭТ‑пластик для экрана
-
Металлическая сетка
Сборка электроники
Компоненты я спаял по схеме:
В живую первый варинт сборки выглядел так:
Центральным компонентом устройства выступает модуль ESP32 WROOM-32. В нём уже есть всё необходимое для Bluetooth‑подключения и управления периферией. К модулю подключён ЦАП (DAC) GY‑PCM5102. На него приходит сигнал по интерфейсу I2S (не путать с I2C), а выходит аналоговый звуковой сигнал на усилитель.
Между ESP32 и ЦАП можно обнаружить делитель напряжения. Он нужен для того, чтобы понизить среднеквадратичное значение напряжения, выходящего с ЦАП, с 2,1vrms до 0,27vrms. Если не понижать, то усилитель начнёт захлёбываться и издавать ужасные шумы. Коэффициент делителя подобран опытным путём. В вашем случае он может быть другим даже с такой же моделью усилителя (поскольку дешёвые железки не отличаются точностью и надёжностью).
Кстати, об усилителе XH‑A232. Я его взял просто потому, что он у меня уже был. Его пиковая мощность — 30 ватт на канал, в то время как номинальная мощность колонок — 50 ватт. Но для моих целей он подошёл, громкость звука меня устраивает, поэтому я оставил его.
Подключение энкодера и OLED‑экрана не должно вызвать вопросов — оно стандартное, как написано в сотнях мануалов к ним.
Единственное пояснение: стоит брать модуль энкодера, где уже распаяны резисторы для подтяжки контактов и конденсаторы для устранения дребезга. Можно, конечно, припаять это всё самому или даже справиться с проблемой дребезга программно, но нужно ли это делать — решать только вам.
А вот если вы заметили на схеме проблему с питанием, то вы молодец. Если питать ЦАП и управляющую плату одним источником питания внутри одной цепи, то прослушивание музыки будет сопровождаться цифровыми шумами. В идеальном случае нужно питать ЦАП и ESP32 независимо, но, к сожалению, мы в реальном мире, и об этой проблеме я подумал уже тогда, когда обнаружил её акустически. Пришлось искать обходной путь решения проблемы.
Вся схема питается от блока на 12 вольт. Усилитель подключён к нему напрямую, а ESP32 — через понижающий преобразователь. ESP32 питается от 3,3 вольт. Если подать напряжение больше на соответствующий порт питания, модуль сгорит. Для ЦАП требование к входному напряжению не так критично: он работает в диапазоне 3,3–5 вольт. Изначально я настроил преобразователь на требуемые 3,3 вольта и подал питание и на ЦАП, и на ESP32 одновременно, запустил музыку и услышал шум.
На плате WROOM также есть встроенный понижающий преобразователь напряжения. Можно подать на его вход напряжение значительно выше 3,3 вольт — он понизит до нужного уровня сам. Я подал 3,3 вольта туда — и ничего не заработало. Повысил до 3,65 — плата запустилась. И поскольку теперь ЦАП питается от одного понижающего преобразователя, а ESP32 — фактически от другого, шумы исчезли.
Вопреки расхожему мнению, что на GPIO‑порты ESP32 нельзя подавать больше 3,3 вольт, скажу: в даташите об этом явно нигде не написано. Сказано, что нельзя подавать питание на сам модуль выше 3,3 вольт. GPIO нормально работают и в пятивольтовом режиме, как и остальные компоненты системы. Однако я решил ограничиться минимально необходимым, поэтому вся цепь у меня работает на напряжении 3,65 вольт (кроме усилителя, там 12 вольт).
В процессе сборки важно не забыть запаять перемычки на плате ЦАП. Они нужны, чтобы устройство работало в нужном нам режиме.
Между ЦАП и усилителем провода должны быть короткими и их нужно скрутить, это тоже влияет на шумы.
Ну и напоследок: я поставил два керамических конденсатора по 0,1 мкФ после ЦАП — опять же из‑за шумов. Это я сделал скорее уже для собственного спокойствия, по совету из интернета. По факту шумов на этом этапе я уже не слышал ухом; возможно, их можно было бы увидеть осциллографом, но для моей задачи это избыточно.
В остальном сборка не должна вызывать вопросов.
Прошивка
Весь код проекта я залил на GitHub: https://github.com/IvanZhuck/esp32-iz‑music
Решать низкоуровневые задачи по передаче звука по Bluetooth не придётся — за нас уже разработали протокол A2DP и реализовали его в качестве библиотеки для Arduino: https://github.com/pschatzmann/ESP32-A2DP
Остаётся только правильно ей воспользоваться — и всё будет готово. Я не буду расписывать весь код целиком, остановлюсь лишь на основных моментах.
В качестве фреймворка для разработки я использовал PlatformIO.
Содержимое файла конфигурации (platformio.ini):
[env:esp32dev]platform = espressif32board = esp32devframework = arduinomonitor_speed = 115200lib_deps = adafruit/Adafruit SSD1306@^2.5.16https://github.com/pschatzmann/ESP32-A2DPhttps://github.com/pschatzmann/arduino-audio-tools.gitmaffooclock/ESP32RotaryEncoder@^1.2.0board_build.partitions = huge_app.csvmonitor_filters = esp32_exception_decoder
Как видно из зависимостей, используются следующие библиотеки:
-
pschatzmann/arduino‑audio‑tools — библиотека для работы с аудио, нужна в том числе для управления ЦАП;
-
pschatzmann/ESP32-A2DP — реализация A2DP, как сказано выше. По сути, это дополнение к arduino‑audio‑tools от того же автора;
-
adafruit/Adafruit — нужна для работы с OLED‑экраном;
-
maffooclock/ESP32RotaryEncoder — библиотека для взаимодействия с энкодером.
Чтобы устройство минимально работало, нам нужно инициализировать A2DP:
BluetoothA2DPSink a2dpSink;...void setup() { ... a2dpSink.set_auto_reconnect(true, 1000); a2dpSink.set_stream_reader(a2dpDataCallback, false); a2dpSink.set_raw_stream_reader(a2dpRawDataCallback); a2dpSink.set_on_connection_state_changed(a2dpConnectionStateChanged); a2dpSink.set_on_audio_state_changed(a2dpAudioStateChanged); a2dpSink.start("IZMusic"); ...}
Стоит обратить внимание на функцию обратного вызова a2dpDataCallback. Она вызывается, каждый раз, когда по Bluetooth прилетает новая порция аудио‑данных:
constexpr int MIXER_BUFFER_MAX_SIZE = 5000;SineWaveGenerator<int16_t> sineWave(500);GeneratedSoundStream<int16_t> generatedSineSound(sineWave);uint8_t mixerBuffer[MIXER_BUFFER_MAX_SIZE];... void a2dpDataCallback(const uint8_t* data, uint32_t length) { generatedSineSound.readBytes(mixerBuffer, length); outputMixer.write(mixerBuffer, length); outputMixer.write(data, length);}... void setup() { ... sineWave.begin(sineWaveConfig, 3.0f); ...}
Из текста функции можно понять, что она зачем‑то подмешивает сгенерированную тут же низкочастотную волну в 3 герца в outputMixer.
outputMixer — это поток звуковых данных, который дальше отправится в ЦАП.
Наша модель модуля ЦАП обладает неприятной особенностью: она уходит в энергосберегающий режим, когда через неё проходит тишина. Переход в этот режим сопровождается негромким щелчком в колонках. Такие щелчки слышны и между треками, и в паузах между словами, например если слушать подкаст. Чтобы модуль не уходил в сон, я генерирую волну низкой частоты, которую физически не могут воспроизвести колонки, но которая постоянно подмешивается в поток вывода.
И раз уж мы увидели outputMixer, давайте посмотрим, из чего он состоит:
I2SStream i2sStream;VolumeStream volumeStream(i2sStream);Equalizer3Bands equalizerStream(volumeStream);...OutputMixer<int16_t> outputMixer(equalizerStream, 2);void setup() { auto i2sConfig = i2sStream.defaultConfig(); i2sConfig.sample_rate = 44100; i2sConfig.buffer_count = 6; i2sConfig.buffer_size = 512; i2sConfig.pin_bck = I2S_PIN_BCK; i2sConfig.pin_ws = I2S_PIN_WS; i2sConfig.pin_data = I2S_PIN_DATA; i2sStream.begin(i2sConfig); auto equalizerConfig = equalizerStream.defaultConfig(); equalizerConfig.copyFrom(i2sConfig); equalizerConfig.gain_low = 1.8; equalizerConfig.gain_medium = 0.7; equalizerConfig.gain_high = 0.5; equalizerStream.begin(equalizerConfig); auto volumeConfig = volumeStream.defaultConfig(); volumeConfig.copyFrom(i2sConfig); volumeStream.begin(volumeConfig); outputMixer.begin(MIXER_BUFFER_MAX_SIZE);}
Цифровое аудио, полученное по Bluetooth, проходит ряд модификаций, прежде чем попасть в ЦАП. Данные передаются по цепочке:
Bluetooth → outputMixer → equalizerStream → volumeStream → i2sStream
Из названий можно догадаться, что equalizerStream — это эквалайзер, а volumeStream — это уровень громкости.
Эквалайзер мы настраиваем под конкретный усилитель. Я поднял низкие частоты и убрал верхние, чтобы компенсировать писклявость моего усилителя и получить более глубокий бас.
Громкостью volumeStream мы будем управлять с помощью энкодера. Класс управления громкостью находится в lib/SoundVolume/src.
Класс очень простой, не думаю, что требует пояснений.
В финале звук попадает в i2sStream — этот поток уже отправляется в ЦАП, где превращается в аналоговый звук.
На этом работа со звуком окончена. Из оставшихся частей интерес представляет, пожалуй, только визуализатор.
Как видно из кода выше, a2dpSink дёргает ещё одну функцию обратного вызова в процессе работы:
AudioRealFFT fft;...void a2dpRawDataCallback(const uint8_t* data, uint32_t length) { fft.write(data, length);}...void setup() { ... a2dpSink.set_raw_stream_reader(a2dpRawDataCallback); ...}
В эту функцию также попадают данные звука с Bluetooth, но к ним не применяется уровень громкости, выставленный на источнике звука (на смартфоне), так как по задумке визуализация отображается всегда одинаково, независимо от громкости.
Класс AudioRealFFT отвечает за быстрое преобразование Фурье — алгоритм, необходимый для того, чтобы разделить звуковую волну на спектр частот. Очень доступно принцип работы этого алгоритма рассматривается в этом ролике: https://www.youtube.com/watch?v=9bqrTYCS6DQ
Спектр частот — это буквально те колонки визуализатора, что нам нужны. Единственное, что частота разбиения выше, чем нужно: 512 диапазонов частот на оба канала (у нас стереозвук), или 256 на каждый. Нам потребуется получить из этого 16 колонок. И, казалось бы, всё просто: делим 256 на 16 и рисуем 16 колонок.
Но проблема заключается в том, что мы слышим звук в диапазоне 20–22000 Гц, и частота дискретизации каждого канала по этой же причине — 22 000 Гц, но наиболее интересующие нас частоты находятся в диапазоне 100–5000 Гц. Именно в этом диапазоне играет большинство музыкальных инструментов и находится человеческий голос, следовательно, при визуализации стоит уделить этим частотам (колонкам) больше внимания, чем прочим.
Я не дружу с математикой и логарифмическими функциями, поэтому просто прикинул на калькуляторе, какой диапазон спектра частот какой колонке визуализатора соответствует:
/// Разделение спектра частот на 16 полос для далтнейшей визуализацииuint8_t m_visualizerDataRanges[DISPLAY_VISUALIZER_COLS][2] = { {0,0}, //до 86,13 Гц {0,1}, //до 172,27 Гц {1,2}, //до 258,4 Гц {2,3}, //до 344,53 Гц {3,5}, //до 516,8 Гц {5,7}, //до 689,06 Гц {7,9}, //до 861,33 Гц {9,11}, //до 1033,59 Гц {11,17}, //до 1550,39 Гц {17,23}, //до 2067,19 Гц {23,31}, //до 2756,25 Гц {31,45}, //до 3962,1 Гц {45,64}, //до 5598,63 Гц {64,88}, //до 7665,82 Гц {88,120}, //до 10422,07 Гц {120,255} //до 22050 Гц};
Исходя из этого, написал функции для рассчета высот колонок визуализатора:
void Display::fillVisualizerDataRawBuffer() { float* magnitudes = m_fft.magnitudes(); uint8_t percent = 0; for (int i = 0; i < m_fft.config().length / 2; i++) { /** * Значения амплитуд изменяются от 0 до 3-6 в зависимости от громкости звука. * Но бывают выпады и выше. Коэффециент 0.05 подобран "на глаз". Чтобы примерно загнать * амплитуду в процентный диапазон от 0 до 100. */ percent = std::min<uint8_t>(static_cast<uint8_t>(magnitudes[i] / 0.05), 100); m_visualizerDataRawBuffer[i] = static_cast<uint8_t>(DISPLAY_VISUALIZER_ROWS * 0.01 * percent); }}void Display::computeVisualizerData() { for (uint8_t i = 0; i < DISPLAY_VISUALIZER_COLS; i++) { unsigned int totalValue = 0; uint8_t length = 0; for (uint16_t j = m_visualizerDataRanges[i][0]; j < m_visualizerDataRanges[i][1] + 1; j++) { if (m_visualizerDataRawBuffer[j] < 10) { continue; } totalValue += m_visualizerDataRawBuffer[j]; length++; } length = std::max<uint8_t>(length, 1); /* * Для визуализации используется среднее от суммы текущего значения и предыдущего. * Это нужно для плавной анимации колонок визуализатора. */ m_visualizerData[i] = static_cast<uint8_t>((totalValue / length + m_visualizerData[i]) / 2); }}
Этот код можно найти в lib/Display/src. Важные моменты уже прокомментированы.
Думаю, что больше про прошивку писать не имеет смысла — остальные вещи в исходниках либо полностью соответствуют примерам из документации, либо их принцип работы и назначение очевидны.
Корпус
Хочется, чтобы устройство не выглядело как груда проводов, прибитых к макетной плате.Я спроектировал корпус в программе «КОМПАС-3D» и распечатал все его части на 3D‑принтере из PLA‑пластика.
Сборка
Когда все детали были у меня на руках, а работа устройства отлажена, я приступил к сборке. Сначала впаял втулки в места, куда будут вкручиваться крепежные винты:
Затем установил все компоненты внутрь корпуса:
Вклеил кусок прозрачного ПЭТ‑пластика, он выполняет роль защитного экрана:
Установил OLED‑дисплей и проклеил его черной изолентой по контуру, чтобы в зазоры между ним и корпусом проигрывателя, не попадал свет и конструкция выглядела монолитной снаружи:
Приклеил решетку с помощью термоклея:
Подключил шлейфы энкодера/дисплея и закрыл крышку:
Устройство готово:
В заключении
Проигрыватель стоит у меня в мастерской и радует меня звуком. На весь проект у меня ушло около трёх недель в формате «по вечерам и выходным». Этот проект я делал в образовательных целях и не планирую его развивать. Не уверен, что лицензия домашней версии «КОМПАС-3D» предусматривает публикацию моделей в общий доступ, поэтому их здесь нет. Но если они вам нужны, то со мной можно связаться по контактам из профиля.
Буду рад комментариям, отзывам и предложениям. Спасибо, что прочитали статью.
ссылка на оригинал статьи https://habr.com/ru/articles/1064228/