Как это устроено
Перевод устарел. Английский оригинал изменился 2026-10-01: перевод сделан с версии
1ad9c6f8, сейчас актуальна2ad353b. Расхождение не проверено построчно — если что-то важно, откройте английскую страницу.
Что происходит между тем, как звук выходит из колонок, и тем, как на экране сдвигается фигура. Написано для тех, кто хочет разобраться в устройстве, а не менять его: ничего отсюда не нужно знать, чтобы пользоваться приложением или писать пресет.
Два фронтенда, один мозг
Ritmolux — это один движок отрисовки, питать который можно двумя способами. Отдельное приложение перехватывает то, что играет на машине; компоненту для foobar2000 сэмплы передаёт проигрыватель, внутри которого он работает. Ни то ни другое до движка не доходит: он принимает поток PCM-кадров и не знает, откуда они взялись. Эта единственная абстракция и позволяет одному и тому же визуальному коду обслуживать оба фронтенда.
Шов между звуком и картинкой — это кольцевой буфер без блокировок. Звук приходит в темпе звуковой карты, кадры рисуются в темпе дисплея, и ни один цикл не ведёт другой: сторона захвата пишет и немедленно возвращается, сторона отрисовки читает то, что есть. Это не оптимизация. Если обратный вызов захвата на чём-то задерживается — на блокировке, выделении памяти, записи в журнал, — в звуке слышен щелчок. Отсюда и правило: он не делает ничего из этого и отдаёт сэмплы за постоянное время. Контракт выписан в Ring determinism.
Встроить движок во что-то своё — это та же картинка, где вместо двух фронтендов стоит ваш код; Embedding the core проходит этот путь, а контракт C ABI — то, чем этот шов держится.
Что происходит за кадр
Сэмплы приходят
Фронтенд передаёт чередующийся PCM с той частотой, на которой работает устройство. Частота дискретизации, число каналов и размер буфера проверяются один раз, там, где звук входит в движок; дальше по потоку их уже считают верными — горячий путь, который занимается перепроверкой, занят не тем.
Спектр
Анализ идёт по шагу — фиксированному шагу по потоку, 512 сэмплов при 48 кГц, — а не по кадрам, поэтому то, что читает пресет, не зависит от частоты обновления вашего дисплея.
Окон работает два, а не одно. Окно в 2048 сэмплов кормит всё, что чувствительно ко времени: атаку, долю, темп, а также средние и высокие полосы. Окно в 8192 сэмпла кормит только полосы ниже примерно 246 Гц, потому что бин шириной 23,4 Гц не отличит бочку от басовой ноты. Эта цена признана и ничем не компенсируется: длинное окно задерживает нижние полосы примерно на 85 мс. Так устроено окно такой длины, и с этим мирятся, а не борются — путь от доли до реакции его не касается вовсе, так что бюджет задержки в 60 мс не страдает.
Уровни, и почему это отношения
bass, mid, treb и onset каждый делится на свой собственный медленно спадающий текущий
пик, с порогом тишины, чтобы тихая комната читалась как 0, а не как усиленный шум. Поэтому
bass > 0.5 означает «громко для этого трека» на любом треке и при любой громкости — именно это и
делает порог, записанный в пресет, переносимым.
Цена намеренная: абсолютная динамика скрыта. И тихий отрывок, и громкий доходят до 1.0 каждый по
своему пику. Там, где пресету действительно нужна абсолютная величина, её несут bass_raw и три
его собрата — ненормированные и маленькие.
Доля и такт
Обнаружение атаки — это спектральный поток: насколько спектр изменился с прошлого шага. Он даёт всплеск на атаках, а не на громкости, поэтому тянущаяся нота его не запускает, а приглушённый удар — запускает.
Трекер темпа превращает эти атаки в оценку BPM и фазу доли — 0 на каждой доле, нарастающая до
1 перед следующей. Над этим стоят часы такта: на какой доле такта вы находитесь, насколько такт
пройден, сколько тактов прошло. Атаки — это не доли. Детектор срабатывает от 1,2 до 2,3 раза за
долю в зависимости от материала — поэтому beat_index это храповик, а не счётчик, и поэтому
bar_phase существует отдельно.
Переменные и выражения
Всё это приходит в пресет набором из примерно двух десятков переменных только для чтения — четыре уровня, их сырые двойники, ворота доли, часы, темп плюс несколько таких, что несут положение, а не звук. Пресет привязывает именованный параметр системы отрисовки к короткому выражению над ними:
[params]warp = "0.4 + bass * 0.6"hue = "time * 0.05 + onset * 0.2"Эти выражения компилируются один раз при загрузке пресета и вычисляются раз в кадр. Грамматика,
функции, select() и то, что сообщается о неверном выражении, описаны в
языке выражений, а каждый параметр каждой системы есть в
справочнике параметров.
Сцена и цепочка за ней
Пресет называет одну систему отрисовки — рой частиц, параметрическую кривую, фрагментное поле, решётку реакции-диффузии и так далее, — и эта система рисует кадр. Прямо на экран она не рисует никогда. Каждая сцена проходит через общую составную цепочку:
фон → сцена → следы → калейдоскоп → переход → тон → показ
Предварительный проход фона, сама сцена, следы обратной связи, хранящие затухающую копию прошлых кадров, экранный калейдоскопический сгиб, переход на два входа, который смешивает уходящий пресет с приходящим, и завершающее тональное отображение. Новой системе отрисовки всё это достаётся без дополнительной работы — включая переходы в любой другой пресет и обратно.
Стадии — такие же параметры, как все прочие, поэтому пресет настраивает их по имени. Каталог техник — тот обзор, из которого вышли семейства сцен.
Показ
Кадр попадает на дисплей через графическую абстракцию — Metal на macOS, DX12 или Vulkan на Windows. Ни одна сцена не знает, какая именно; в этом и смысл писать под одну абстракцию, и это основополагающее решение.
Уровни качества
Движок рисует на одном из двух уровней. rich — это полная цепочка в полном внутреннем разрешении;
floor — та же цепочка, рассчитанная на встроенную видеокарту. Если уровень не закреплён,
приложение стартует на rich, а регулятор времени кадра один раз понижает его, если бюджет
кадра дисплея не выдерживается; закреплённый уровень не понижается никогда. Чем каждый уровень
держат численно — в Non-functional requirements, а как
закрепить — в Configuration.
Внутренняя цель отрисовки — это разрешение, а не форма: она квантуется и ограничивается сверху, поэтому её соотношение сторон не равно оконному. Всё, что считает геометрию, идущую на экран, берёт соотношение сторон у окна — поэтому пресет выглядит одинаково на ноутбуке 16:10 и на проекторе 16:9.
Что детерминировано, а что нет
Анализ — чистая функция от поданных ему сэмплов. Один и тот же звук даёт один и тот же спектр, ту же огибающую атак и ту же оценку доли, в каждом прогоне и на любой машине: внутри него нет ни чтения системных часов, ни случайности с незаданным зерном. Именно поэтому безоконная оснастка захвата вообще может что-то утверждать о картинке.
Визуальная случайность никуда не делась — разлёт частиц, дрожание, — но её зерно задано явно,
поэтому сцена воспроизводится в точности. Что не детерминировано, так это темп кадров: сколько
кадров ляжет между двумя шагами анализа, зависит от вашего дисплея и вашей видеокарты, а пресет,
который понадеялся на обратное, выглядел бы иначе на мониторе 144 Гц. Поэтому time — это часы сцены в секундах, а не
счётчик кадров.
Built from 13c7582 at version 0.158.0. This site tracks main and is not versioned per release.