О том, как писался саундтрек Megademica (часть I)

Subliminal Extacy #07

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

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

Для подобных ситуаций мне помогает взгляд на количество байтов как на количество отдельных идей или проектных решений, которые вы можете включить. «Робот синевато-серебристого цвета» — это проектное решение на один байт (при условии, что существует 256 осмысленно различающихся цветов); «анимация ходьбы для левой ноги робота — это зеркальное отражение правой ноги, сдвинутое на 12 кадров» — может быть... байтов на 10? Это не точная наука, и, очевидно, все зависит от уровня знаний, которые вы получаете «бесплатно» вместе с платформой, на которой работаете. «Закрашенный треугольник с координатами v1, v2, v3» — это идея меньше чем на 10 байтов, если ваша платформа имеет встроенную поддержку рисования закрашенных треугольников, и гораздо больше, если вам приходится реализовывать это с нуля.

Кто-то, наблюдавший за прогрессом сжатия видео в последние 20–30 лет, мог задаться вопросом: «Может ли когда-нибудь появиться алгоритм, достаточно продвинутый, чтобы уместить "Гражданина Кейна" на дискету с приемлемым качеством?». Этот вопрос приобретает новый смысл в эпоху генеративного ИИ, способного превратить текстовую подсказку в видео. Мы можем представить идеальный алгоритм декомпрессии как такой, который принимает на вход поток всех творческих решений, принятых Орсоном Уэллсом — возможно, в текстовой форме или в каком-то другом машиночитаемом кодировании — и выводит видео, следующее этим спецификациям. Этот ввод включал бы детали вроде снежного шара, выпадающего из руки Кейна, и конкретную компоновку кадра, где он разбивается о землю, но, вероятно, не точные траектории каждого осколка стекла. Сколько идей составляет весь фильм? Возможно, сотни тысяч — вероятно, меньше миллиона.

На гораздо более приземленном уровне этот тип мышления стал ключом к оптимизации приглашения на Lovebyte 2021 для TIC-80, над которым я работал вместе с g0blinish и Hellmood, и которое было сосредоточено вокруг развевающегося логотипа Lovebyte. Растровая графика для этого логотипа занимала основную часть упакованного размера интро, и попытки уменьшить его с помощью таких подходов, как RLE-сжатие, ни к чему не привели. В самом деле, это понятно — список из «6 белых пикселей, 20 черных пикселей, 10 белых пикселей...» очень далек от набора человеческих проектных решений, которые были вложены в этот логотип. Поскольку у TIC-80 не было доступа к конкретному шрифту, использованному в логотипе, стало ясно, что наиболее близкой аппроксимацией к этому набору идей будет последовательность движений пера, из которых состояли буквы — иными словами, перерисовка их с помощью операторов 'tri' и 'rect'. Это была пара часов кропотливой работы, но я был уверен, что она окупится, учитывая, насколько ближе это было к идеальному представлению.

Так как же это применимо к музыке? Обычно на демосцене мы берем за основу трекерную музыку — сетку, по которой мы движемся с фиксированной скоростью, с событиями, размещенными на этой сетке, чтобы указать, какие ноты играть в какое время. Но это не передает музыкальные идеи больше, чем сетка пикселей передает движения пера. Музыкант мыслит ритмами, гаммами, аккордовыми прогрессиями — можем ли мы запечатлеть эти идеи напрямую, а не результаты их применения?

У меня была возможность применить это на практике, когда SerzhSoft обратился ко мне с просьбой написать музыку для его 4K-интро Megademica. Это была непростая задача с самого начала — визуал полностью использовал 8-минутный лимит времени вечеринки Revision, оставляя 700–800 байтов от упакованного размера интро на саундтрек, который должен был заполнить это время, соответствуя разнообразию визуальных эффектов.

