
Привет, Хабр!
В прошлой статье я делился опытом создания портативной мини-акустики с передачей аудио по Wi-Fi вместо Bluetooth. В этой — представляю её более мощную версию. Мы напечатаем корпус, усовершенствуем скрипты, разработаем фирменное приложение для Hi-Fi трансляции звука и добавим эквалайзер в систему.
❯ Вместо начала
«Поигравшись» со своим предыдущим проектом, я был настолько воодушевлен, что мне захотелось сделать что-то большее как в аппаратном плане, так и в программном. В итоге было решено собрать более мощную портативную Wi-Fi акустику с интересным дизайном и отличными характеристиками. Для передачи аудио мы будем использовать те же стандарты (технологии), что были описаны в предыдущей статье, но с некоторыми дополнениями. В результате обдумывания будущего DIY изделия, у меня сформировалось следующее подобие технического задания:
Реализация акустики со следующими параметрами:
-
Мощность акустической системы: 50 Вт на канал;
-
Тип акустического оформления: пассивный излучатель;
-
Диапазон воспроизводимых частот (минимум): 35-18 000 Гц;
-
Тип применяемых динамических головок: широкополосные, 100 мм;
-
Физические элементы управления: копка со светодиодным индикатором без фиксации;
-
Питание: питание акустики выполнить с помощью встроенного аккумулятора 21 В (Li-ion 5S), зарядку и питание обеспечить с помощью разъема USB Type-C с технологией Power Delivery (PD) 120W 20V;
-
Клиентское программное обеспечение: для трансляции аудио разработать мобильное приложение с элементами управления параметрами акустической системы.
Что касается физических элементов управления на акустической системе — я сторонник минимализма и искренне не понимаю, зачем в наш современный цифровой век производители портативной акустики зачастую превращают панель в пульт управления атомной станции, добавляя различные крутилки и кнопочки. Ведь всё управление можно реализовать в мобильном приложении, ведь более рационально выполнять регулировку из точки прослушивания, чем каждый раз подходя к устройству.
❯ Корпус акустики
Корпус акустики проектировался с учетом своего предыдущего (студенческого) опыта проектирования и сборки акустических систем (и усилителей). Как обычно, разработка выполнялась в свободной САПР FreeCAD. Ниже представлены скриншоты разработанной модели.
По существу, корпус акустики состоит из двух блоков, скреплённых перемычками, в пространстве между которыми реализована система для размещения электроники. Дизайн корпуса выбран не случайно — это моя дань прошлому. При проектировании я вдохновлялся магнитофоном «Романтик М-309 С», с которым провёл немало времени в детстве.
С тыльной стороны размещены пассивные излучатели, которые обеспечивают дополнительную излучающую поверхность, что обеспечивает отличное качество воспроизведения низких частот. Также спроектированы боковые панели с необходимыми элементами для размещения дисплея и разъема. Для улучшения стереобазы, корпус спроектирован так, чтобы динамические головки были повернуты в стороны от центра на небольшой угол.
❯ 3D печать
Я часто слышу утверждение, что пластик — худший материал для изготовления акустики. В некоторых случаях это верно, но, как сказал бы Альф: «Вы просто не умеете их готовить». Если изготавливать корпус из пластика методом литья, то да, акустические свойства корпуса могут быть хуже, чем у деревянных. Но тут нам на помощь приходит 3D-принтер, который позволяет менять рисунок и плотность заполнения стенок корпуса — что недостижимо при литье. Работая с 3D-печатью, я давно заметил, что изменение плотности заполнения влияет на акустические свойства. Для печати моделей я применяю HIPS пластик — это идеальный вариант для печати корпусов акустических систем, благодаря своим свойствам. Ниже представлен скриншот слайсера с отображением модели корпуса. Степень заполнения стенки 65%, рисунок «сетка» и толщина слоя 0,4 мм.
Несмотря на то, что HIPS пластик менее подвержен расслоению при печати, я рекомендую выполнять печать в закрытой термокамере и после завершения печати дождаться полного остывания, прежде чем доставать модель из принтера. Я из-за своей нетерпеливости получил пару небольших трещин, которые вы можете наблюдать далее на готовом корпусе, но, к счастью, данные дефекты легко исправляются с помощью паяльника.
❯ Электроника
В конструкции нашей портативной акустики применяются относительно недорогие компоненты, иначе всякий смысл в DIY пропадает.
▨ Вычислительная платформа
«Мозг» устройства переезжает из предыдущего проекта (одноплатник Mango Pi MQ-Quad) — он выполняет роль вычислительной системы. В качестве источника аналогового аудио сигнала будет использоваться встроенный ЦАП, его характеристики меня полностью устраивают.
▨ Усилитель мощности
В качестве усилителя я выбрал компактный модуль XH-M562 на базе чипа TPA3116d2. Эта микросхема представляет собой высококачественный УМЗЧ класса D с двумя каналами по 50 Вт и поддержкой работы на несущей частоте 1,2 МГц.
Этот модуль был заказан на всем известном оранжевом маркетплейсе. После двух недель ожидания, модуль наконец-то у меня. Первое включение и тест усилителя меня очень разочаровал, качество звука было ужасное: практически полностью отсутствовали низкие частоты и выходная мощность составляла не более 20 Вт на канал, при питании 21 В. Китайские инженеры опять что-то намудрили, поэтому пришлось воспользоваться даташитом, чтобы довести усилитель до адекватных параметров. Изучая документацию на микросхему, я обратил внимание на следующий таблицы:
Сравнив данные параметры с номиналами установленных компонентов, были обнаружены следующие причины плохого качества звучания:
-
Входные резисторы R5 и R6 имеют номинал 1 кОм, что вносит значительные искажения в входной сигнал.
-
Входные конденсаторы C10, C9, C7, C6 имеют номинал 1 мкФ, хотя в датащите четко написано: «Если требуется ровный басовый отклик вплоть до 20 Гц, рекомендуемая частота среза составляет одну десятую от этого, 2 Гц.
В таблице 2 перечислены рекомендуемые конденсаторы связи по переменному току для каждого шага усиления.» -
Резисторы R3 и R2, которые отвечают за настройку уровня усиления, имеют номиналы 20 кОм и 100 кОм — что устанавливает режим усиления на 26 dB, но при этом, номинал входных сопротивление должен соответствовать 30 кОм, вместо установленных 1 кОм.
Решение проблемы:
Входные конденсаторы C10, C9, C7 и C6 были заменены на 10 мкФ. Согласно таблице 2 даташита, входные резисторы R5 и R6 заменены на 9 кОм. Для установки коэффициента усиления 36 дБ резисторы R3 и R2 были заменены на 47 кОм и 75 кОм соответственно.
После проделанной операции, усилитель заиграл новыми красками! Теперь я доволен качеством звучания. Видимо китайские инженеры сознательно исказили параметры усилителя, чтобы он мог выжить в неумелых руках, так как он поставляется без радиатора и конструкция платы затрудняет его установку из-за торчащих конденсаторов (С4, С5, С3, С2, С16, С17, С18, С19). В нашем режиме установка радиатора обязательна, так как при воспроизведении низких частот микросхема имеет неплохой нагрев. Для установки радиатора необходимо демонтировать конденсаторы С4, С5, С3, С2, С16, С17, С18, С19 и заменить их на электролитические буферные ёмкости с номиналом 2200 мкФ 25 В, к счастью, на плате уже для них предусмотрены контактные площадки.
▨ Система питания
Чтобы раскрыть весь потенциал усилителя, было принято решение использовать уровень питающего напряжения 21 В. Данный уровень позволяет реализовать питание акустики как от зарядного устройства с PD 120W, так и от встроенного аккумулятора.
А так как напряжение питания нашей Mango Pi MQ-Quad составляет 5 В, то необходимо реализовать и понижающий преобразователь до указанного уровня. Учитывая данные потребности, был разработан модуль управления питанием, схема которого показана ниже:
Понижение уровня напряжения для питания одноплатника реализовано на популярной микросхеме LM2595-ADJ, где выходное напряжение задается с помощью делителя R1 и R2. Управление функции включения и выключения нашей акустики также завязано на данной микросхеме, разрешающий сигнал формируется транзистором Q1. Основной сигнал включения формируется кнопкой с последующим подхватом с помощью сигнала от SBC (Mango Pi MQ-Quad). На плате реализован вход для подключения внешнего источника, который служит для зарядки встроенного аккумулятора или питания акустики. Проверка нужного уровня входящего напряжения выполняется с помощью стабилитрона D4 с напряжением пробоя 18 В. Переключение на нужный уровень напряжения при питании от USB Type-C выполняется с помощью триггера.
Плата модуля разрабатывалась в KiCad, ниже представлены некоторые изображения из проекта
Далее плата была вытравлена и собрана, ниже размещено изображение с некоторыми этапами:
Как всегда, платы изготавливались с помощью моего небольшого лазерного станка.
Что касается встроенной аккумуляторной батареи, то здесь все по классике: я использовал б/у аккумуляторы от ноутбука, которые у меня давно валялись без дела. Батарея собрана по схеме 5S с применением платы BMS.
Для контроля уровня заряда, я собрал простой индикатор на базе компаратора LM324, ниже приведена принципиальная схема индикатора:
И чтобы стало яснее, как реализована система управления питанием, ниже приведена схема подключения управляющих цепей:
Для наглядности осталось показать только схему подключения цепей питания:
Как вы можете заметить, в схеме используются синфазные дроссели L1 и L2, которые выполняют роль фильтра для подавления шума, возникающего в процессе работы платы Mango Pi MQ-Quad. Используемый усилитель очень чувствителен к шуму в питающей цепи, поэтому установка данных фильтров обязательна. Дроссели были взяты из б/у блока питания компьютера, индуктивность не замерял, поэтому и не скажу.
❯ Программная часть
В этот раз, в отличии от прошлой серии, я использовал собственную сборку операционной системы Debian 12 со своим кастомным ядром, так как производители платы Mango Pi MQ-Quad не особо заморачиваются с программной поддержкой своих плат, а единственный «живой» образ на сайте производителя оставляет желать лучшего. Также, в отличии от прошлой версии, я не стал применять дополнительный пакеты для управления GPIO, а использовал API операционной системы. Ну, что ж, приступим.
▨ Конфигурация звуковой подсистемы
Для начала нам нужно сконфигурировать звуковую подсистему. Для начала необходимо посмотреть какие звуковые устройства нам доступны, выполнив команду:
aplay -l
В результате выполнения команды, мы увидим что-то подобное:
**** List of PLAYBACK Hardware Devices **** card 0: Codec [H616 Audio Codec], device 0: CDC PCM Codec-0 [CDC PCM Codec-0] Subdevices: 0/1 Subdevice #0: subdevice #0 card 1: sndahub [sndahub], device 0: Media Stream sunxi-ahub-aif1-0 [Media Stream sunxi-ahub-aif1-0] Subdevices: 1/1 Subdevice #0: subdevice #0 card 1: sndahub [sndahub], device 1: System Stream sunxi-ahub-aif2-1 [System Stream sunxi-ahub-aif2-1] Subdevices: 1/1 Subdevice #0: subdevice #0 card 1: sndahub [sndahub], device 2: Accompany Stream sunxi-ahub-aif2-2 [Accompany Stream sunxi-ahub-aif2-2] Subdevices: 1/1 Subdevice #0: subdevice #0 card 2: allwinnerhdmi [allwinner-hdmi], device 0: hdmi i2s-hifi-0 [] Subdevices: 1/1 Subdevice #0: subdevice #0 card 3: hificyberexsoun [hifi-cyberex-sound], device 0: sunxi-ahub-cpu-aif0-pcm5102a-hifi pcm5102a-hifi-0 [] Subdevices: 1/1 Subdevice #0: subdevice #0
В нашем случае, мы будем использовать встроенный ЦАП, который определяется как Codec [H616 Audio Codec] и имеет адрес устройства card 0. Запоминаем адрес карты и идем дальше.
Нет смысла использовать мощную акустику без подстройки АЧХ, поэтому для этих целей мы будем применять десятиполосный параметрический эквалайзер. Чтобы реализовать эту функцию в нашей аудиоподсистеме, необходимо установить дополнительные плагины с помощью команды:
sudo apt update sudo apt install libasound2-plugin-equal alsa-tools-gui
После успешной установки, после выполнения команды:
alsamixer -D equal
вы увидите что-то подобное:
Не обращайте внимание на уровни, в вашем случае все ползунки будут выставлены на уровень 50, скриншот делал со своей рабочей системы.
Также перед дальнейшим использованием, нам необходимо настроить наше основное аудио устройство с помощью команды:
alsamixer
И сконфигурировать, как показано на скриншоте:
Далее добавим наш эквалайзер в основную конфигурацию аудиоподсистемы:
sudo nano /etc/asound.conf
Добавив следующее содержимое:
ctl.equal { type equal } pcm.plugequal { type equal slave.pcm "plug:dmixer" } pcm.equal { type plug slave.pcm plugequal } pcm.dmixer { type dmix ipc_key 1024 slave { pcm "hw:0" period_time 0 period_size 1920 buffer_size 19200 rate 48000 format S32_LE } } pcm.!default { type plug slave.pcm "equal" } ctl.!default { type hw card 0 }
Теперь мы сможем перенаправлять аудиопоток через эквалайзер.
▨ Установка рендерера
Как и в прошлой статье, для приема аудиопотока мы воспользуется DLNA рендерером Gmrender-Resurrect. Ниже представлены шаги по установке.
Установка дополнительных зависимостей, необходимых для компиляции:
sudo apt-get install build-essential autoconf automake libtool pkg-config
Установка дополнительных библиотек, которые использует рендерер для своей работы:
sudo apt-get update sudo apt-get install libupnp-dev libgstreamer1.0-dev \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ gstreamer1.0-libav
Дополнительные зависимости для работы с нашей аудио подсистемы:
sudo apt-get install gstreamer1.0-alsa
Клонируем репозиторий:
git clone https://github.com/hzeller/gmrender-resurrect.git
Переходим в папку репозитория, выполняем конфигурацию и сборку пакета:
cd gmrender-resurrect ./autogen.sh ./configure make
Установка:
sudo make install
Теперь нам нужно создать сервис для автозапуска рендерера в нашей системе:
sudo nano /etc/systemd/system/gmediarender.service
И добавим следующее содержимое:
[Unit] Description=DLNA Renderer After=network.target After=sound.target [Service] User=root Group=root ExecStartPre=/bin/sleep 30 ExecStart=/usr/local/bin/gmediarender --gstout-audiosink=alsasink --gstout-audiodevice=equal --friendly-name "CYBEREX SOUND" interface=wlan0 Nice=-20 Restart=on-failure [Install] WantedBy=multi-user.target
--gstout-audiosink=alsasink --gstout-audiodevice=equal — данные аргументы устанавливают в качестве устройства воспроизведения наш эквалайзер.
Сохраняем файл и добавляем в автозагрузку:
systemctl enable gmediarender.service
▨ Алгоритм включения
Как вы могли видеть ранее на схеме подключения управляющих цепей, на модуль управления питанием приходят два сигнала ON SIGN BUTTON и ON CONFIRM SBC — именно они отвечают за запуск системы и подачу питания. Первый сигнал приходит с кнопки включения, а второй фиксирует запуск и формируется GPIO одноплатником Mango Pi MQ-Quad (в случае корректного запуска системы). Ниже представлен код Python скрипта, который формирует разрешающий сигнал:
Скрипт формирования разрешающего сигнала [hello_display.py]
from luma.core.interface.serial import i2c from luma.core.render import canvas from luma.oled.device import ssd1306 from PIL import ImageFont import time # Настройка I2C интерфейса serial = i2c(port=0, address=0x3C) # 0x3C - адрес для SSD1306 device = ssd1306(serial, width=64, height=48) # Загрузка шрифта с поддержкой кириллицы font_path = "fonts/UbuntuMono-R.ttf" # Шрифт font = ImageFont.truetype(font_path, 31, encoding='UTF-8') # размер шрифта font_b = ImageFont.truetype(font_path, 16, encoding='UTF-8') # размер шрифта GPIO_PIN_POWER = 234 # PH10 # Экспорт GPIO для PH10 (выход) with open("/sys/class/gpio/export", "w") as f: f.write(str(GPIO_PIN_POWER)) # Настройка пина как выход with open(f"/sys/class/gpio/gpio{GPIO_PIN_POWER}/direction", "w") as f: f.write("out") # Подаем разрешение на питание with open(f"/sys/class/gpio/gpio{GPIO_PIN_POWER}/value", "w") as f: f.write("1") # Текст и начальная позиция бегущей строки title_d = "" artist_d = "ЗАГРУЗКА..." album_d = "" track_time = "" def display_print(): start_pix_t = 64 start_pix_ar = 1 start_pix_al = 64 while True: if True: title_width = len(title_d) * 6 # Ожидаемая ширина текста (по 6 пикселей на символ) artist_width = len(artist_d) * 6 album_width = len(album_d) * 6 max_width = max(title_width, artist_width, album_width) with canvas(device) as draw: #draw.text((start_pix_t, 1), title_d, fill="white", font=font) draw.text((start_pix_ar, 4), artist_d, fill="white", font=font) #draw.text((start_pix_al, 24), album_d, fill="white", font=font) #draw.text((15, 36), track_time, fill="white") # Сдвиг текста влево if title_width > 64: start_pix_t -= 1 else: start_pix_t = 1 if artist_width > 64: start_pix_ar -= 1 else: start_pix_ar = 1 if album_width > 64: start_pix_al -= 1 else: start_pix_al = 1 # Если текст полностью вышел за экран, вернем его в начальную позицию if start_pix_t < -max_width: start_pix_t = 64 if start_pix_ar < -max_width: start_pix_ar = 64 if start_pix_al < -max_width: start_pix_al = 64 time.sleep(0.05) display_print()
Данный скрипт также отображает на дисплее бегущую строку с надписью «ЗАГРУЗКА», появление которой сигнализирует о том, что кнопку можно отпустить.
Для активации скрипта при запуске системы, необходимо создать сервис:
sudo nano /etc/systemd/system/hello_display.service
Со следующим содержанием:
[Unit] Description=OLED Logon Display Service #After=power_on.service [Service] User=root Group=root ExecStart=/root/myvenv/bin/python3 /home/scripts/hello_display.py WorkingDirectory=/home/scripts Environment="PATH=/root/myvenv/bin:/usr/bin:/bin" StandardOutput=inherit StandardError=inherit [Install] WantedBy=multi-user.target
Скрипт работает с виртуальным окружением, как его активировать смотрите в предыдущей статье, там же найдете информацию касательно дисплея. И для активации автозагрузки, выполним следующую команду:
systemctl enable hello_display.service
▨ Дисплей и функция выключения
Как и раннее, качестве дисплея используется OLED модуль SSD1306 с разрешением 64 х 48, а вся логику управления дисплеем реализована в небольшом Python скрипте с дополнением функции выключения системы по нажатию кнопки питания:
Код скрипта [media_info_disp.py]
from luma.core.interface.serial import i2c from luma.core.render import canvas from luma.oled.device import ssd1306 from PIL import ImageFont from threading import Thread import time import datetime import math import requests from xml.etree import ElementTree as ET import netifaces import subprocess # Настройка I2C интерфейса serial = i2c(port=0, address=0x3C) # 0x3C - адрес для SSD1306 device = ssd1306(serial, width=64, height=48) # Загрузка шрифта с поддержкой кириллицы font_path = "fonts/UbuntuMono-R.ttf" # Шрифт font = ImageFont.truetype(font_path, 12, encoding='UTF-8') # размер шрифта font_b = ImageFont.truetype(font_path, 16, encoding='UTF-8') # размер шрифта font_b2 = ImageFont.truetype(font_path, 36, encoding='UTF-8') # размер шрифта # Указываем номер GPIO GPIO_PIN_AMP = 272 # PI16 включение усилителя GPIO_PIN_BUTTON = 271 # PI15 мониторинг кнопки # Отмена экспорта GPIO #try: # with open("/sys/class/gpio/unexport", "w") as f: # f.write(str(GPIO_PIN_AMP)) # with open("/sys/class/gpio/unexport", "w") as f: # f.write(str(GPIO_PIN_BUTTON)) #except ValueError: # print(f"Какая-то ошибка.") # Экспорт GPIO with open("/sys/class/gpio/export", "w") as f: f.write(str(GPIO_PIN_AMP)) # Экспорт GPIO для PI15 (вход) with open("/sys/class/gpio/export", "w") as f: f.write(str(GPIO_PIN_BUTTON)) # Настройка пина как выход with open(f"/sys/class/gpio/gpio{GPIO_PIN_AMP}/direction", "w") as f: f.write("out") # Настройка PI15 как вход with open(f"/sys/class/gpio/gpio{GPIO_PIN_BUTTON}/direction", "w") as f: f.write("in") # Интерфейс interface_name = "wlan0" # Получаем информацию об интерфейсе try: addresses = netifaces.ifaddresses(interface_name) if netifaces.AF_INET in addresses: ip_address = addresses[netifaces.AF_INET][0]['addr'] print(f"IP-адрес на интерфейсе {interface_name}: {ip_address}") else: print(f"Интерфейс {interface_name} не имеет IPv4-адреса.") except ValueError: print(f"Интерфейс {interface_name} не найден.") #ip_address = "192.168.1.205" service_url = f"http://{ip_address}:49494/upnp/control/rendertransport1" # Текст и начальная позиция бегущей строки title_d = "" artist_d = "ГОТОВ К ПОДКЛЮЧЕНИЮ" album_d = "" track_time = "" current_time_arh = "" counter_end = 0 en = False power_off = False def set_poff_bool(bools): global power_off power_off = bools def read_poff_bool(): global power_off return power_off def set_bool(bools): global en en = bools # print(en) def read_bool(): global en return en # Делаем часики def draw_clock(draw, now): center_x = 32 center_y = 24 radius = 25 # Делаем рамку с закруглением draw.rectangle(device.bounding_box, outline="black", fill="black") draw.rounded_rectangle(device.bounding_box, radius=8, outline="white", fill="black") # Часовая hour_angle = 2 * math.pi * (now.hour % 12 + now.minute / 60) / 12 hour_x = center_x + int(radius * 0.5 * math.sin(hour_angle)) hour_y = center_y - int(radius * 0.5 * math.cos(hour_angle)) draw.line((center_x, center_y, hour_x, hour_y), fill="white") # Минутная minute_angle = 2 * math.pi * now.minute / 60 minute_x = center_x + int(radius * 0.7 * math.sin(minute_angle)) minute_y = center_y - int(radius * 0.7 * math.cos(minute_angle)) draw.line((center_x, center_y, minute_x, minute_y), fill="white") # Секундная second_angle = 2 * math.pi * now.second / 60 second_x = center_x + int(radius * 0.9 * math.sin(second_angle)) second_y = center_y - int(radius * 0.9 * math.cos(second_angle)) draw.line((center_x, center_y, second_x, second_y), fill="white") # Рисуем круг циферблата # draw.ellipse((center_x - radius, center_y - radius, center_x + radius, center_y + radius), outline="white") # Делаем рамку с закруглением #draw.rounded_rectangle(device.bounding_box, radius=5, outline="white", fill="black") def display_print(): start_pix_t = 64 start_pix_ar = 64 start_pix_al = 64 while True: if read_bool(): title_width = len(title_d) * 6 # Ожидаемая ширина текста (по 6 пикселей на символ) artist_width = len(artist_d) * 6 album_width = len(album_d) * 6 max_width = max(title_width, artist_width, album_width) with canvas(device) as draw: if title_d == "swyh-rs": # draw.text((start_pix_t, 1), title_d, fill="white") draw.text((1, 12), "ПК АУДИО", fill="white", font=font_b) #draw.text((start_pix_al, 24), album_d, fill="white") draw.text((15, 36), track_time, fill="white") else: # Прокрутка текста # draw.rectangle(device.bounding_box, outline="white", fill="black") draw.text((start_pix_t, 1), title_d, fill="white", font=font) draw.text((start_pix_ar, 12), artist_d, fill="white", font=font) draw.text((start_pix_al, 24), album_d, fill="white", font=font) draw.text((15, 36), track_time, fill="white") # Сдвиг текста влево if title_width > 64: start_pix_t -= 1 else: start_pix_t = 1 if artist_width > 64: start_pix_ar -= 1 else: start_pix_ar = 1 if album_width > 64: start_pix_al -= 1 else: start_pix_al = 1 # Если текст полностью вышел за экран, вернем его в начальную позицию if start_pix_t < -max_width: start_pix_t = 64 if start_pix_ar < -max_width: start_pix_ar = 64 if start_pix_al < -max_width: start_pix_al = 64 time.sleep(0.05) # Получение данных с рендеринга # Функция для разбора CurrentURIMetaData def parse_metadata(metadata): global title_d global artist_d global album_d if metadata: # Парсим метаданные как XML root = ET.fromstring(metadata) namespace = {'didl': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/', 'dc': 'http://purl.org/dc/elements/1.1/', 'upnp': 'urn:schemas-upnp-org:metadata-1-0/upnp/'} # Извлекаем информацию о треке title = root.find('.//dc:title', namespace) artist = root.find('.//upnp:artist', namespace) album = root.find('.//upnp:album', namespace) album_art = root.find('.//upnp:albumArtURI', namespace) title_d = title.text if title is not None else "Unknown" artist_d = artist.text if artist is not None else "Unknown" album_d = album.text if album is not None else "Unknown" #print("Track Information:") #print(f" Title: {title.text if title is not None else 'Unknown'}") #print(f" Artist: {artist.text if artist is not None else 'Unknown'}") #print(f" Album: {album.text if album is not None else 'Unknown'}") #print(f" Album Art URI: {album_art.text if album_art is not None else 'None'}") #else: #print("No metadata available.") # Функция для получения временных меток def get_position_info(): global track_time global current_time_arh global title_d global counter_end # Заголовки и тело SOAP-запроса headers = { "Content-Type": 'text/xml; charset="utf-8"', "SOAPAction": '"urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo"', } body = """<?xml version="1.0" encoding="utf-8"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"> <InstanceID>0</InstanceID> </u:GetPositionInfo> </s:Body> </s:Envelope>""" # Отправляем запрос response = requests.post(service_url, headers=headers, data=body) # Разбираем результат if response.status_code == 200: xml_response = ET.fromstring(response.content) rel_time = xml_response.find('.//RelTime').text # Текущее время track_duration = xml_response.find('.//TrackDuration').text # Общая длительность трека track_time = rel_time if current_time_arh == rel_time: if counter_end > 10: set_bool(False) #wiringpi.digitalWrite(PI16, 0) # отклюающий сигнал для усилителя with open(f"/sys/class/gpio/gpio{GPIO_PIN_AMP}/value", "w") as f: f.write("0") counter_end += 1 else: current_time_arh = rel_time set_bool(True) #wiringpi.digitalWrite(PI16, 1) # разрешающий сигнал для усилителя with open(f"/sys/class/gpio/gpio{GPIO_PIN_AMP}/value", "w") as f: f.write("1") counter_end = 0 #print("Playback Position Info:") #print(f" Current Time: {rel_time}") #print(f" Track Duration: {track_duration}") #else: #print(f"Error getting position info: {response.status_code}, {response.text}") def read_data_from_renderer(): # Функция для получения информации о воспроизводимом медиа с рендерера. # Заголовки для GetMediaInfo headers = { "Content-Type": 'text/xml; charset="utf-8"', "SOAPAction": '"urn:schemas-upnp-org:service:AVTransport:1#GetMediaInfo"', } # Тело SOAP-запроса для GetMediaInfo body = """<?xml version="1.0" encoding="utf-8"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:GetMediaInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"> <InstanceID>0</InstanceID> </u:GetMediaInfo> </s:Body> </s:Envelope>""" # Отправляем запрос на GetMediaInfo response = requests.post(service_url, headers=headers, data=body) # Проверяем ответ if response.status_code == 200: # Парсим XML ответа xml_response = ET.fromstring(response.content) # Извлекаем данные о медиа nr_tracks = xml_response.find('.//NrTracks').text current_uri = xml_response.find('.//CurrentURI').text metadata = xml_response.find('.//CurrentURIMetaData').text # print("Basic Media Info:") # print(f" Number of Tracks: {nr_tracks}") # print(f" Current URI: {current_uri}") # Разбираем метаданные parse_metadata(metadata) # Получаем временные метки get_position_info() #else: #print(f"Error: {response.status_code}, {response.text}") # Функиця для периодического вызова def periodic_task(): while True: read_data_from_renderer() time.sleep(1) # Задержка в 1 секунду def periodic_task_clock(): while True: if not read_bool() and not read_poff_bool(): now = datetime.datetime.now() with canvas(device) as draw: draw.rectangle(device.bounding_box, outline="white", fill="black") draw_clock(draw, now) time.sleep(1) def read_power_button_status(): while True: # Чтение состояния PI15 with open(f"/sys/class/gpio/gpio{GPIO_PIN_BUTTON}/value", "r") as f: ph10_state = f.read().strip() if ph10_state == '1': set_poff_bool(True) print(f"Выключение.") with canvas(device) as draw: draw.text((15, 12), "ВЫКЛ", fill="white", font=font_b) # Асинхронное выполнение команды shutdown now subprocess.Popen(["/sbin/shutdown", "now"]) time.sleep(1) # Запуск потока для отображения текста t1 = Thread(target=display_print, name='t1') t1.start() # Запуск переодического опрома медиа t2 = Thread(target=periodic_task, name='t2') t2.start() # Запуск переодического круглые часы t3 = Thread(target=periodic_task_clock, name='t3') t3.start() # Запуск опрос кнопки t4 = Thread(target=read_power_button_status, name='t4') t4.start()
По аналогии, создаем сервис для автозапуск скрипта:
sudo nano /etc/systemd/system/oled_display.service
Добавляем следующее содержимое:
[Unit] Description=OLED Display Service After=network.target After=gmediarender.service [Service] User=root Group=root ExecStartPre=systemctl stop hello_display.service ExecStart=/root/myvenv/bin/python3 /home/scripts/media_info_disp.py WorkingDirectory=/home/scripts Environment="PATH=/root/myvenv/bin:/usr/bin:/bin" StandardOutput=inherit StandardError=inherit Restart=always [Install] WantedBy=multi-user.target
И для активации автозапуска, выполним следующую команду:
systemctl enable oled_display.service
Существует ещё одна особенность: при завершении работы системы пины не сбрасывают свой статус, а нам это критически необходимо для сброса разрешающего сигнала, тем самым отключая питание системы. Для решения этой проблемы создадим новый сервис, который будет запускаться после выполнения команд, завершающих работу системы:
sudo nano /etc/systemd/system/gpio-shutdown.service
И добавим следующее содержание:
[Unit] Description=Reset GPIO pin PH10 on shutdown DefaultDependencies=no Before=shutdown.target reboot.target halt.target [Service] User=root Group=root Type=oneshot ExecStartPre=/bin/sleep 15 ExecStart=/bin/bash -c "echo 0 > /sys/class/gpio/gpio234/value" RemainAfterExit=yes [Install] WantedBy=halt.target reboot.target shutdown.target
И активируем автозапуск скрипта:
systemctl enable gpio-shutdown.service
▨ API управления эквалайзером
Управление эквалайзером через командную строку это конечно по гиковски, но хотелось бы упростить задачу настройки эквалайзера для рядового пользователя, поэтому я решил перенести функцию регулировки эквалайзера в мобильное приложения. Для осуществления задуманного, нам необходимо реализовать API, сделаем это с помощью следующего Python скрипта:
Код API эквалайзера [eq.py]
import json import subprocess from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse class EQRequestHandler(BaseHTTPRequestHandler): def _set_headers(self, status_code=200): self.send_response(status_code) self.send_header('Content-type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.end_headers() def do_OPTIONS(self): self._set_headers(204) def do_GET(self): if self.path == '/eq': self._set_headers() eq_values = self.get_equalizer_values() self.wfile.write(json.dumps(eq_values).encode()) else: self._set_headers(404) self.wfile.write(json.dumps({"error": "Not found"}).encode()) def do_POST(self): if self.path == '/eq': content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) try: data = json.loads(post_data) results = {} for band, values in data.items(): if isinstance(values, list): success = self.set_equalizer_value(band, *values) else: success = self.set_equalizer_value(band, values) results[band] = "success" if success else "failed" self._set_headers() self.wfile.write(json.dumps(results).encode()) except json.JSONDecodeError: self._set_headers(400) self.wfile.write(json.dumps({"error": "Invalid JSON"}).encode()) else: self._set_headers(404) self.wfile.write(json.dumps({"error": "Not found"}).encode()) def get_equalizer_values(self): """Получает текущие значения эквалайзера""" process = subprocess.Popen( ['amixer', '-D', 'equal', 'contents'], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, stderr = process.communicate() if stderr: return {"error": stderr.decode()} equalizer_data = stdout.decode().split('\n') equalizer_values = {} current_band = None for line in equalizer_data: line = line.strip() if line.startswith("numid=") and "name=" in line: name_start = line.find("name='") + 6 name_end = line.find("'", name_start) current_band = line[name_start:name_end] elif line.startswith(": values=") and current_band: values_start = line.find("=") + 1 values = line[values_start:] equalizer_values[current_band] = values.split(',') return equalizer_values def set_equalizer_value(self, band_name, left_value, right_value=None): """Устанавливает значение для полосы эквалайзера""" if right_value is None: right_value = left_value cmd = [ 'amixer', '-D', 'equal', 'cset', f"name='{band_name}'", f"{left_value},{right_value}" ] result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 def run(server_class=HTTPServer, handler_class=EQRequestHandler, port=8090): server_address = ('', port) httpd = server_class(server_address, handler_class) print(f'Starting httpd server on port {port}...') httpd.serve_forever() if __name__ == "__main__": run()
Данный скрипт выполняет функцию выгрузки конфигурации эквалайзера в JSON формате по запросу приложения и установки нового уровня полосы. По сути, мы просто парсим командную строку с выводом эквалайзера для чтения текущих уровней и устанавливаем новые с помощью модуля subprocess.
Как и предыдущие скрипты, скрипт API эквалайзера добавим в автозагрузку с помощью сервиса:
sudo nano /etc/systemd/system/eq_web.service
Со следующим содержимым:
[Unit] Description=EQ Web Service After=network.target After=gmediarender.service [Service] User=root Group=root ExecStartPre=systemctl stop hello_display.service ExecStart=/root/myvenv/bin/python3 /home/scripts/eq.py WorkingDirectory=/home/scripts Environment="PATH=/root/myvenv/bin:/usr/bin:/bin" StandardOutput=inherit StandardError=inherit Restart=always [Install] WantedBy=multi-user.target
И активируем автозапуск:
systemctl enable eq_web.service
▨ Мобильное приложение
Здесь я изобретаю свой велосипед разработал мобильное приложение для трансляции аудиопотока в Hi-Fi качестве с мобильного устройства, дополнительно реализовав функцию управления эквалайзером, используя наш API, который мы реализовали ранее. Ниже представлены скриншоты экранов приложения:
В приложении реализован запуск/остановка трансляции системного звука, настройка эквалайзера, лаунчер приложений популярных стриминговых сервисов и, чтобы было красивее, добавил индикатор спектра. Регулировка громкости акустики выполняется с помощью кнопок громкости смартфона, во время работы главного экрана приложения. Трансляция аудиопотока выполняется со следующими характеристиками:
-
Контейнер WAV;
-
Частота дискретизации: 48 кГц;
-
Глубина: 32 бит;
-
Кодек: PCM.
Акустика автоматически определяется в сети с помощью UPnP протокола в процессе запуска приложения.
❯ Сборка акустической системы
Пожалуй, это один из самых приятных процессов, словно собирать конструктор в детстве.
▨ Всё акустическое
После теста нескольких динамиков одного форм-фактора и разных производителей, я остановился на следующей модели:
Производитель заявляет:
Акустика серии ACV PD – это широкополосные динамики по бюджетной стоимости, которые отлично подходят для штатной установки. Имеют достаточно высокую номинальную мощность для такого вида систем и чувствительность 88 дБ.
Динамики исполнены в стальных штампованных корзинах, оснащены бумажными диффузорами с резиновыми подвесами, воспроизводят диапазон 70 Гц-20 кГц.
Ниже приведены технические характеристики динамика:

Несмотря на то, что производителем заявлен диапазон воспроизводимых частот 70 Гц–20 кГц, данный динамик неплохо справляется с воспроизведением НЧ-диапазона, начиная с 28 Гц после настройки эквалайзера. Динамик имеет хороший подвес и больший ход по сравнению с аналогами в том же ценовом сегменте.
Поскольку в моей конструкции используется акустическое оформление типа ПИ (пассивный излучатель), необходимо обеспечить максимальную герметичность корпуса колонки для качественного воспроизведения низких частот. Для герметизации динамиков я напечатал уплотнительные кольца из TPU-пластика, который отлично справляется с этой задачей.
Для снижения внутренних переотражений звуковой волн, я использовал небольшое количество синтепона, при этом не перекрывая путь к отверстию, где будет размещена мембрана пассивного излучателя.
Пассивные излучатели были заказаны в Китае. Пока они шли, я решил провести эксперимент: а что, если напечатать их самостоятельно из TPU-пластика? В результате нескольких итераций и модернизаций мне удалось создать оптимальную конструкцию мембраны.
После установки мембран, задняя часть акустики выглядела следующим образом:
Самодельные пассивные излучатели показали себя достаточно хорошо, поэтому идея не лишена смысла. Далее я заменил их на заводские мембраны, после того как они пришли ко мне:
▨ Установка электроники
Платы электроники и аккумуляторная батарея устанавливается в шасси, которое спроектировано особым образом для удобного расположения компонентов и проводки:
Блок АКБ также приобрел свой корпус, который был напечатан на 3D принтере. Для возможности отключения аккумулятора используется разъем XT-60.
Индикатор заряда и OLED дисплей устанавливается на переднюю панель:
В итоге передняя панель выглядит следующим образом:
Единственный физический элемент управления — это кнопка питания, она располагается на верхней панели акустики и имеет встроенный красный светодиодный индикатор, который очень хорошо сочетается с черным цветом корпуса акустики.
▨ Итоговая конструкция
После завершения процесса сборки, вы можете видеть следующий результат:
В данной конструкции я постарался максимально продумать все элементы крепления, и кажется у меня получилось. И так как это только первый прототип, я не особо уделял внимание постобработке корпуса, поэтому на корпусе присутствуют различные неровности и т. п.
❯ Итоги
Изначально я испытывал скепсис относительно воспроизведения низкочастотного диапазона, но результат развеял все сомнения. Звучание акустической системы оказалось достаточно приятным и вызывает только положительные эмоции — низкие частоты мягкие и естественные. Основную роль в этом, конечно, играет система доставки аудиоконтента и качество усилителя. Ниже приведены демонстрационные видео работы системы.
Видео демонстрации
На ближайшие недели данная акустика стала любимым средством для воспроизведения стримингового контента в Hi-Fi качестве. И подозреваю, что соседи тоже довольны.
Если у вас есть что добавить, то добро пожаловать в комментарии! Всем спасибо за уделенное время!
Ссылки к статье:
-
Исходники проекта [GitHub];
-
Мобильное приложение [GooglePlay].
Данная статья сгенерирована человеком.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
ссылка на оригинал статьи https://habr.com/ru/articles/897010/
Добавить комментарий