Эта статья о том, как череда не связанных между собой событий привела меня от разработки программы цветомузыки на Arduino к созданию функционального онлайн-плеера, который не только закрыл мои музыкальные потребности, но и заменил мне и моим друзьям ушедшие зарубежные стриминговые площадки.

Всем привет. Меня зовут Владислав. Я работаю в компании NTechLab фронтенд-разработчиком и уже более 10 лет пишу на JavaScript и TypeScript. В своей жизни я часто использую эти навыки для решения различных бытовых задач. Как и в этой истории, например.

image


История моего проекта


Проект, который я хотел бы представить, начался довольно скромно, но со временем перерос в нечто гораздо большее. Все началось на новогодних праздниках, когда я разработал простое приложение на Qt, которое отправляло сигналы на Arduino для управления автомобильными лампами 12 V, мигающими в такт музыке. Работа с музыкой происходила через библиотеку BASS, которая умеет и воспроизводить, и считать значения спектра. Лампочки подключались через транзисторы на ШИМ-выводы. Результат оказался довольно забавным: плавные переходы цветов и мерцание ламп создавали необычную, праздничную атмосферу. Приложение выглядело довольно просто, но было очень эффектным.


Остатки от старой версии


Старый софт

От ламп накаливания до взаимодействия с MIDI


На следующий Новый год были уже лампочки накаливания на 220 V. Они управлялись от той же Arduino с использованием модуля тиристоров. Программное обеспечение сильно не изменилось, а необходимость в блоке питания на 12 V отпала. Лампы обеспечивали значительную яркость и могли осветить небольшой зал. Для дома этого хватало с головой.
Слово за слово, и мы взяли их на день рождения друга, который проходил на репетиционной базе. Вместо старой программы были разработаны наброски нового ПО, соединяющего лампы с MIDI-клавиатурой. Все было написано на JavaScript с Vue.JS в виде веб-приложения, ведь JavaScript умеет работать как с MIDI-устройствами, так и с COM-портами. Таким образом, когда звучала музыка, каждый мог подойти и поиграть светом. Эта тема оказалась очень занимательной, и с тех пор мы стали брать лампочки на каждые посиделки.

Новые горизонты: от ламп к DMX-системе и визуализации


Постепенно захотелось большего. Одним из минусов ламп было то, что они сосредоточены в одной точке на большой деревянной рейке, и возникло желание разнести их по углам помещения. Кроме того, цапонлак на лампах не позволял достигать сочных цветов, как это делают светодиоды. В итоге был разработан переходник с USB на DMX512 с использованием той же Arduino и модуля на микросхеме MAX485. Это позволило подключить свет к готовым DMX-светильникам, что значительно расширило возможности управления.


Новая площадка с экранами

У нас появилась новая площадка, где мы могли протестировать обновленное оборудование с использованием DMX512. Всяческий свет, лазерный проектор, дым-машина. В дополнение на этой площадке висело около 20 мониторов, подключенных к одному хабу. Захотелось не просто отображать на них случайные изображения, а создать что-то более интересное и связанное с освещением. Эта идея вдохновила меня на разработку видеоклипов, которые синхронизировались бы со звуком и светом, создавая гармоничное аудиовизуальное пространство для всех участников мероприятий.

Синхронизация музыки и видео



youtu.be/lyGvBiTKeXk?si=TEdvui6LIWfolDvs

Самое первое, с чем я столкнулся, — это необходимость отладки. В приложение добавились воспроизведение видео с кошкодевочками, специальный таймлайн для разметки аудиофайла. Можно было как сделать разметку файла вручную, так и разметить пачку файлов через программу Native Instruments Traktor. Далее сетка (BeatGrid) парсилась из «Трактора» и подгружалась в мое приложение.
Для синхронизации музыки и видео я разработал код, который вычислял скорость и фазу видео в зависимости от аудиотрека. Это оказалось относительно просто, но потребовало внимательности в обработке таких моментов, как изменения позиции в треке и паузы. Формула, по которой я делал синхронизацию, была достаточно прямолинейной:
  • скорость видео = BPM видео / BPM аудиотрека
  • фаза видео = фаза аудиотрека

