- JavaScript 83.7%
- CSS 6.5%
- Kotlin 6.2%
- PowerShell 2%
- HTML 1.6%
| android | ||
| core | ||
| desktop | ||
| tools | ||
| .gitignore | ||
| alphatest.png | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
musetext
Кинетическая типографика под то, что играет прямо сейчас. Приложение само определяет трек через системный медиа-сеанс, находит синхронизированный текст, режет его на слова и разгоняет по чёрному экрану — с переменной перспективой (аналог CC Power Pin из After Effects), вариативной раскладкой строк и цветными вспышками.
Одно ядро на две платформы: core/ — весь визуальный движок, desktop/ и
android/ — только оболочки, которые дают ему доступ к ОС.
core/ общий веб-движок: раскладки, перспектива, эффекты, поиск текста
desktop/ Electron + чтение Windows SMTC через PowerShell
android/ Kotlin + WebView + NotificationListenerService
tools/ дев-сервер для работы над core/ в браузере
Запуск на ПК
npm install
npm start
Собрать установщик:
npm run dist
Требуется Windows 10 1809 или новее. Определение трека работает для всего, что регистрирует медиа-сеанс: Spotify, YouTube Music, браузеры, AIMP, foobar2000, десктопные UWP-плееры.
Работать над визуалом можно и без Electron:
npm run web
и открыть http://localhost:5173 — там доступны ручной ввод трека и
локальные аудиофайлы.
Сборка под Android
Открыть папку android/ в Android Studio и собрать app. Или из консоли,
если установлен Android SDK:
cd android
./gradlew assembleRelease
Gradle подключает core/ как assets напрямую, копировать ничего не нужно.
После установки приложение попросит доступ к уведомлениям — Android отдаёт
список активных медиа-сеансов только владельцу включённого
NotificationListenerService. Сервис в MuseNotificationListener.kt пустой:
он существует только как пропуск и не читает содержимое уведомлений.
Откуда берётся текст
Порядок источников:
- Локальный кэш в
localStorage. - LRCLIB — точный запрос, затем поиск в нескольких формулировках сразу: по очищенному названию, по сырому, только по названию, и с переставленными местами исполнителем и названием на случай, если файл назван наоборот. Кандидаты со всех запросов оцениваются вместе.
- NetEase — только на десктопе: эндпоинт не отдаёт CORS-заголовки, поэтому запрос идёт через главный процесс Electron.
- Файл, брошенный в окно. Он закрепляется за текущим треком навсегда.
Текст с таймингом всегда весомее точного совпадения по имени: строка без таймкодов приложению почти бесполезна, поэтому поиск продолжается, даже когда точный запрос уже что-то вернул.
Форматы
Понимает три вида разметки:
[00:07.17] строка обычный LRC
[00:07.17]<00:07.17>по <00:07.60>словам расширенный LRC
00:07.17 - строка без скобок, с любым разделителем или без него
Часы (1:00:07.5) тоже читаются. Формат без скобок включается, только если так
размечен весь файл, иначе строка, начинающаяся со времени суток, была бы принята
за таймкод.
Если пословных меток нет, слова раздаются внутри строки пропорционально их длине — достаточно точно, чтобы читалось как караоке.
Метаданные
У файла, брошенного в окно, читаются настоящие теги — ID3v2, MP4/M4A, FLAC,
Ogg/Opus — и только если их нет, берётся имя файла. Имена вида
y2mate_Название 1080p, 03. Название, Название (Official Video) чистятся
перед поиском.
Если текст всё равно не нашёлся, приложение показывает карточку с исполнителем и названием: поправь их и нажми «Искать». Найденное закрепится за треком, так что повторять не придётся.
Про YouTube Music. Определение трека оттуда работает (это обычный медиа-сеанс). А вот текста через публичный API у неё нет, поэтому слова всё равно приходят из LRCLIB или NetEase.
Рендер видео
Раздел Рендер видео в настройках. Кнопка спрашивает, куда сохранить, и пишет файл с настоящим альфа-каналом: чёрного фона в нём нет, только текст.
| Формат | Что внутри |
|---|---|
| QuickTime ProRes 4444 | prores (4444), yuva444p12le — то, что ждёт таймлайн в After Effects |
| QuickTime Animation | qtrle, argb — без потерь, файл заметно больше |
| Секвенция PNG | rgba, папка с кадрами, ffmpeg не нужен |
Разрешение задаётся пресетом или руками, отдельно кадры в секунду и отрезок трека. Галочка «Только текст» убирает эффекты, которые двигают строку.
Как это работает: страница открывается второй раз — скрыто, прозрачно и сразу в нужном разрешении — и прогоняется по кадрам. Ничего не снимается с того окна, которое вы видите, поэтому на результат не влияет ни скорость машины, ни то, что происходит в живом приложении. Рендер трёхсекундного отрезка занял 3.5 секунды.
Два ограничения стоит знать заранее. Полноэкранные слои — вспышки, зерно, скан-линии, виньетка — в прозрачном рендере выключаются всегда: каждый из них превратился бы в сплошную заливку поверх альфы. И для двух форматов QuickTime нужен ffmpeg в системе; если его нет, приложение так и скажет, а секвенция PNG работает без него.
Рендер есть только в десктопной сборке. В браузере и на Android кнопка честно сообщает, что не может.
Какой плеер слушать
Приложение показывает все медиа-сеансы, которые видит система, и само выбирает играющий: «текущий сеанс» по версии Windows часто принадлежит вкладке браузера, которая доиграла час назад. Выбор можно переопределить в настройках, раздел Источник.
VLC 3 не публикует медиа-сеанс вообще — это ограничение самого плеера, а не приложения. Его ловит отдельная дорожка через встроенный веб-интерфейс:
- В VLC: Инструменты → Настройки → Интерфейс → Показывать все → Основные интерфейсы → включить Web.
- Там же Lua → HTTP → задать пароль.
- Перезапустить VLC.
- В настройках musetext включить «Ловить VLC» и вписать тот же пароль.
Пять режимов подачи
Классика — строка ставится целиком. Перспектива меняется на протяжении её жизни, строка дрейфует и растворяется на месте.
Разлёт — перспектива задаётся один раз, в момент появления, и дальше не трогается. Кусок летит по прямой с постоянной скоростью и уходит за край экрана, а не тает в воздухе. Длинная строка не ставится одним предложением: она режется на два-три куска, и каждый появляется в своём углу кадра. Рез идёт по знакам препинания, короткие строки не дробятся.
В этом режиме размер куска берётся из громкости голоса в момент его появления, а на смехе, тянутых гласных и вибрато текст трясётся. Смех и тянутые гласные опознаются по самому тексту, вибрато по звуку, если включена реакция на звук.
Кусок не улетает, пока его ещё поют: до последнего слова он едва дрейфует, а разгоняется и уходит за край только после него.
Лесенка — строки стоят уступом: первая прижата влево, последняя вправо, между ними ровные ступени. Ни поворота, ни наклона; глубина идёт только от перспективы, которая задаётся при появлении и дальше не меняется. Ступень держится до тех пор, пока её место в стопке не понадобится снова, поэтому вся лесенка стоит на экране целиком, а не по одной строке.
Число ступеней равно значению «Строк на экране». Поставьте 2, чтобы получить ровно две строки, влево и вправо.
Камера — вся песня разложена в одну длинную ленту вправо, а едет не текст, а точка обзора. Строка стоит на своём месте в мире и оказывается по центру ровно в свой таймкод. Она рождается уже за правым краем и снимается со сцены, только когда целиком ушла за левый: никаких проявлений и растворений в воздухе, строку просто режет край кадра. Сверху медленный крен, лёгкое покачивание и дыхание трансфокатора. Скорость и крен настраиваются.
Тоннель — песня становится коридором, а зритель летит по нему. Каждая строка это плита объёмного текста, стоящая в своей точке глубины: часть написана на левой стене, часть на правой, что-то поперёк прохода, что-то на полу и потолке. Плита не двигается сама, движется точка обзора.
Объём настоящий, а не тень: у каждого слова свои боковые грани, сложенные стопкой копий по оси Z и затенённые от светлого ближнего ребра к тёмному дальнему. Тень так не умеет, потому что выдавливание идёт вдоль нормали плиты, а когда плита развёрнута к стене, это направление выходит из её плоскости.
Стены повёрнуты примерно на 55–65 градусов, а не на прямой угол: под 90 строка стоит ребром и читается, только когда вы с ней поравнялись. Строка собирается по словам на подлёте, в ритме собственных слов, и стоит целиком ровно в свой таймкод. Глубина граней настраивается ползунком «Объём букв», скорость полёта берётся из «Скорости камеры».
Фон
По умолчанию за текстом стоит обложка трека: сильно размытая, почти чёрная, и подсвечиваемая цветными вспышками по музыке. Смысл в том, что картинку освещают, а не показывают.
Обложка берётся из тегов файла (ID3v2 APIC, MP4 covr, FLAC PICTURE, Vorbis METADATA_BLOCK_PICTURE), а если файла нет, ищется по исполнителю и названию через поиск iTunes — ключ не нужен, ответ отдаётся с открытым CORS.
Если обложки нет нигде, остаются одни вспышки в палитре пресета. Всё это отключается и регулируется в разделе Фон: режим, размытие, базовая яркость и сила вспышек.
Длительность строки
Тайминг в LRC отмечает только начало строки. Наивный способ дотянуть её до следующей строки ломается там, где после фразы идёт проигрыш: слова расползаются на всю паузу.
Поэтому по умолчанию стоит по темпу пения. Скорость пения оценивается по самому треку: у строки внутри фразы отношение «символы к промежутку» близко к правде, у строки перед проигрышем оно занижено, так что настоящий темп лежит в верхней части распределения. Берётся высокий процентиль, и длительность строки считается из её длины, но не длиннее промежутка до следующей.
На реальном треке это укоротило 10 строк из 36, худшую с 8 секунд до 1.35. Строки внутри фраз не тронуты.
Режим по темпу + обрыв на тишине делает то же самое и вдобавок гасит строку, когда после последнего слова звук держится тихим. Работает только при включённой реакции на звук: без анализатора взять это неоткуда.
Режим до следующей строки — прежнее поведение, если оно вам нужно.
Текст в скобках
Бэк-вокал и ад-либы почти всегда пишут в скобках. По умолчанию такие слова приглушены, их можно вернуть в обычный вид или убрать совсем. Если в строке не было ничего кроме скобок, она пропускается целиком.
Что настраивается
Панель по клавише S или по кнопке рядом с названием трека.
Вайб — пресет: шрифт, палитра, энергия, набор раскладок и эффектов. На «Авто» пресет выбирается по плотности текста, длине строк, наличию кириллицы и, если включена реакция на звук, по реальной энергии трека.
| Пресет | Шрифт | Характер |
|---|---|---|
| Brutal | Stalinist One | монументально, медленно, крупно |
| Edit (AE) | Oswald | диагонали и перспектива, как в эдите |
| Hyperpop | Unbounded | быстро, ярко, много вспышек |
| Tech | JetBrains Mono | мелко, плотно, RGB-сдвиг и скан-линии |
| Pixel | Tiny5 | пиксельно, кислотно |
| Dream | Unbounded | мягко, крупно, с зерном |
| Raw | BBH Bartle | если файл шрифта на месте |
Движение — скорость выезда, сила перспективы, наклон, кегль, сколько строк держится на экране, отступ от краёв, насколько раньше строка появляется и сколько висит после.
«Отступ от краёв» задаёт поле, за которое строка не заедет в момент появления. Считается по повёрнутому габариту, так что наклонная строка тоже остаётся в кадре. Раскладки, которые нарочно уходят за край, это поле игнорируют.
Эффекты — вспышки, тряска, зум-панч, RGB-сдвиг, инверсия, зерно, скан-линии, виньетка. Каждый — отдельная величина от нуля до единицы. Пока стоит галочка «Эффекты по пресету», они следуют за вайбом; любое движение ползунка её снимает.
Синхронизация — сдвиг текста относительно звука и реакция на звук.
Управление
Под названием трека стоит ряд кнопок, дублирующий все горячие клавиши: спрятать интерфейс, перетасовать раскадровку, сдвинуть текст на ±0.1 с, пауза, полный экран. Плюс шестерёнка справа от названия открывает настройки. На телефоне это единственный способ до них добраться, поэтому кнопки есть всегда.
Когда интерфейс спрятан, вернуть его можно коротким тапом или щелчком по экрану. Долгое нажатие под это не попадает — оно занято другим.
Клавиши
H |
спрятать интерфейс |
S |
настройки |
F |
полный экран |
R |
перетасовать раскадровку |
[ ] |
сдвиг текста на ±0.1 с |
Пробел |
пауза |
Esc |
закрыть панель или карточку |
Есть и кое-что ещё, если поставить шрифт Tiny5 и подержать пять секунд левую
кнопку мыши или палец на экране. Там стрелки и WASD, включая русскую раскладку,
а на телефоне управление перетаскиванием: сердце ходит за движением пальца, а не
прыгает под него. Выход по Esc или по крестику в углу. Пока идёт игра, клавиши
интерфейса отключены, чтобы S не открывал настройки вместо шага вниз.
Шрифты
В комплекте идут четыре гарнитуры под лицензией SIL Open Font License — Stalinist One, Unbounded, Oswald, JetBrains Mono и Tiny5. Все с кириллицей.
BBH Bartle и Druk распространять нельзя, поэтому их нет в репозитории. Положите свои файлы сюда, и они сами появятся в списке шрифтов:
core/fonts/user/BBHBartle.woff2 (или .woff / .ttf / .otf)
core/fonts/user/Druk.woff2
Как это устроено
Перспектива. core/js/powerpin.js считает гомографию, которая переносит
четыре угла элемента в четыре произвольные точки, и отдаёт её как CSS
matrix3d. Это ровно то, что делает CC Power Pin. Углы задаются не свободно, а
через коэффициенты сжатия рёбер — так трапеция остаётся выпуклой, а точка схода
не подходит вплотную к строке. Без этого ограничения длинная строка схлопывается
в нечитаемую ленту.
Раскладки. core/js/layouts.js — семь способов поставить строку: крупно по
центру, с уходом за край, по диагонали, с сильной перспективой, субтитровым
пролётом, вразброс и стопкой. Выбор взвешенный и зависит от пресета, повтор
подряд подавляется. Всё детерминировано от идентификатора трека, так что одна и
та же песня всегда режется одинаково, а R меняет зерно.
Часы. Позиция от ОС приходит раз в 250–300 мс и дёргается. core/js/timing.js
держит собственные часы на performance.now() и подтягивает их к каждому отчёту,
а прыжок больше 0.6 с считает перемоткой. Плееры, которые не публикуют позицию
вовсе, тоже работают: в отчёте есть метка времени, от которой позиция
экстраполируется.
Отрисовка. Никаких CSS-анимаций — все трансформации переписываются каждый кадр от позиции в треке. Текст всегда ровно там, где песня.
Ограничения
- Определение трека на десктопе — только Windows. Ядро при этом кроссплатформенное: на macOS и Linux остаётся ручной ввод и локальные файлы.
- VLC 3 не отдаёт медиа-сеанс — только через его веб-интерфейс, см. выше.
- Реакция на звук работает на десктопе (петля системного звука через Electron). На Android обычному приложению системный звук не отдают, поэтому там визуал идёт только по часам текста.
- Точность позиции зависит от плеера. Некоторые публикуют её редко или не публикуют вовсе — на такой случай есть ползунок сдвига.