Сегодня — экстремальный geek out: максимально узкоспециальная тема с запутанным кодом на ассемблере Z80. Раскроем секреты Тима Фоллина в «биперной» музыке на Sinclair ZX Spectrum 48K, попытаемся повторить, а может быть и превзойти его достижения. Некогда объяснять, разберёмся по ходу кода!

▍ Контекст


Есть такой популярный британский домашний компьютер 1982 года выпуска — Sinclair ZX Spectrum. Его аппаратные клоны и игры были очень популярны в странах бывшего СССР и других развивающихся странах (от Польши до Бразилии), довольно значительным образом повлияв на компьютеризацию населения в этой местности. С тех пор по всему миру осталось множество бывших пользователей, ностальгирующих по этой платформе, её играм, графике и даже музыке.

Оригинальный ZX Spectrum 48K (фото Nico Kaiser)

У оригинальной модели ZX Spectrum нет вообще никакого аппаратного синтезатора звука, или так называемого звукового чипа. У него есть только бипер — простейший динамик, подключённый непосредственно к однобитному порту вывода. Но есть игры, и какие-то звуки и музыка в этих играх есть. В некоторых даже настолько неплохие, что смогли запасть в душу игрокам, и оставаться любимыми даже спустя 30 с лишним лет.

Вместо звукового чипа, звук на ZX Spectrum генерируется полностью программно, кодом, написанным на ассемблере его центрального процессора Zilog Z80, с точным расчётом времени выполнения, буквально по тактам — так называемыми биперными движками. Несмотря на примитивность этой технологии, аналогичной использовавшейся в первых экспериментах по компьютерному синтезу звука в 1950-х на компьютерах типа TX-0 и PDP-1, талант энтузиастов позволил достичь интересных результатов.

Плата ZX Spectrum. Справа внизу виден динамик

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

И хотя платформа ZX Spectrum давно перестала быть массовой, энтузиасты остались — теперь это ретрокомпьютинг. В этой среде в начале 2000-х годов возник новый интерес к биперу, появились новые разработки. Ведь даже по прошествии многих лет кому-то всё ещё не давали покоя лавры Тима нашего Фоллина. Кому-то — это, например, мне.

▍ Импульсный синтез


Один из самых популярных и любимых слушателями способов сверх-минималистичного синтеза многоканального звука — метод сверх-коротких импульсов, так называемых «иголок» (pin), или «импульсный» синтез.

Именно он придаёт то самое звучание музыкальным композициям Тима Фоллина (игры Agent-X, Chronos, Vectron), Кейта Тинмана (Cabal, Firefly, Midnight Resistance), Дэвида Уиттакера (ATV Simulator, Beyond the Ice Palace, Trantor). Практически концентрированная однобитная звуковая ностальгия для тех, кто застал ZX Spectrum и игры на кассетах.

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

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

Достоверно неизвестно, кто придумал и впервые реализовал импульсный синтез, но его корни прослеживаются до Music System 1977 года от Processor Software для компьютера Sol-20 и похожих Альтаир-совместимых машин с шиной S-100. Эта система позволяла программно синтезировать трёхголосую полифонию с тем самым характерным звучанием, но ещё без плавного затухания.

К слову, позже эта программа под названием Музыкальная Система была адаптирована для советских компьютеров на базе микропроцессора КР580ВМ80, типа Радио 86РК, и, видимо, именно это стало причиной такого странного подключения динамика: к сигналу разрешения прерывания (такое решение было на компьютерах с шиной S-100).

Но действительно широкое распространение импульсный синтез получил во времена коммерческих игр для ZX Spectrum и Apple II, в 1984-1989 годах. На ZX Spectrum этот звук можно услышать в великом множестве игр, в частности, в играх Code Masters и SpecialFX Software, а на Apple II нечто подобное звучит в оригинальном Prince of Persia и Factor Seven (со звуковыми процедурами Кайла Фримена).