const audioPosition = audioPlayerState.position;
const audioBeat = (audioPosition - audioOffset) / audioPeriod;
const audioPhase = audioBeat % 1;

const videoPosition = this.video.currentTime;
const videoBeat = (videoPosition - videoOffset) / videoPeriod;
const newTimes = [
videoOffset + videoPeriod * (Math.floor(videoBeat - 1) + audioPhase),
videoOffset + videoPeriod * (Math.floor(videoBeat) + audioPhase),
videoOffset + videoPeriod * (Math.ceil(videoBeat) + audioPhase)
]
.filter((item: number) => item >= 0)
.sort((a, b) => {
	return Math.abs(a - videoPosition) - Math.abs(b - videoPosition);
});
if (this.video) {
this.video.playbackRate = videoRate;
this.video.currentTime = newTimes[0];
}


В самом начале видео по мигающим ушам кошкодевочек было сразу видно, что темп видео совпал с темпом аудио и мы молодцы. А кадр из видео так и стал основным логотипом плеера.
После внесения этих улучшений я немного причесал интерфейс и решил выложить как есть на «Пикабу». Почему бы не поделиться с людьми и не позволить им тоже позалипать в этом эксперименте? В своем посте я попросил помощи у кого-то, кто разбирается в дизайне, так как тогда я был больше погружен в код и математику.
Когда я выложил приложение на «Пикабу», отклик был удивительно положительным. Люди писали комменты, ностальгировали по разным старым программам с похожим функционалом, предлагали разные варианты развития. Это вдохновило меня улучшить интерфейс. Дизайнер Борис Маслов помог мне задать вектор для редизайна: он предложил добавить обложки к трекам, папки для загрузки музыки, улучшить вид плейлиста и в целом сделать интерфейс более удобным и красивым. Это был важный шаг, ведь до этого интерфейс выглядел довольно сыро.
Однако у меня не было обложек для треков, БД, учетных записей. Казалось, что, чтобы все это реализовать, нужно практически написать с нуля. Я почти забросил проект, наслаждаясь музыкой в Spotify. Но неожиданно, когда сервис ушел из страны, я столкнулся с проблемой потери плейлистов. Я кое-как переехал на «Яндекс Музыку». Через пару месяцев большая часть моих треков там просто пропала. Это стало толчком для того, чтобы вернуться к проекту и продолжить его развитие. Ведь теперь у меня появилась возможность создать собственный музыкальный сервис, который не зависел бы от внешних факторов.

Новая жизнь проекта: восстановление и оптимизация


Я был, мягко говоря, расстроен, когда потерял доступ к своим любимым плейлистам. Однако, вместо того чтобы сдаваться, я решил упорядочить оставшуюся у меня музыку. Какую-то часть добавили друзья. Полученную коллекцию я обернул через NodeJS чексуммами для отслеживания поврежденных файлов и использовал PostgreSQL для хранения метаданных, таких как год выхода, артист, альбом и лейбл. С появлением БД появились учетные записи, а с ними и простановка лайков трекам. Для быстрого поиска я интегрировал Elasticsearch, что оказалось настоящим прорывом. Теперь, когда я вводил запрос, результаты появлялись за доли секунды. Таким образом, проект получил новый импульс.


Мой ЦОД из ноутбука и док-станции с жесткими дисками

Все части были раскиданы по докер-контейнерам и проброшены порты. Получился сервис, который позволял слушать музыку и дома, и в машине, и на работе, и на даче. Баги иногда стреляли, но они не блокировали работу и не вызывали сильных неудобств. В таком виде система стала функционировать, долгое время закрывая мои потребности по прослушиванию музыки.




Старый дизайн плеера

