Кодируя идеи: создание саундтрека к Megademica (часть II)

Subliminal Extacy #07

Кодируя идеи: создание саундтрека к Megademica — Часть II

И вот мы подошли к самой сути задачи. Одно дело — описать все эти сущности (ритмы, мелодии, лады) изолированно друг от друга, но в итоге их всё равно требуется связать воедино на временной шкале, указав, какие элементы и в какой момент должны вступать. Для 8-минутного саундтрека это самая «дорогая» с точки зрения объёма задумка. Я решил разбить таймлайн на 256 тактов, каждый по 16 строк (что идеально ложилось на 16-битное значение ритмического паттерна, о котором шла речь выше) и со скоростью воспроизведения 6 прерываний на строку — то есть всего 24576 фреймов, или 491,5 секунды.

Это означало наличие списка из 256 элементов, каждый из которых фиксирует всё происходящее в текущем такте. По большей части это должны были быть указатели на переиспользуемые паттерны, объявленные в других блоках данных. Но учитывая жесткий лимит в 2 КБ на весь плеер вместе с музыкой, на один такт приходилось буквально считанное число байтов. В конце концов я свёл всё к трём байтам.

Первый байт — это указатель на мелодические данные. Они хранятся единым непрерывным потоком, включающим все используемые в треке ноты, упакованные по две ноты на байт; указатель задаёт смещение, с которого следует начинать считывание для конкретного такта.

Это давало нам максимум 256 нот, или 128 байт — из которых я задействовал 68.

Второй байт кодирует ритмический рисунок и музыкальный лад такта: старшие пять бит выбирают один из 32 ритмов (например, %1001001010010010 для риффа «ТАМ-там-там ТАМ-там-там»), а младшие три бита определяют один из 8 ладов. В итоге мне понадобилось всего 9 уникальных ритмов (18 байт) и 5 разных ладов (по 8 байт каждый, то есть 40 байт суммарно).

Третий и последний байт отвечает за ударные и басовую партию. Из-за нехватки памяти мы наложили ограничение: бас обязан звучать на одной и той же высоте на протяжении всего такта. Четыре бита байта выбирают конкретную ноту из 16 возможных. Для баса мы применили классический трюк чипа AY: синтез звука из смеси прямоугольного тона и треугольной/пилообразной волны огибающей генератора на специально подобранной скорости. Соответственно, каждая басовая нота задаётся четырьмя байтами параметров: старшим и младшим байтом периода тона, типом огибающей и её скоростью. Всего я использовал 14 таких басовых нот, на что ушло 56 байт параметров.

Остальные четыре бита третьего байта выбирают один из 16 комбинированных паттернов баса и ударных.

Здесь снова используется плотная упаковка: четыре бита на строку. 16-строчный паттерн может выглядеть следующим образом:

c8 3a d0 b0 98 ba d8 30

При разборе старший бит каждого полубайта (11 01 10 10 11 11 11 00) указывает, в каких строках звучит наш бас, а младшие 3 бита (40 32 50 30 10 32 50 30) задают партию ударных. Всего запрограммировано пять звуков ударных (0 — тишина):

  • 1 = бас-бочка (bass drum)
  • 2 = тихая бас-бочка
  • 3 = хай-хэт (hi-hat)
  • 4 = крэш (crash)
  • 5 = малый барабан (snare)

В обычном трекерном модуле каждый сэмпл представлял бы собой массив из 32 значений громкости, шума и высоты тона, описывающих их изменение во времени, и занимал бы порядка 100 байт. Но снова зададимся вопросом: какие идеи заложены в эти данные? В 99% случаев все эти параметры меняются линейно, и наши сэмплы — не исключение. Любой из них целиком укладывается всего в пять байт: начальная громкость, скорость затухания, маска микшера (включение тона/шума), скорость изменения шума и скорость изменения высоты тона.

Из 16 доступных барабанно-басовых паттернов я использовал 13, по 8 байт каждый. Вот и вся музыка — 8-минутный саундтрек занял около 1200 байт!

Чтобы облегчить работу упаковщику, глобальная последовательность тактов хранилась не группами по три байта на такт, а в виде трёх раздельных таблиц по 256 байт.

Благодаря этому, если у нас шли четыре такта с разными мелодиями, но одинаковым ритмом, басом и ударными, два из трёх байтов для этих тактов повторялись, и компрессор видел длинные цепочки одинаковых байтов. Это позволило нам легко уложиться в рамки 700–800 байт после упаковки.

Отдельно стоит отметить то, чего здесь вообще нет. В частности, полностью отсутствует управление динамикой мелодии: всё играется одним и тем же инструментом на постоянной громкости. Чтобы разнообразить звучание, добавлен эффект эха: в строках, где нет основной ноты, на пониженной громкости звучит нота, игравшая две строки назад. В обычном трекере это пришлось бы вбивать вручную, расставляя тихие ноты по паттернам — мало того, что это адски нудно, так ещё и раздувает данные на сотни байтов! Сама же идея эха элементарна: я только что сформулировал её одним предложением, а реализация в коде заняла считанные байты. Эффективное выражение идей — это не только форматы данных, но и умение вовремя понять, когда замысел проще решить кодом, а не таблицами.

Один из уроков, извлечённых из Haiku (моей предыдущей 4K-интро, где я тоже по полной выложился с кастомным плеером), заключался в том, что ни в коем случае нельзя писать музыку сразу в таком сжатом виде. В тот раз вся композиция была продумана заранее, и процесс сводился к простой транскрипции. Но это вышло мне боком: когда вся задуманная демка была готова, остался ещё целый свободный килобайт памяти. Я с удовольствием добавил бы ещё пару сцен, но расширение музыки потребовало бы перекраивать всю готовую структуру данных. К тому же в Haiku и близко не было такого нагромождения «таблиц, ссылающихся на другие таблицы», как в Megademica — ручная сборка подобного трека гарантированно свела бы меня с ума.

На этот раз я написал вспомогательный скрипт на Python.

По трекерным меркам инструментарий был предельно аскетичным, однако он позволял выстраивать композицию как массив объектов-паттернов со всеми удобствами высокоуровневого языка: давать осмысленные имена переменным для разных музыкальных частей и склеивать их в итоговую последовательность. Каждый паттерн записывался в текстовой нотации, очень похожей на трекерную. Затем скрипт брал этот исходник, извлекал ритмы, последовательности нот и лады, находил совпадения с уже встреченными паттернами и генерировал компактные бинарные данные.

После этого оставалась «сущая мелочь» — написать трек, который выжмет максимум выразительности из этого алгоритма при минимуме уникальных кирпичиков. В начале звучат несколько четырёхтактовых тем — чаще всего просто «голая» мелодия. Затем она повторяется поверх монотонного баса-дрона, следом подключается простой бит, далее мелодия дублируется октавой выше (за счёт проигрывания тех же мелодических данных по другому ладу), и наконец вступает напористый барабанный ритм. Во второй части применён иной приём: мелодические мотивы специально подобраны так, чтобы гармонировать с самыми разными басовыми нотами, раскрываясь каждый раз в новом ключе. Бас задаёт развитие всей композиции, маскируя цикличность мелодии.

Результатом я остался чрезвычайно доволен. И эти принципы вы можете применять в любых задачах и масштабах. Чем бы вы ни занимались — расстановкой нот, рисованием пикселей или написанием ассемблерных опкодов, — всегда спрашивайте себя: какие фундаментальные идеи лежат в основе этой работы? Возможно, вы найдёте гораздо более изящный способ их представления и отлично сэкономите на размере файла.

Gasman

Поделитесь вашим мнением о статье