Ну а самым главным мастером этого дела стал Тим Фоллин — легенда чиптюна, автор невероятной музыки в огромном количестве игр на множестве 8 и 16-битных платформ. Начав заниматься компьютерным звуком в 15 лет, свои первые успехи он совершал на ZX Spectrum, поражая воображение слушателей иллюзией крутейшей прог-роковой музыкальной композиции, скрытой в вое пылесоса, раздающегося из маленького динамика.


Тим сам разрабатывал код биперных движков и потом сам же писал для них музыку, поэтому он смог выйти за рамки возможного, и повысить полифонию сначала до трёх каналов (Vectron), потом до четырёх (Future Games), и даже до пяти (Chronos). Это было достигнуто ценой серьёзных компромиссов, но до сих пор его работы являются своего рода Святым Граалем среди любителей биперной музыки: вершиной и технического, и композиторского мастерства.

▍ Как это работает


Как известно, для генерации простейшей «квадратной» звуковой волны, так называемого меандра, нужно менять уровень выходного сигнала дважды за его период, через равные промежутки времени. Длительность периода определяет частоту получаемого звука.

Классическая квадратная волна, две разные частоты и две разные скважности

Если промежутки времени внутри периода становятся неравными, изменяется скважность и спектр гармоник. На слух это воспринимается как более «тонкий» звук, чем больше скважность, тем тоньше и тише. Это качество полезно для музыкальных целей, чтобы разнообразить звучание, и часто применяется в звуковых синтезаторах. Например, в игровой консоли Famicom (Денди, NES) реализовано три скважности: 50%, 25% и 12.5%, а в звуковом синтезаторе компьютера Commodore 64 — аж 4096 градаций скважности, что позволяет создавать замечательные модуляционные эффекты.

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

ШИМ разной скважности, проценты соответствуют количеству передаваемой потребителю энергии

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

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

Импульсный синтез: разные частоты, одинаковая «громкость»

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

Фрагмент трека из игры Agent X. Указатель установлен на ударный инструмент — это буквально один период волны низкой частоты

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

▍ Реализация на Z80


Тим Фоллин лично делится своими секретами на страницах журнала Your Sinclair в номере за август 1987 года

Типичная реализация импульсного синтеза на микропроцессоре Z80 такова.
Есть основной цикл генерации звука, имеющий фиксированное время выполнения — оно определяет базовую частоту.

Есть набор 8-битные счётчиков-делителей, по числу канала. Например, два. Каждую итерацию основного цикла каждый счётчик декрементируется. Если счётчик достиг нуля, он заново загружается значением, соответствующим генерируемой частоте, а также генерируется очень короткий импульс-иголка.

Таким образом счётчики делят базовую частоту на значение от 1 до 255, получая 255 возможных частот, производных от базовой.

В псевдокоде это выглядит так:

цикл:

   счётчик1--
   если счётчик1 == 0
      счётчик1 = N
      генерация импульса1

   счётчик2--
   если счётчик2 == 0
      счётчик2 = N
       генерация импульса2

   переход на цикл

Длительность генерируемого переполнением счётчиков импульса дольше, чем время основного цикла. Сам импульс состоит из двух частей: сначала порт бипера устанавливается в 1, проходит некоторое время, от которого зависит ощущаемая громкость канала, потом порт устанавливается в 0, и проходит оставшееся время импульса. Это необходимо для того, чтобы псевдо-громкость не влияла на генерируемую частоту.

В псевдокоде генерация импульса выглядит следующим образом:

длительность1 = текущая громкость канала
длительность2 = максимальная громкость - текущая громкость канала

бипер = 1

while длительность1 > 0
   длительность 1--

бипер = 0

while длительность2 > 0
   длительность 2--

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

Опуская несущественные детали, реализация счётчика и выдачи импульса Z80 для одного канала на ассемблере выглядит примерно так:

;счётчик канала

    dec d			;декремент счётчика
    jp nz,...		;пропуск ветки, если переполнения не случилось
    ld d,_F			;перезагрузка счётчика

;генерация импульса

    ld a,_D1		;время первой задержки (неактивная часть импульса)