Люди добавляли новую музыку, количество пользователей увеличивалось. В свободное время я продолжал не торопясь исправлять ошибки и дорабатывать интерфейс. Стала появляться даже музыка друзей друзей. Нагрузка на систему росла в геометрической прогрессии. Вскоре я понял, что «Трактор» (Native Instruments Traktor) уже не мог справляться с такими объемами треков. К тому же работать с ним становилось все менее удобно, а качество данных, которые он выдавал, уже не впечатляло. Поэтому я принял решение начать вычислять сетку темпа для трека самому.

Разработка собственного генератора сетки темпа


Первое, что пришлось улучшить, — это средство для отладки результатов. Таймлайн с сеткой был оптимизирован для быстрого обновления сетки, созданной моим алгоритмом. Прошло еще какое-то время, и мои размышления о создании собственного генератора сетки темпа начали постепенно приносить плоды. Я начал экспериментировать, и стало получаться что-то интересное.


Программа для разметки сетки треков

Однако самое неприятное в этом процессе заключалось в том, что алгоритм для разметки треков был сделан на С++ c kissfft и ffmpeg и мог работать в течение нескольких недель, а затем внезапно упасть с сегфолтом на каком-то конкретном треке. Бывали ситуации, когда я даже не знал, на каком именно треке произошел сбой, а иногда, наоборот, знал и сталкивался с падением в 1 из 10 случаев. Или просто память текла. Или просто проц ревел, что пашет за десятерых, а процесс не шел.
Как говорится, вода камень точит. Постепенно я начал прояснять загадки тех магических багов, которые вызывали сбои в разметке треков.
Самым интересным, на мой взгляд, был цикл на флоатах, в котором прибавлялся настолько маленький флоат, что он его просто игнорировал и зацикливался. Поэтому всегда используйте циклы с целыми числами.
Процесс исправления ошибок был небыстрым: я правил всего пару строчек кода, затем запускал алгоритм и уходил на неделю или даже месяц, оставляя его работать в фоновом режиме. Это требовало терпения, но каждый новый успех вдохновлял меня продолжать работу над проектом.

Эволюция веб-интерфейса плеера


Сам веб-интерфейс моего музыкального плеера также постепенно становился все более стабильным. Я добавлял различные полезные функции и улучшения в пользовательский опыт (UX). Плеер активно использовал не только я, но и мои друзья, и это придавало дополнительную мотивацию для дальнейшей работы.
Я использовал алгоритм квантизации цветов обложки трека, чтобы извлекать основные цвета и перекрашивать интерфейс плеера в соответствии с этими цветами, что добавляло визуальную привлекательность.


Цветовая схема использует цветовую палитру из обложки трека

Кроме того, Elasticsearch значительно ускорил процесс поиска и добавления пачки треков. Я мог легко импортировать музыку из «ВКонтакте» и «Яндекса» по названиям. Достаточно было просто скопировать список треков из любого источника и выполнить поиск, чтобы добавить их в новый плейлист. Это значительно облегчило взаимодействие с плеером и сделало процесс импорта треков максимально простым и быстрым.


Поиск альбомов и треков

В выходных данных разметчика теперь были аккорды, частоты трека на таймлайне и изображение спектра. Это стало настоящим помощником, когда я хотел подыграть на пианино под текущий играющий трек, поскольку теперь я мог сразу видеть аккорд, который звучит в данный момент.


Отображение спектрограммы, сетки и аккордов.

Если затронуть мобильное приложение, я сразу закладывал максимальную адаптивность в верстке, так как делать отдельное приложение под все устройства не было ни сил, ни времени, ни возможности, ни знаний.

Вместе с одной знакомой, которая учится на дизайнера, мы посмотрели разные готовые приложения и сделали наброски мобильного варианта. За что ей тоже большая благодарность, если она это читает. Пришлось немного переделать десктопную версию, чтобы она могла перетекать в мобильную. На телефоне в Chrome можно было установить сайт как PWA-приложение, тогда плеер устанавливается как приложение с отдельным ярлыком. На десктопе в адресной строке Chrome есть стрелочка с подобным функционалом.

Я бы поспорил с тем, что если бы вначале была мобильная версия, то десктопную сделать проще. Может быть, и проще, если задача — просто отобразить хоть как-то, но если хочется сделать красиво и гармонично, придется продумать этот переход.


