Як я написав власний B2B-аудіоплеєр на Python, щоб охоронці перестали панікувати під час тривог та блекаутів
Як я написав власний B2B-аудіоплеєр на Python, щоб охоронці перестали панікувати під час тривог та блекаутів
Привіт, спільното SPEKA! Поки індустрія повністю поглинута хмарами, мікросервісами та черговими Electron-додатками, які здатні «вижерти» гігабайт оперативки ще до того, як користувач побачить перше вікно, мені довелося зіткнутися із суворою реальністю українського ритейлу «на землі».
Мене звати Олександр, я працюю системним адміністратором. Останні роки жорстко навчили нас, що стабільність ІТ-інфраструктури в Україні — це поняття дуже й дуже відносне. Сьогодні я хочу поділитися досвідом розробки та розгортання NekoWeave — десктопного додатка для автоматизації фонового звуку та аудіомаркетингу, який я писав 5 місяців під наші, цілком конкретні й жорсткі болі.
### Як все починалося: Від простого скрипта до комбайна Історія NekoWeave почалася не з бажання зробити музичний плеєр. Спочатку це був просто маленький Python-скрипт під назвою AlertC4tz.py.
Справа в тому, що у нас комп'ютери, які крутять музику в торгових центрах, зазвичай стоять у кімнаті охорони або в операторській. Звук із них виводиться на потужний трансляційний підсилювач. Паралельно на цьому ж компі крутиться специфічний софт для керування вентиляцією та іншими системами будівлі. **Проблема:** Коли починалася повітряна тривога, охоронець чи оператор через неуважність (або тому що відійшов) часто не встигав вчасно вимкнути музику і вручну запустити звук сирени та правила евакуації. Людський фактор давав збої. Тому я написав скрипт, який автоматично моніторив тривоги і глушив звук. Я запустив цей скрипт на п'яти ТЦ в авторежимі. Усе працювало ідеально, аж поки бізнесу не знадобилося «по-нормальному» керувати ще й музикою та рекламою. Класичні плеєри не вміли нормально інтегруватися з моїм скриптом тривог. Саме тоді я подумав: «А чому б не написати повноцінний плеєр із візуальним планувальником і вбудованим моніторингом тривог в одному флаконі?». На розробку першої стабільної збірки та тести пішло приблизно 5-6 місяців. Усе допилювалося рівно так, як це було потрібно реальному бізнесу, а не так, як це бачать маркетологи SaaS-продуктів.
Технічний стек та «юридичне пекло»: Чому PySide6, а не PyQt5 чи Electron?
Тягнути Chromium-движок (Electron) на операторський комп'ютер, який і так ледве дихає від софту для вентиляції, було б чистим самогубством. Мені потрібен був монолишний софт, який споживає мінімум ресурсів і працює як годинник у режимі 24/7. Тому я одразу обрав Python як базу.
Але щойно ти переходиш від написання «скриптів для себе» до створення комерційного продукту, ти різко врізаєшся в сувору реальність опенсорс-ліцензування. Я думав: «Зараз швиденько накидаю інтерфейс на улюбленому PyQt5 і готово». Ага, щас. Почавши читати юридичні мануали, я зрозумів, що PyQt5 тягне за собою жорстку вірусну ліцензію GPL.
Якщо ти використовуєш GPL у закритому B2B-продукті, ти зобов'язаний або безкоштовно відкрити всі вихідні коди своєї програми для всіх (що вбиває суть комерційного захисту), або платити величезні відрахування за комерційну ліцензію компанії Riverbank Computing. Це був холодний душ. Мені довелося витратити купу часу, вичитуючи ліцензійні угоди десятків бібліотек, розбираючись, що можна спокійно тягнути в пропрієтарний софт, а за що доведеться відповідати.
Так я прийшов до PySide6 — офіційного порту від самої Qt Company. Він розповсюджується під LGPL v3, що легально дозволяє динамічно лінкувати його в закритий комерційний софт абсолютно безкоштовно, за умови правильного зазначення авторства. У результаті я створив акуратний файл LICENSE_NOTICES.txt, де чесно перерахував усі використані компоненти (PySide6, NumPy, Requests), і мій софт став юридично «чистим».
Щодо аудіо-рушія: standardний QtMultimedia — це ще той біль. Він періодично ловить краші при обривах мережі. Тому для керування звуком під Windows (особливо для реалізації ефекту Ducking при тривогах) я задіяв бібліотеку PyCAW (Python Core Audio Windows). Вона розповсюджується під безпечною MIT-ліцензією і дозволяє напряму смикати системний мікшер через COM-інтерфейси (WASAPI). Я отримав можливість філігранно керувати гучністю додатка на рівні самої ОС, без милиць.
#### Біль компіляції, Антикраш та магія Nuitka
Замість класичного PyInstaller я використав Nuitka. Це крутий транслятор, який переганяє Python-код у C++ і компілює його через компілятор MSVC або GCC у справжній бінарний .exe.
На практиці збірка великого PySide6 проєкту через Nuitka виглядає як окремий вид мазохізму, поки не підбереш правильні прапори і не налаштуєш «антикраш» механізми (через специфіку роботи Qt-плагінів у Windows). Щоби софт запускався на будь-якій «чистій» Windows 10/11 без відвалу графічного рушія, моя фінальна команда збірки виглядає так:
nuitka --standalone --enable-plugin=pyside6 --disable-console --windows-icon-from-ico=app_icon.ico --onefile main.py
Прапорець --onefile у зв'язці з --standalone змушує Nuitka не просто зробити зліпок, а розкласти все в оптимізований бінарник. На виході я отримав один файл розміром рівно 169 МБ. Я запакував його через Inno Setup і закинув на GitHub. При старті він не тупить, як це робить PyInstaller. Крім того, декомпілювати збірку Nuitka в рази складніше, що дає базовий захист логіки від реверс-інжинірингу.
### Детектор тиші та Автоматичний Failover: Коли провайдер «лягає»
Якщо ви повністю залежите від інтернет-радіо, і десь на магістралі обірвався лінк або блимнуло світло — починається катастрофа.
Підписуйтеся на наші соцмережі
Щоб вирішити цю проблему, я реалізував у NekoWeave власний Silence Detector. Замість того, щоб чекати абстрактних помилок від сокета, система аналізує реальний рівень звуку (dB) на льоту.
Технічно розрахунок RMS (середньоквадратичного значення) для перевірки наявності звуку в чанку виглядає наступним чином. Ми беремо масив байт із буфера, перетворюємо його за допомогою numpy у масив амплітуд і рахуємо рівень у децибелах:
def calculate_db(audio_data):
import numpy as np
samples = np.frombuffer(audio_data, dtype=np.float32)
if len(samples) == 0:
return -100.0
rms = np.sqrt(np.mean(samples**2))
if rms == 0:
return -100.0
db = 20 * np.log10(rms)
return db
Якщо цей метод стабільно повертає значення нижче заданого порогу (наприклад, -50 dB) протягом п'яти секунд, ми фіксуємо аварію стріму. Отримавши подію StreamDeadEvent, плеєр миттєво перемикає вихідний аудіо-канал на локальний «Резервний пул» — папку на диску з MP3-треками. Перемикання йде через crossfade (плавне згасання), тому відвідувачі нічого не підозрюють. Коли онлайн-радіо «оживає», music плавно повертається назад. Повна автономність досягнута.
### Інтеграція з API alerts.in.ua: Автоматизація цивільного захисту
Як я вже казав, це була першочергова фіча. Окремо хочу висловити величезну подяку команді проєкту alerts.in.ua — вони офіційно дозволили мені використовувати їхнє API в моїй програмі. Це дуже крутий приклад взаємопідтримки в нашому ІТ-ком'юніті!
Щоб усе працювало, я зробив максимально дружній інтерфейс. У налаштуваннях NekoWeave є розділ «Тривоги» зі спеціальним полем для вводу API-ключа. Копіюєш, вставляєш у програму — і все! Після цього в головному вікні додатка одразу активується живий моніторинг і навіть візуальна карта тривог.
Оскільки закладу в Одесі не цікаво, що відбувається у Львові, в налаштуваннях є жорстка геолокаційна прив'язка: можна обрати конкретну область, район чи територіальну громаду.
Фоновий потік опитує API кожні 15-20 секунд. Щоб не покласти сервер хлопців з alerts.in.ua паразитним навантаженням, я використовую requests з утриманням сесії (Session) та правильною обробкою заголовків ETag і Last-Modified. Трафік виходить просто мізерним.
**Ducking ефект у дії:**
Коли статус в API змінюється на active, спрацьовує механізм «Ducking». Гучність основної музики плавно знижується до 10-15%, і поверх неї на іншому віртуальному аудіоканалі запускається .wav ролик з інструкціями щодо евакуації. Щойно статус змінюється на resolved, система може відновити гучність або відіграти джингл «Відбій повітряної тривоги». Охоронцям більше не треба бігати до комп'ютера.
### Візуальний планувальник та маркетингові ін'єкції
Закладу постійно потрібно крутити акції: «Зачиняємося через 15 хвилин» або правила ТЦ. У звичайних плеєрах це робиться через жахливі плейлисти. Це неможливо зробити, якщо у вас грає безперервний інтернет-стрим.
Я написав свій Візуальний планувальник (Visual Scheduler) за принципом легкої DAW (Digital Audio Workstation).
* Є візуальна сітка на 7 днів тижня.
* Можна «врізати» аудіоролик у точний час (наприклад, 14:15:00).
* І головна кіллер-фіча — матриця пріоритетів. Ролик може поставити фоновий стрим на паузу, може зробити плавний Ducking, а може просто накластися поверх.
Усе це працює асинхронно і не блокує основний цикл обробки подій PySide6 (Main GUI Thread). Інтерфейс не зависає навіть під час рендерингу складних розкладів.
### Граблі, на які я наступив: GC, Throttling та DWM
Коли пишеш великий додаток на зв'язці C++ (Qt) та Python, тебе чекає кілька дуже специфічних пасток, які не гугляться за п'ять хвилин. У коді налаштувань NekoWeave я залишив багато коментарів із приставкою STABILITY та FIX, бо крові вони випили чимало. Ось мій топ-3:
**1. Збирач сміття (GC) вбиває ваші потоки**
У Python є класична проблема з Qt: ви створюєте QThread або воркер у локальній області видимості методу, запускаєте його, метод завершеться... і Python'івський Garbage Collector радісно видаляє об'єкт з пам'яті, хоча фоновий C++ потік все ще працює. У результаті програма або тихо «ковтає» потік, або вибухає з Segmentation fault.
Довелося створювати глобальне сховище для фонових задач (self._active_workers = []), щоб штучно утримувати посилання на об'єкти, поки вони самі не відрапортують через сигнал finished.
**2. DDoS-атака на власний Event Loop**
Оскільки я додав вбудований DSP (Gate, Limiter, AGC) та керування системним мікшером через WASAPI, виникла проблема з повзунками в GUI. Якщо повісити оновлення параметрів звуку прямо на сигнал valueChanged, то швидкий рух мишкою генерує сотні подій на секунду. Це перевантажувало чергу повідомлень (Event Loop), викликаючи жорсткі лаги звуку та фризи інтерфейсу.
Рішення — Throttling (дебаунсинг) через QTimer. Тепер, коли юзер тягне повзунок, застосування параметрів відкладається на кілька десятків мілісекунд, поки рух не зупиниться. Захист від DDoS-атаки на власну ж програму!
**3. Кастомний дизайн і Windows DWM**
Щоб інтерфейс виглядав сучасно (без потворних білих рамок Windows), стандартного прапорця Qt.FramelessWindowHint виявилося замало — залишалися артефакти. Довелося лізти через ctypes напряму у Windows API і відключати рендеринг рамок на рівні самого Desktop Window Manager (через передачу winId() вікна у системні DLL).
### Підсумки: Що далі?
Загалом, незважаючи на всі граблі, програма вийшла напрочуд придатною для життя. Вона успішно тримає ефір на точках, не просить їсти і не вимагає ніякого шаманства після початкового налаштування. Музика, реклама і тривоги тепер живуть в єдиному зручному моноліті.
Розробка NekoWeave довела мені одну просту річ: десктопні програми все ще критично важливі для офлайн-бізнесу «на землі». А зв'язка Python + PySide6 + Nuitka — це просто імба, якщо ти хочеш отримати майже нативну швидкість, легально закрити код і не сивіти при роботі зі звуком (дякую, PyCAW).
Дякую за увагу! Буду радий відповісти на ваші запитання в коментарях. Залюбки обговорюю архітектуру PySide6-додатків, нюанси компіляції через Nuitka та той самий біль роботи з WASAPI під Windows.