.delay1
    dec a			;цикл первой задержки
    jr nz,.delay1		;переход на цикл
    ld a,_O			;16 для активного канала, 0 для полного заглушения
    out (#fe),a		;вывод в порт
    ld a,_D2		;время второй задержки (активная часть импульса)
.delay2
    dec a			;цикл второй задержки
    jrt nz,.delay2		;переход на цикл
    out (#fe),a		;после цикла A=0, вывод в порт

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

У этой реализации есть несколько проблем.

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

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

Многоканальные же движки Тима Фоллина были настолько подвержены кросс-расстройке каналов, что каждое сочетание нот в треке требовало подбора каждого из делителей, чтобы попасть в правильные ноты. И именно это, помимо прочих нетривиальных ограничений, не позволило до сих пор создать удобный редактор для классических движков Фоллина. Сам же автор делал это вручную, набирая данные музыки прямо в ассемблерном коде и на слух, подбирая нужные компенсации частот.

▍ Бипер, я и QChan


В 2009 году, вдохновившись творчеством небезызвестного Mister BEEP’а, я плотно заинтересовался темой биперной музыки. Как известно, ограничения провоцируют креативность, и этот крайне специфический формат неожиданным образом позволил мне найти оптимальный баланс между технической и музыкальной составляющими в собственном творчестве.

Удобных инструментов для создания биперной музыки тогда не было, и поначалу я перерабатывал уже существующие музыкальные редакторы прошлых лет под более современный трекерный интерфейс. К 2010 году, накопив некоторый опыт, я начал изобретать и свои собственные движки. Я пробовал различные подходы и искал новые идеи, которые позволили бы реализовать больше каналов, более чистое и стабильное звучание, чем было достигнуто в разработках прошлых лет.

Мне сходу удалось создать крайне удачный движок Phaser1, полюбившийся многим чиптюн-композиторам, а годом позже ещё два популярных движка — Tritone и Octode. Они основаны на других принципах, и в целом это уже совсем другая история. Главное, что в их тени осталась ещё одна разработка того периода.

Во время торфяных пожаров августа 2010 года в дикой жаре и натуральном Сайлент Хилле за окном в попытках улучшить идеи движка музыкального редактора Music Synth 48K (используется в игре Ano Gaia) родилась идея.

Сначала она была реализована в двухканальном движке Stocker — продвинутом аналоге Music Synth 48K, обладающем развитой системой огибающих и импортом нотного текста из Vortex Tracker.

Практически сразу же после этого я решил пойти дальше и посягнуть на лавры Тима нашего Фоллина. Так родился четырёхканальный QChan.

Stocker и QChan используют вариацию импульсного синтеза, отличающуюся другим набором компромиссов, и, как следствие, кардинально другой реализацией, имеющей свои преимущества и недостатки.

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

Счётчики и аккумуляторы — это два основных принципа организации частотных делителей в простых синтезаторах звука:

  • Счётчик — целочисленная переменная, которая декрементируется до обнуления и перезагружается заново. В момент перезагрузки инвертируется хранимое отдельно состояние выходного сигнала.
  • Аккумулятор — целочисленная переменная более высокой разрядности, к которой прибавляется некоторое константное значение, пока разрядность счётчика не переполнится. При такой схеме перезагрузка не требуется, а выходной сигнал можно просто брать из старшего бита (и не только, все старшие биты — готовая к употреблению скважность сигнала).

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

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

Недостатком получившегося на этих принципах нового движка стала очень большая длительность цикла, то есть низкая частота дискретизации, порядка 8.6 кГц, а также очень «тонкий» и довольно тихий звук, ещё тоньше и тише, чем у Фоллина. Одно связано с другим: увеличить громкость можно только удлинением импульса, но тогда вырастет время выполнения цикла, и диапазон воспроизводимых частот уйдёт из приемлемого на практике.

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

Но эти недостатки также стали и характерными особенностями движка, и как оказалось, специфичная для них звуковая окраска тоже имеет своё применение и привлекательность.

Разберём код основного цикла генерации звука, все четыре канала, без купюр:

soundLoop

    add ix,de	;аккумулятор первого канала
    sbc a,a		;трюк: превращаем признак переноса в значение 0 или 255
.vol0=$+1
    and 0		;накладываем громкость по AND и получаем 0 или громкость
    ld b,a		;в B хранится длительность импульса для текущей итерации цикла
    add hl,sp	;аккумулятор второго канала
    sbc a,a		;всё то же самое
.vol1=$+1
    and 0
    or b		;складываем длительности по OR
    ld b,a		;запоминаем обратно в B
    exx		;регистры кончились, включаем альтернативный набор
    add iy,de	;аккумулятор третьего канала
    sbc a,a
.vol2=$+1
    and 0
    exx		;возвращаем набор, чтобы сохранить длительность импульса
    or b
    ld b,a
    exx		;опять альтернативный набор
    add hl,bc	;аккумулятор четвёртого канала
    sbc a,a
.vol3=$+1
    and 0
    exx		;опять возвращаем основной набор
    or b		;накладываем длительность, это также обновляет флаг Z
    jr z,.noOut	;если длительность нулевая, пропускаем генерацию активной части импульса
    ld b,a		;загружаем счётчик цикла. гарантированно ненулевой
    ld a,16		;устанавливаем бит бипера
    out (#fe),a	;выводим в порт
    ld a,b		;возвращаем длительность A, она нужна дальше
    djnz $		;цикл задержки для активной части импульса
    cpl		;инвертируем A, чтобы получить длительность неактивной части импульса
.noOut
    add a,17	;прибавляем к предыдущей длительности 17. Если она была 0, станет 17, а если была ненулевой, в результате CPL получается вычитание
    ld b,a		;загружаем счётчик цикла, снова гарантированно ненулевой
    xor a		;сбрасываем бит бипера
    out (#fe),a	;выводим в порт
    djnz $		;цикл задержки для неактивной части импульса
    dec c		;счётчик итераций основного цикла
    jp nz,soundLoop	;выполняем основной цикл

Сразу становится ясно, почему канала четыре (и отсюда же название движка, Q от Quad) — на большее просто не хватило регистров. Каждый канал требует две 16-битные пары, которые можно складывать одной командой. Это пары IX и DE,HL и SP (указатель стека тоже задействован для счётчика), IY и DE’, HL’ и BC’. Оставшиеся три регистра, A, B и C, применяются для генерации импульса и организации цикла.

▍ Реновация


Несмотря на некоторые достоинства, движок QChan оказался довольно сложным для освоения инструментом, особенно на первых порах: тогда никаких редакторов ещё не было, и музыку для него нужно было писать в формате XM-модуля, а потом конвертировать прилагающейся утилитой для командной строки.

Со временем появилось ещё два способа создания музыки — поддержка в редакторе Beepola и поддержка в моём 1tracker. Но и более удобными инструментами нашлось не так уж много композиторов, способных раскрыть потенциал движка и обратить его особенности себе на пользу.

Главным образом это британец Рич Холлинс, известный как AtariTufty, который более десяти лет регулярно создаёт новые музыкальные композиции с использованием QChan. И хотя Tufty никогда не жаловался на нехватку возможностей, настало время открыть новые грани его творчества, обновив движок.

Опыт многих предыдущих разработок в этой области подсказал список действительно полезных на практике возможностей, которые стоило бы добавить:

  • Тонкая настройка частоты нот, для каждой в отдельности.
  • Не только спад, но и нарастание громкости.
  • Произвольное назначение громкостей и скорости спада или нарастания каждой ноте в отдельности.
  • Возможность старта ноты с перезапуском огибающей или без него, для каждой ноты в отдельности (режим легато).
  • Сэмплы для ударных разной длительности с компенсацией темпа.
  • Какой-никакой слайд тона для каналов.
  • Более тонкий контроль темпа.

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

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

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

За счёт этого хака удалось повысить скорость основного цикла, а значит, получить более точный контроль частоты генерируемого звука. Теперь в случае, когда импульса нет (большую часть времени) цикл занимает 357 тактов вместо прежних 404 тактов, что даёт частоту дискретизации уже 9.8 кГц вместо оригинальных 8.6 кГц, и это вполне может считаться хоть и небольшим, но улучшением. Пустячок, но приятно.

Можно было пойти дальше и сделать ещё хуже: добавить к только что достигнутым 357 тактам ещё 7+4 тактов пустых операций и довести время цикла до 368 тактов. И я попробовал это сделать. Дело в том, что в оригинальном ZX Spectrum из-за особенностей схемотехники вывод в порт бипера выравнивается на 8 тактов, и для некоторых биперных движков несоблюдение этой размерности приводит к сильным артефактам (алиасинг, вырождающийся в шум) в звуке. Эксперимент показал, что QChan не подвержен этой напасти: проверка не показала заметной на слух разницы, и данное ухудшение я фиксировать не стал.

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

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

Фрагмент теста QChan2. Видно два сэмпла ударных инструментов, прерывающих импульсы тональных каналов

У меня уже есть готовый универсальный код для подобной перкуссии, ранее применённый в движке Ear Shaver EX. Он уже обладает множеством полезных возможностей: грубая регулировка громкости, смещение начала сэмпла, и даже некоторое подобие фильтра, огрубляющего звучание. Его я и внедрил в новый движок. Ожидалась возможная проблема перекоса баланса громкости между довольно тихими каналами тона и громкими ударными, но удивительным образом баланс сразу совпал очень хорошо и не потребовал доработок.

Новую версию движка я решил назвать самым очевидным образом: QChan24.

▍ Формат


Новые возможности потребовали изобретения нового формата музыкальных данных.

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

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

Принцип синтеза звука в QChan более-менее располагает с относительно медленному парсингу. Но он всё же не настолько нетребовательный, как в более «громких» движках на принципе Squeeker и Squat (с гораздо более широкими импульсами), в которых можно проводить вне цикла генерации звука очень значительное время без сильного влияния на звучание.

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

Также я использовал организацию данных в 16-битных словах — практика, введённая utz в его мощнейших биперных разработках. У этого способа есть большое преимущество: можно читать данные трека стеком, устанавливая SP на начало нужных данных и получая байтовые пары операцией POP за 10 тактов (очень быстро). Таким образом читаются и ссылки на строки, и содержимое самих строк. В том числе можно читать пару AF, сразу занося значения в регистр флагов (у меня это свойство не задействовано, но в других случаях оно крайне полезно).

Строки паттерна состоят из последовательности от одного до нескольких 16-битных слов. Их количество зависит от содержания конкретной строки. Первая байтовая пара содержит длительность строки и флаги последующих данных.

Длительность строки — это количество «кадров» в ней. Оно зависит от настройки скорости трека, но также меняется в зависимости от звучащей в этой строке перкуссии: длительность строки компенсируется на длительность сэмпла, чтобы поддерживать стабильный темп.

Флаги — просто битовые поля, указывающие на наличие данных каждого из тональных каналов, слайда и перкуссии. Если бит установлен, далее в данных будут представлены соответствующие ему 16-битные слова с параметрами, в порядке следования флагов.

Если установлен флаг тонального канала, для него в данных присутствует следующий пакет из одного или двух слов.

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

Второе слово — настройки огибающей. Первый его байт содержит скорость огибающей в формате битовой маски (1,3,7,15,31,63,127,255). Второй байт содержит «громкость» 0..15 и два бита режима огибающей: 00 атака с нуля до заданной громкости, 01 спад с заданной громкости до нуля, 10 и 11 удержание заданной громкости.

Если установлен флаг слайда, в данных будет слово со знаковым значением направления и скорости слайда.

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

▍ Новый код


Разберём пару интересных моментов в коде нового движка.

Прежде всего, это новый цикл генерации звука тональных каналов, который стал проще прежнего. Главное его отличие в способе накопления длительности импульса при переполнении аккумуляторов канала. Также укорочена общая длительность неактивной части импульса за счёт применения честной смены знака числа (CPL:INC A).

soundFramePlay

   ;регистр A всегда установлен в 0 перед началом цикла и в конце цикла
   ;громкость (длина активной части импульса) теперь накапливается в нём, а не в B

    add ix,de		;аккумулятор первого канала
    jp nc,.noPulse0 	;переход происходит часто, JP быстрее, чем JR
.vol0=$+1
    or 0			;если произошло переполнение, накладываем громкость
.noPulse0
    add hl,sp 		;аккумулятор второго канала
    jp nc,.noPulse1
.vol1=$+1
    or 0
.noPulse1    
    exx			;альтернативные регистры
    add iy,de		;аккумулятор третьего канала
    jp nc,.noPulse2
.vol2=$+1
    or 0
.noPulse2
    add hl,bc		;аккумулятор четвёртого канала
    jp nc,.noPulse3
.vol3=$+1
    or 0
.noPulse3
    exx			;возвращаемся к нормальным регистрам
    or a 			;проверяем длительность импульса
    jp z,.noOut		;если нулевая, пропускаем активную часть
    ld b,a 			;загружаем счётчик цикла, гарантированно ненулевой
    ld a,16			;устанавливаем бит бипера
    out (#fe),a
    ld a,b			;восстанавливаем A
    djnz $			;цикл задержки для активной части импульса
    cpl			;меняем знак длительности
    inc a
.noOut
    add a,16		;считаем длительность неактивной части импульса
    ld b,a			;загружаем счётчик цикла, гарантированно ненулевой
    xor a			;сбрасываем бит бипера
    out (#fe),a
    djnz $			;цикл задержки для неактивной части импульса
    dec c			;счётчик итераций основного цикла
    jp nz,soundFramePlay 	;выполняем основной цикл

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

.ch0envMask=$+1
    and 0			;накладываем маску делителя на счётчик кадров по AND
    jr nz,.ch0eskip 	;пропускаем логику огибающей, если не 0
    ld hl,soundFramePlay.vol0	;указатель на байт с громкостью в основном цикле
    ld a,(hl)			;получаем байт
.ch0envVol=$+1
    cp 0			;сравниваем с целевой громкостью
    jr z,.ch0eskip		;если достигнута, пропускаем следующую операцию
.ch0envOp=$
    dec (hl)			;изменяем громкость
.ch0eskip

Скорость огибающей задаётся маской, которая складывается по AND с постоянно работающим счётчиком кадров. Шаг огибающей случается, когда полученное значение равно нулю. То есть при маске в 1 спад происходит каждые два кадра, при маске 3 — каждые четыре и так далее. Это хак для сокращения времени выполнения кода, обеспечивающий достаточную гибкость в выборе скорости огибающей.

Для обновления громкости огибающей используется самомодифицирующийся код. Внешней обвязкой проигрывателя подменяется граничное значение громкости в операции CP, а также устанавливается операция INC (HL), DEC (HL) или NOP для режима атаки, спада и удержания соответственно.

▍ Пятая нога


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

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

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

soundFramePlay

    ;A всегда равен 0

.add0=$+1
    ld de,0			;слагаемое для первого канала
    add ix,de		;аккумулятор первого канала
    jp nc,.noPulse0	;если нет переполнения, пропускаем
.vol0=$+1
    or 0			;накладываем громкость
.noPulse0
.add1=$+1
    ld de,0			;слагаемое для второго канала
    add hl,de		;аккумулятор второго канала
    jp nc,.noPulse1
.vol1=$+1
    or 0
.noPulse1    
    exx			;меняем набор регистров
.add2=$+1
    ld bc,0			;слагаемое для третьего канала
    add iy,bc		;аккумулятор третьего канала
    jp nc,.noPulse2
.vol2=$+1
    or 0
.noPulse2
.add3=$+1
    ld bc,0			;слагаемое для четвёртого канала
    add hl,bc		;аккумулятор четвёртого канала
    jp nc,.noPulse3
.vol3=$+1
    or 0
.noPulse3
    ex de,hl		;берём аккумулятор пятого канала из DE’
.add4=$+1
    ld bc,0			;слагаемое для пятого канала
    add hl,bc		;аккумулятор пятого канала
    jp nc,.noPulse4
.vol4=$+1
    or 0
.noPulse4
    ex de,hl		;возвращаем аккумулятор пятого канала в DE’
    exx			;возвращаем набор регистров

;далее генерация импульса как в четырёхканальной версии

    or a
    jp z,.noOut
   …

▍ Редактор


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

Сейчас мы уже не делаем конвертеры из XM и других форматов, которые тоже требуют определённых технических знаний для использования. По возможности новые движки получают поддержку в моём универсальном редакторе 1tracker, о котором я уже рассказывал на Хабре. Это не всегда простое дело, так как движки постоянно усложняются, и осталось ещё приличное количество разработок недавнего времени, у которых до сих пор такой поддержки нет, по тем или иным причинам — а значит, они остаются недоступными для музыкантов. Поэтому критически важно реализовать поддержку как можно раньше, иначе это получается работа «в стол».

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

Новые движки добавляются в трекер в виде скрипта на AngelScript, размещаемого в текстовом файле с расширением 1te, в котором реализовано несколько методов API редактора. Туда же включается исходник движка на ассемблере Z80 в виде простого текста, так что всегда можно оперативно подкрутить что-нибудь в его коде.

Методы скрипта конфигурируют интерфейс трекера, формируя нужные поля ввода для нот и прочих параметров. Перед проигрыванием скрипт получает данные от трекера и компилирует их в описанный выше формат, в виде директив ассемблера, добавляет исходник движка, и собирает встроенным (написанным на том же AngelScript) кросс-ассемблером Z80. Собранный бинарный файл проигрывается встроенным эмулятором. Скрипт также реализует экспорт данных наружу: ассемблерный исходник с данными музыки или собранные файлы популярных форматов (AY, TAP, SCL).

Что касается управления движком со стороны интерфейса трекера, я решил сделать всё максимально просто и понятно. Вместо настроек трека и системы инструментов, каждый из параметров нот задаётся непосредственно в треке. Хотя это увеличивает количество полей на экране, это всё же проще для понимания и удобнее для работы: всё сразу под рукой.

QChan24 в 1tracker

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

В каналах тона можно ввести следующие параметры:

  • Нота.
  • Расстройка частоты ноты, от 1 до 9.
  • Признак легато, пусто или 1.
  • Громкость, от 1 до 16 (от тихой до громкой).
  • Скорость огибающей, от 1 до 8 (от быстрой до медленной).
  • Режим огибающей, от 1 до 3 (атака, спад, удержание).

Расстройка и легато применяются только на ту ноту, для которой они указаны, а настройки огибающей распространяются на все последующие ноты в канале. У первого канала, поддерживающего слайд, есть ещё два дополнительных поля: скорость слайда вверх и вниз, от 1 до 9.

В канале перкуссии вводятся следующие параметры:

  • Номер сэмпла, от 1 до 99.
  • Громкости, от 1 до 4.
  • Фильтр, от 1 до 9 (от глухого звука до нормального).
  • Смещение начала сэмпла, от 1 до 9.

▍ Заключение


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

Скрипт движка пока не включён в релизную версию 1tracker, но его уже можно найти на тематическом форуме или в посте в моём личном блоге. Он будет включён в базовую поставку с ближайшим обновлением программы.

Работа же над новейшими достижениями в области древнейших технологий продолжается. Зачем? Потому что можем.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

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


  1. Zara6502
    18.09.2024 10:05
    +3

    Спасибо, ролик с утра посмотрел на ютубе. Не знал совсем о таком направлении в формировании звука. Звучит конечно необычно.

    Кстати, а не по тому ли принципу звук формировался на PC Speaker в 80-90-е? я слушал MOD файлы таким образом.


  1. qiper
    18.09.2024 10:05
    +2

    Особенно 3 трек впечатлил