Такая мобильная версия получилась.

Интересной особенностью стал вебсокет-чат, который позволял управлять плеером на одном устройстве из другого. Это делало совместное прослушивание музыки очень удобным. Или можно было лежа на диване подключиться к плееру на компе и сменить трек. Я внедрил несколько аудиокомпонентов на странице, что обеспечивало плавный переход от одного трека к другому, создавая непрерывный поток музыки.

Обучение нейросети для генерации плейлистов


Дальше мне захотелось большего, и я решил использовать все накопленные побочные данные аудиоразметчика для обучения нейросети. При создании сетки темпа я в ходе экспериментов собрал множество побочных характеристик трека — ритм, частоты, ноты и прочие параметры. В итоге получилось около 400 различных числовых характеристик, которые позднее переросли в 4000. Возникла идея использовать эти данные для нейросети.
Я использовал PyTorch и четыре полносвязанных слоя. Первый слой принимал числовые характеристики трека. Второй слой был скрытым, чтобы сделать модель более сильной. Третий слой был векторным и предоставлял непосредственно векторы для векторизации треков. Ну и последний слой определял плейлист, которому принадлежит трек. Таким образом, нейросеть училась размещать в векторном пространстве треки тем ближе, чем ближе в плейлисте они расположены.
Я пробовал разные подходы к нормализации данных. Сложно выделить, какой лучше, а какой хуже. Очень часто в результате при выборе топ-10 треков несколько могли попасть прямо совсем мимо, а несколько хоть и звучали по-другому, но передавали тот же вайб.


Обучение нейросети

В процессе работы я столкнулся с некоторыми проблемами, которые мешали мне добиться желаемого результата. К счастью, мне на помощь пришел Михаил Антонович Горохов, который делал видео на YouTube по нейросетям. Его объяснения были очень доступными и понятными, и я был благодарен за помощь, потому что он показывал все без вырезок и монтажа.
Еще возникла проблема, когда объем данных для обучения перестал помещаться в оперативку, как бы я ни старался, пришлось написать свой датасет на базе SQLite. Теперь данные не выдавались из памяти, а считывались с диска. За счет памяти SQLite поддерживала кеш данных, что, как мне кажется, снижало нагрузку на диск. Обучение проходило намного медленнее, но проходило.

class MySqliteDataset(Dataset):
	def __init__(self, file_path:str):
    	super().__init__()
    	self.sqlite_conn = sqlite3.connect(file_path)
    	self.sqlite_cursor = self.sqlite_conn.cursor()
    	self.sqlite_cursor.execute('PRAGMA page_size = 4096')
    	self.sqlite_cursor.execute('PRAGMA cache_size = 7388608')
    
	def __len__(self):
    	self.sqlite_cursor.execute("SELECT MAX(ROWID) FROM train_data")
    	item = self.sqlite_cursor.fetchone()
    	return item[0]

	def __getitem__(self, idx):
    	self.sqlite_cursor.execute("SELECT json_data, category_index FROM train_data WHERE ROWID = ?", [idx + 1])
    	item = self.sqlite_cursor.fetchone()
    	x = torch.from_numpy(np.asarray(json.loads(item[0])))
    	y = torch.tensor(item[1])
    	return (x, y)


Одним из больших плюсов стало то, что я использовал Elasticsearch, который изначально поддерживал поиск по векторам. Это избавило меня от необходимости изобретать велосипед — достаточно было просто добавить векторы эмбеддингов из нейросети в поисковый индекс. Я выбрал 64-мерное пространство для представления векторов. В качестве функции сравнения я использовал CosineSimilarity. Эта функция сравнивает векторы как угол между ними в многомерном пространстве. Также хочу попробовать l1_norm и l2_norm, которые считают среднее и среднеквадратичное расстояние между координатами векторов.

Результаты работы нейросети