Дополнительным осложнением стало жесткое ограничение в 2 килобайта на распакованный размер подпрограммы и данных, поскольку визуал занимал почти всю память Spectrum. В конце концов, повторение — это еще один вид идеи, который проявляется на многих уровнях в музыке, будь то микроуровень барабанных ударов или макроуровень создания мелодических мотивов и возвращения к ним в вариациях. Если упакованный размер файла — ваша единственная забота, вы можете сделать поток данных настолько повторяющимся, насколько хотите, и положиться на упаковщик, который поглотит эту избыточность, так что идея вроде «повтори эту фразу 16 раз» естественным образом превратится в однобайтовую идею без каких-либо усилий. Однако, если структура данных, с которой вы работаете во время воспроизведения, также ограничена по размеру, у вас нет такой роскоши — вам придется встроить идею повторения в свой дизайн.

Итак, переходя к началу саундтрека Megademica, это четырехтактовый рифф с ритмом, который звучит как DAN-dan-dan DAN-dan-dan. В трекере эти четыре такта были бы одним паттерном длиной 64 строки. Это 64 байта, может быть, немного больше, если вы включаете информацию вроде сэмплов и эффектов, или немного меньше, если вы хитро закодируете пустое пространство. Но что, если мы выделим этот ритм «DAN-dan-dan DAN-dan-dan» как самостоятельную идею? Это 16-битное значение %1001001010010010, где биты «1» указывают на моменты, когда мы хотим сыграть ноту. Этот ритм сам по себе является двухбайтовой идеей.

У нас есть четыре такта этого шестинотного ритма DAN-dan-dan DAN-dan-dan, играющих разные высоты тона в каждом такте, что дает в сумме 24 высоты нот, которые мы должны как-то сохранить. Учитывая, что на клавиатуре пианино 88 клавиш, было бы не слишком натянуто хранить их по одному байту на ноту — и вместе с нашими двумя байтами информации о ритме эти 26 байтов уже являются большим улучшением по сравнению с 64, которые мы бы потратили, сохраняя это в формате трекерной сетки. Впрочем, мы можем сделать лучше — в конце концов, эти 88 нот на клавиатуре пианино не все одинаково вероятны. Мелодия не собирается прыгать на несколько октав от одной ноты к другой, и, вероятно, будет придерживаться нот данной музыкальной гаммы. На самом деле, как только вы установили, что это за гамма, я бы сказал, что для любой ноты есть около 16 правдоподобных вариантов, и вы, вероятно, понимаете, к чему я веду с этой цифрой.

Для музыканта работа в рамках музыкальной гаммы — это эквивалент работы пиксель-артиста с фиксированной палитрой. Это ограничение выбора, который вы можете сделать, и, следовательно, меньше информации для хранения — но это ограничение, которому вы, вероятно, и так были бы рады следовать. Если мы заранее решим, что будем придерживаться нот A, A#, C#, D, D#, E, F, G, то можем потратить 8 байтов на определение этой музыкальной гаммы, и с этого момента каждая нота вашей мелодии может быть закодирована тремя битами. В моем случае я решил использовать четвертый бит для обозначения сдвига вверх на октаву, что дало мне 16 нот в двух октавах для игры.

Хорошо подобранная гамма — это настолько мощная концентрация музыкальной сути в очень немногих байтах, что иногда это может быть всем, что вам нужно для вашего саундтрека. Некоторые интро, такие как Mipmap от Ate Bit и мой собственный Spectrumori-on, используют тот факт, что с подходящим образом ограниченной гаммой даже случайные данные могут звучать музыкально. Если ваше интро требует музыкальной текстуры, а не конкретной мелодии, то отдельные ноты могут быть такими же маловажными, как осколки стекла в снежном шаре «Гражданина Кейна», поэтому нет необходимости тратить на них свой бюджет идейных байтов. Ваш человеческий вклад в эту мелодию может быть равен нулю или просто указывать на адрес в ПЗУ, который случайно звучит приятно.

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

Продолжение следует в части II...

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