В итоге получившаяся система оказалась довольно прикольной: когда я начинал искать похожие треки, она генерировала бесконечный плейлист с композициями того же жанра. Например, можно было легко запустить бесконечное радио в жанре chillhop (где девочка на YouTube учит уроки) или, наоборот, что-то танцевальное.



пример 1:
исходный: SwuM, Chief. — Show Me How
первый найденный: G Mills, HM Surf — Mmmm

пример 2:
исходный: Edward Maya & Vika Jigulina — Stereo Love
первый найденный: Violet Light — Love Story (Original Version)

пример 3:
исходный: Simple Plan, Julian Emery — Astronaut
первый найденный не ремикс оригинала: Rush Of Fools — Undo
далее: Josh Ross — Ain't Doin' Jack

Интересно, что нейросеть сама по себе не знает, что такое жанр и каковы его особенности, но тем не менее создает музыкальные последовательности, которые приятно слушать. Это не окончательная версия алгоритма для векторизации и рекомендаций треков. Разметка треков еще не завершена, что позволит увеличить количество треков для обучения и, соответственно, улучшить качество результатов. Современные браузеры поддерживают OpenGL, что позволяет визуализировать треки в 2D и 3D пространстве на основе векторов. Это открывает новые горизонты для музыкального эксперимента и поиска звучания!

Доработка проекта и планы на будущее


В последний год я не занимался разработкой новых сложных функций, а в основном сосредоточился на исправлении мелких багов, на которые натыкался в процессе использования плеера. Теперь я чувствую, что проект уже достаточно зрел, чтобы смело показать его людям на «Хабре».
Что касается приложения на Android, то оно почти готово. Я использовал WebView для интерфейса и создал отдельную прослойку, которая воспроизводит аудио нативно через Media3 (в этом сильно помог @AlexLong4). Это значит, что интерфейс для формирования плейлистов остается полностью готовым, а воспроизведение производится с использованием нативных возможностей Android. Однако мне пока не удалось заставить Media3 кэшировать треки в папку на телефоне. Если кто-то сможет помочь решить эту задачу, это будет замечательно, и мы сможем завершить разработку приложения. Я просто не являюсь Android-разработчиком, поэтому здесь мне нужна поддержка.

В будущем мне бы хотелось создать распространяемый бэкенд, чтобы каждый мог его запустить на своем компе или в Docker на сервере. Бэкенд мог бы хостить локальную музыку или использовать торрент-клиент с Web API (например, transmission-daemon отлично живет в докере) для управления загрузками, следить за статусом загрузки и отдавать треки по HTTP. Плюс было бы удобно поддерживать раздачи тех альбомов, которые находятся в «избранном» плеера и уже лежат на диске. Это позволило бы развивать плеер как статическое веб-приложение, которое только управляет загрузками, раздачей, воспроизведением, рекомендациями и лайками. Можно было бы сделать базу с DHT-хешами, проиндексировать музыку, которая уже находится в торрент-сети. Это все откроет новые возможности для пользователей и сделает приложение еще более функциональным!

Заключение




Мой проект стал чем-то большим, чем просто плеер. Он обрел массу интересных фич, таких как интеграция с нейросетями, синхронизация видео и аудио, а также возможность искать похожие треки и формировать уникальные плейлисты. На данный момент приложение стабильно работает, и я горжусь его прогрессом. В будущем я надеюсь продолжить развивать его, добавляя новые возможности и улучшая производительность.
Если кто-то захочет помочь с доработкой Android-версии или с другими аспектами проекта, буду рад!

Сам плеер тут:
someradio.github.io
web.valse.me

Обсуждение и доки тут:
t.me/valse_me
vk.com/valse_me

Не законченное приложение под Android тут:
github.com/vartemkin/valse_app

Дисклеймер: Все упомянутые в настоящей статье объекты интеллектуальной собственности принадлежат соответствующим правообладателям и используются исключительно в информационных, научных, учебных или культурных целях в качестве иллюстраций тезисов автора статьи в объеме, оправданном поставленной целью.

Комментарии (2)


  1. zabanen2
    05.02.2025 10:44

    "маленькое приложение на qt" политое с любовью