Как работает музыка в NES

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


В этой статье я расскажу о том, как в общих чертах работает звукогенерация в Ricoh 2A03/2A07, которые использовались в NES. Почему именно об этом? Ну, тут не столько я выбираю темы для своих статей, сколько мой pet-project выбирает их для меня.

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


Что создавало звук на вашей старой «восьмибитке»?

Это делал процессор Ricoh 2A03 в регионе NTSC и Ricoh 2A07 в регионе PAL. Отличия между ними сводятся к разнице между соответствующими телевизионными стандартами. Здесь я буду вести речь о NTSC.

Итак, процессор NES работает с таковой частотой 1789773 герц (1662607 в версии PAL). Да, один миллион семьсот восемьдесят девять тысяч семьсот семьдесят три такта в секунду. Чем важно это волшебное число? Это будет видно дальше.

Та часть процессора, которая отвечает за звук называется APU. Её тактовая частота в два раза меньше. Это тоже важный факт, который надо запомнить.

Состоит APU из пяти звукогенераторов, или каналов, каждый из которых управляется четырьмя байтами. То есть, у вас есть четыре байта и вы с помощью местного ассемблера выставляете нужные биты, чтобы указать, как приставка должна генерировать белый шум, например. Ещё один байт включает/выключает каждый из каналов. И ещё один управляет Frame Counter (об этой штуке ниже).

Два канала генерируют сигнал прямоугольной формы. Третий канал выдаёт «треугольный» звук (сигнал в форме эдакой пилы). Ещё один канал выдаёт белый шум. Последний канал позволяет развернуться и, в теории, проиграть любой звук, но с достаточно жёсткими ограничениями. Дополнительную жёсткость им добавляет то, что процессор из железки 1983 года выпуска кроме звука ещё должен целую видеоигру проигрывать.


Общие компоненты

А теперь поговорим о тех вещах, которые используются в нескольких каналах.


Frame counter

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

Есть счётчик, который с каждым циклом APU (два цикла CPU) увеличивается. На определённой цифре посылаются сигналы. Ниже табличка, где X.5 означает задержку на один такт CPU.

Но сначала о том, какие сигналы даёт наш Frame Counter. Есть три типа сигналов. Quarter Frame, Half Frame и, внезапно, Frame. «Четверть» и «Половинка» используются другими частями APU. А сам Frame является процессорным прерыванием, работу которых в NES надо разбирать другой статьёй. Запуск процессорного прерывания можно отключить, если оно вам не нужно.

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

Чтобы числа из левой колонки нам о чём-то сказали, нужно умножить 14915 на два (получим 29830 циклов CPU), а потом наше «волшебное» число 1789773 герца разделить на полученный результат. Получается 59,99909 процессорных прерываний в секунду. Почти 60 фреймов в секунду. Ну, и соответственно, почти 240 фреймов в секунду и почти 120 фреймов в секунду.

Стоит упомянуть ещё «замедленный» режим, за переключение в который есть отдельный флажок. Вот табличка:

Да, всё верно. В замедленном режиме нет процессорного прерывания. 48,00635 циклов в секунду дают нам приблизительно 96 половинок фрейма в секунду и около 192 четвертинок фрейма в секунду.

Отмечу ещё, что Frame Counter он один на весь APU, в отличие от тех компонентов, которые я рассматриваю дальше.


Length Counter (Счётчик длины ноты)

Счётчики длины каждый для своего канала. Он есть у четырёх каналов из пяти: у генераторов прямоугольных, треугольных и шумовых волн.

Состояние этого счётчика описывается флагом «вкл/выкл» (можно записывать туда значение) и, собственно, счётчиком. Когда у нас записано «выкл», этот счётчик принудительно обнуляется и значение, которое там было теряется.

Непосредственно со счётчиком всё тоже не так просто. Мы не можем выставить его напрямую. Вместо этого у нас есть LUT на 32 позиции, и пять бит на выбор одного из предустановленных значений. Если я дам эти значения подряд, они вам покажутся бессмысленными, поэтому сразу копипаст значений этой таблицы с nesdev wiki, соответствующий тому, как это дело размещено на чипе физически (и хоть какой-то логике).


Legend:
(

Linear length values:
1 1111 (1F) => 30
1 1101 (1D) => 28
1 1011 (1B) => 26
1 1001 (19) => 24
1 0111 (17) => 22
1 0101 (15) => 20
1 0011 (13) => 18
1 0001 (11) => 16
0 1111 (0F) => 14
0 1101 (0D) => 12
0 1011 (0B) => 10
0 1001 (09) => 8
0 0111 (07) => 6
0 0101 (05) => 4
0 0011 (03) => 2
0 0001 (01) => 254

Notes with base length 12 (4/4 at 75 bpm):
1 1110 (1E) => 32 (96 times 1/3, quarter note triplet)
1 1100 (1C) => 16 (48 times 1/3, eighth note triplet)
1 1010 (1A) => 72 (48 times 1 1/2, dotted quarter)
1 1000 (18) => 192 (Whole note)
1 0110 (16) => 96 (Half note)
1 0100 (14) => 48 (Quarter note)
1 0010 (12) => 24 (Eighth note)
1 0000 (10) => 12 (Sixteenth)

Notes with base length 10 (4/4 at 90 bpm, with relative durations being the same as above):
0 1110 (0E) => 26 (Approx. 80 times 1/3, quarter note triplet)
0 1100 (0C) => 14 (Approx. 40 times 1/3, eighth note triplet)
0 1010 (0A) => 60 (40 times 1 1/2, dotted quarter)
0 1000 (08) => 160 (Whole note)
0 0110 (06) => 80 (Half note)
0 0100 (04) => 40 (Quarter note)
0 0010 (02) => 20 (Eighth note)
0 0000 (00) => 10 (Sixteenth)

Теперь о поведении этого счётчика. Я уже упоминал, что если он выключен, то сам счётчик занулён и игнорируется. Если он включён, то его уменьшает вышеупомянутый Frame Counter. Каждый «Half Frame» сигнал уменьшает счётчик на еденицу, если там есть что уменьшать. То есть, это -120/96 от значения счётчика в секунду. Целая нота у нас длится, выходит, $160/120=4/3$ секунды. Если счётчик равен нулю и включён, то звук на канале, естественно, заглушается.


Envelope (огибающая, точнее пародия на неё)

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

Громкость во всех трёх каналах задаётся четырьмя битами, что даёт нам выходные значения от 0 до 15. И у нас есть так называемый «Envelope Parameter» из четырёх бит, который позволяет задать громкость. Но не всё так просто. Сначала дадим общую схему механизма, который выдаёт нам эти значения. Общая схема опять утащена с nesdev wiki.plodfdjiou9j4inbfbrt_ghoeyy.png

Видите Quarter Frame Clock? Да, это наш старый знакомый, Frame Counter. И с частотой 240/192 герца (приблизительно) он нам меняет огибающую.

Начнём со стартового флага. Он даёт нам отмашку на то, что можно начинать обратный отсчёт. Он устанавливается, если кто-то меняет длину (см. раздел про Length Counter выше) или частоту ноты. Так сделано потому, что длина и частота ноты расположены рядом и их можно только менять вместе, но об этом позже.

Если стартовый флаг установлен, то мы его сбрасываем, в Decay Level загружаем 15, а в Divider загружаем текущий Envelope Parameter. Такова у нас стартовая расстановка. Если стартовый флаг уже сброшен, мы уменьшаем Divider на один.

Если наш Divider был равен нулю, то вместо -1 мы вновь выставляем Envelope Parameter. В этот же момент мы уменьшаем Decay Level на еденицу. Decay Level и даёт нам постоянно убывающую громкость от 15 до 0. Получается что-то вроде двух вложенных циклов for, прокатывающихся $(V+1)*15$ раз, где V — это Envelope Parameter. V + 1, потому что один из циклов прокатывает все значения от 0 до V. Итоговая длительность ноты тогда будет $(V+1)*15/240$ секунды, если не включён замедленный режим Frame Counter.

Когда Decay Level доходит до 0, в игру вступает Loop Flag. Если он установлен, мы снова заряжаем туда 15, и нота начинает играть заново с переменной громкостью. То есть, убывание громкости от максимума до нуля повторяется. При сброшенном Loop Flag наша убывающая громкость остаётся на нуле.

Рассмотрим ещё один важный флаг, который влияет на конечный результат. Constant Flag. Он отвечает за переключение между «задаём громкость напрямую сами через Envelope Parameter» и «полагаемся на обратный отсчёт». Если этот флаг установлен, Decay Level игнорируется, но продолжает считаться по всё тем же правилам. Это значит, что если вы захотите в пределах одной ноты попереключаться между этими режимами, ваш ждёт много интересного и увлекательного. Видимо, схема на это не рассчитана, но что-то мне подсказывает, что могли найтись психи, которые выставляли полную громкость, а потом посреди ноты сбрасывали флаг Constant, мирясь с тем, что обратный отсчёт громкости по факту стартовал не с 15, а с, например, 10 до которых Decay Level успел опуститься.



Halt/Loop Flag

Да, Halt Flag из Length Counter и Loop Flag из Envelope это один и тот же бит. То есть, у вас есть выбор между конечной нотой и бесконечной нотой. Если вы выбрали конечную ноту, то она будет заглушена тем, кто первый успеет довести отсчёт до нуля. Придётся заняться подсчётами, чтобы предсказать на какой громкости оборвётся нота. А если ваша нота бесконечна, то или громкость задавайте сами, или имейте дело с громкостью, идущей по кругу от 15 до 1. Да, никто не говорил, что будет легко.


Status

Закончим отдельно стоящим однобайтным регистром. Он содержит пять бит на запись/чтение, которые включают/выключают наши пять звукогенераторов. Все, кроме канала дельта-модуляции выключаются сразу с помощью зануления Length Counter. У канала дельта-модуляции зануляется число оставшихся байт.

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


Каналы

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


Прямоугольный

vhf13ybjl8bekyuchcetvbqlz80.png641h_1nfpb-omznj9bpsnsoebby.png

Сказать, что эта часть чипа генерирует звук будет не совсем верно. Она генерирует число от 0 до 15, из которого потом складывается звуковая волна определённой громкости. Громкость — это амплитуда, так уж работает звук. Амплитуда — это высота этих самых столбиков, которые вы видите на картинке выше. И это самое число от 0 до 15 определяется с помощью Envelope, который я описал выше.

Теперь рассмотрим ту часть, которая из этого потока в диапозоне »0–15» нарубает ровные столбики прямоугольного сигнала. Для определения формы сигнала есть четыре LUT, из которых можно выбрать нужную с помощью двух бит в регистрах, управляющих прямоугольным сигналом. Выглядят эти таблицы так:

С первыми двумя колонками, думаю, всё должно быть понятно. Третья же касается деталей реализации. Как вы могли уже догадаться, форма выходного сигнала задаётся циклом от 0 до 7. Точнее, от 0 до 1. На старте переменная, отвечающая за текущее значение из LUT инициализирована в 0, но она изменяется в сторону уменьшения. То есть:0, 7, 6, 5, 4, 3, 2, 1.

Теперь рассмотрим вопрос частоты волны. Частота звука будет зависеть от того, как быстро прокручивается этот цикл. И задаётся эта скорость тем способом, который был бы удобен инженерам Nintendo, а не вам.

Выше я уже упоминал, что частота и длина ноты задаются вместе. И задаются они двумя байтами, где 5 бит отведено на длину ноты, а 11 на частоту. У нас есть число от 0 до 2047. Пусть оно будет t. Каждый такт APU переменная, отвечающая за цикл секвенсора, уменьшается на еденицу. Если там ноль, становится t. При этом изменяется в сторону уменьшения текущая фаза звуковой волны (0, 7, 6, 5, 4, 3, 2, 1). Весь цикл колебания нашей квадратной волны занимает $2*8*(t+1)$ тактов CPU. Таким образом, если мы хотим частоту f, нам нужно посчитать t по следующей формуле:

$t = \frac{f_{cpu}}{16*f}-1$

Не забудьте округлить и помните, что если вы выставите t меньше 8, ваш прямоугольный звук превратится в тыкву. Не то, чтобы это изначально отличалось от тыквы. Чем меньше t, тем выше частота, потолок получается около 12,4 килогерц.



Pulse Sweep, или единственное отличие между двумя квадратичными звукогенераторами NES.

Для того, чтобы облегчить жизнь композиторам на NES целый байт был щедро выделен на управление механизмом sweep, позволяющим менять частоту на ходу. Он позволяет менять значение t. У нас есть:


  • Флажок вкл/выкл (1 бит)
  • Значение периода изменения частоты P (3 бита)
  • Флажок Negate, делает выбор между уменьшать/увеличивать t (1 бит)
  • S, которое определяет насколько надо менять (3 бита)

Сначала про P. Там уже типичный для звукового чипа NES цикл P, P-1, P-2… 1, 0, P. Уменьшается он по Half-frame сигналу от Frame Counter (96/120 герц). Соответственно, когда мы переключаемся с 0 на P, раз в P+1 half-frame сигналов, происходит магия.

t меняется не на фиксированное число, а на заданный с помощью S процент. Достигается это следующим образом:


  1. Берётся копия текущего t, сдвигается на S бит вправо.
  2. Если флажок Negate установлен, значение из первого пункта делается отрицательным.
  3. t = t + c из пункта 2

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

Кстати, помните я говорил, что t меньше 8 ставить нельзя? Вот для защиты от этого механизма оно и было сделано. Цикл завязанный P, чуть что, крутится продолжит, но само t меняться не будет. Именно поэтому, кстати, самую высокую из доступных октав некоторые разработчики предпочитали не использовать вовсе. От греха подальше.


Треугольный

cdxjna_jfbsvslqrl9hgzcevxky.pngb-sj17yekv746hkz-il5wyxkw50.png

Снова весёлые картинки. Обратите внимание, у нас здесь нет Envelope. Почему? Потому что громкость у треугольного канала одна. А она одна из-за того, что делать вариации было слишком сложно и того не стоило (на дворе начало 80-х, что вы хотели).

На выход идёт текущее значение из следующей LUT:

Мы имеем всё тоже 11-битное t, которое управляет частотой. На этот раз с каждым циклом мы посылаем на выход новое значение из LUT выше. Но есть нюанс — этот таймер тикает каждый цикл CPU. А частота считается уже по иной формуле:

$t = \frac{f_{cpu}}{32*f}-1$

Тут нет механизма Sweep, а потому и канал при t < 8 не заглушается. Тут у нас простор частот аж до 55 килогерц, то есть, за той планкой, где уже начинается ультразвук. Тем ироничнее, что треугольный звук используют как «басуху». Очень уж сочно треугольные волны в низких частотах звучат.

Linear Counter — это такая обрезанная версия Length Counter, которая управляется одним байтом. Бит выделяется на вкл/выкл, а остальные семь — это значение счётчика. Счётчик уменьшается на еденицу с каждым quarter-frame сигналом. Семь бит — 127 quarter-frame сигналов. Это чуть больше полусекунды, поэтому этой возможностью пользовались достаточно редко.


Шумовой

myrw3xp7hty3spo9l9qox3prgkq.pngolvkgmxqibbyo3aqqsvev4tdd2g.png

А теперь откапываем свои университетские конспекты по криптографии, и вспоминаем, что такое LFSR, он же регистр сдвига с линейной обратной связью. Зачем оно нам надо? Скажем так, устройство этого канала похоже на квадратичный с той разницей, что 0/1 в местном секвенсоре определяется не LUT с ровными столбиками, а генератором случайных чисел. А какой там ещё может быть генератор случайных чисел, реализованный аппаратно в простеньком чипе?

Напомню/расскажу, что оно из себя представляет. Есть регистр, в котором определённое число бит. В нашем случае — 15, и они пронумерованы так: 14 — 13 — 12 — 11 — 10 — 9 — 8 — 7 — 6 — 5 — 4 — 3 — 2 — 1 — 0. Каждый шаг происходит следующее:


  • Мы считаем по формуле, которую легко реализовать аппаратно, новый бит. Это один бит, 0/1.
  • Мы сдвигаем наш регистр на один бит вправо.
  • На освободившееся место ставится свежепосчитанный 14-ый бит.
  • Это уже не про LFSR, но обращу ваше внимание, что в секвенсор идёт бит 0.

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


  • Если флаг Mode сброшен, то результат — XOR 0 и 1 битов. Получается последовательность длиной 32767 чисел, которая звучит как «пш-ш-ш-ш-ш-ш» (белый шум на телевизоре).
  • Если флаг Mode установлен, то результат — XOR 0 и 6 битов. Получается последовательность 93 или 31 число (зависит от того, когда вы флаг выставили), которая звучит как «пи-и-и-и-и-и-и-и» (знаете этот звук на телевизоре, когда вещание уже закончилось и осталась только настроечная таблица).

Здесь работает тот же Length Counter, который определяет длину ноты. А вот с переключением на следующий бит, то есть с управлением частотой, тут всё иначе. Местное T берётся из LUT на четыре бита, которая выглядит (в версии для NTSC) так:

Эти четыре бита лежат в соседнем байте с конструкцией |LLLLLTTT|TTTTTTTT|, и устанавливаются отдельно. 11 бит, которые регулируют частоту в других каналах, здесь никакой силы не имеют.

Обычно у этого канала два применения. Во-первых, взрывы и прочий «пыщь-пыщь-пыщь». Если выставить местный параметр частоты в 10, и проигрывать это коротенькими нотами можно услышать взрыв моста в джунглях из Contra. Во-вторых, перкуссия и прочее шумовое непотребство, вносящее разнообразие в музыку.



Дельта-модуляция

5h6oo6ul6l3n-eya_eir2gy2ise.pngnjf8mhf2e_cri4osjtrotma3dom.png

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

Что такое дельта-модуляция? Это когда ты пишешь не текущую фазу звуковой волны напрямую, как это делается в PCM (.wav без всякого сжатия, например), а разницу между ними. В случае, например, с белым шумом толку ноль. Но если мы последовательность чисел »0, 1, 2, 3, 4, 5, 6, 8, 10, 11, 10…» заменим на »1, 1, 1, 1, 1, 1, 2, 2, 1, -1», то это будет проще и сжимать, и хранить (на маленькие числа нужно меньше бит).

Здесь эта идея выкручена на максимум. Этот звукогенератор даёт вывод в пределах 0–127, вместо 0–15, как в остальных. Мелодия здесь однобитная, где 0 в потоке бит значит $x_n=x_{n-1}-2$, а 1 — $x_n=x_{n-1}+2$. Если получается меньше 0 или больше 127, то очередной бит игнорируется. Конечно, это сильно ограничивает вас, но другого пути разнообразить музыку или выдать особый звук по случаю прыжка, например, у нас нет.

Теперь рассмотрим, чем это счастье управляется (всё write-only):


  • Адрес. Целый байт, указывающий номер 16-битного блока, с которого начинается сэмпл.
  • Длина. Целый байт, указывающий число таких блоков. Длина сэмпла $L*16+1$.
  • Rate. Частота определяется четырьмя битами, которые смотрят в LUT ниже. Время до переключения на следующий бит указано в циклах CPU.
  • Loop. Однобитный флаг назначение которого, надеюсь, очевидно.
  • IRQ. Этот флаг разрешает делать процессорное прерывание, когда текущий сэмпл закончил играть.
  • Direct load.Самое весёлое. Семь бит отданы на то, чтобы напрямую задать вывод в обход механизма загрузки дельта-сэмплов. Постоянно меняя это 0–127 в теории можно играть любую музыку. На практике вам надо ещё и игру показывать, поэтому применяется это счастье только в заставках.

Тут LUT частот. Уточняю — это циклы CPU, то есть частота делится на это число напрямую.


И как из всё этого создаётся музыка?

Мы имеем пять звукогенераторов. Четыре из них выдают числа от 0 до 15. Один из них от 0 до 127. Остаётся самое главное: составить из этого звуки.


Вывод: Mixer

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

Каждый из звукогенераторов имеет свой ЦАП. Прямо сейчас будет рассказ о той части, которую я недолюбливал в школе, поэтому ссылка на источник. Каждый ЦАП ведёт себя, как резистор по принципу, «чем больше вывод, тем меньше сопротивление»: $r_{ЦАП}=r_{базовое}/output$. Ниже rбазовое всех каналов.

Квадратичные ЦАП соединены параллельно. Остальные три соединены параллельно в другую группу. Выходное напряжение обеих групп формируется делителем напряжения с резистором в 100 Ом. На вход в этот делитель изнутри чипа подаётся ток в 1,17 вольт.

Наконец, всё это смешивается с помощью двух резисторов в 12 и 20 кОм. Получается, пропорция 3/5, где 3 части отдаётся квадратичны звуками, а 5 всем остальным. Этот вольтаж и подаётся на динамики. Из всего этого была составлена следающая формула для использования в эмуляторах:

$output=\frac{95.88}{\frac{8128}{pulse_1+pulse_2}+100}+\frac{159.79}{\frac{1}{\frac{triangle}{8227}+\frac{noise}{12241}+\frac{dmc}{22638}}+100}$

Получается диапозон от 0.0 до 1.0, который можно использовать для генерирования звука в любом удобном вам формате.

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


Ввод: 22 волшебных байта

Мы теперь знаем, как именно пять звукогенераторов создают звук. Но чем они управляются? Как заставить их играть музыку? Вот тут уже приходить включать магию программирования на местном ассемблере. Каждый из пяти звукогенераторов имеет четыре write-only регистра по одному байту. Все эти биты, флаги, байты, о которых я говорил выше, рассованы по четырём байтам для каждого звукогенератора. Ещё два байтовых регистра живут отдельно. Один управляет Frame Counter’ом, а второй используется, чтобы определять/изменять состояние вкл/выкл у всех каналов.

Я не специалист по ассемблеру NES, но сумел выкопать тамошний аналог «Hello, world», который зацикливает писк, близкий к ноте «ля» малой октавы (212,79 герца).


reset:
lda #$01 ; square 1
sta $4015
lda #$08 ; period low
sta $4002
lda #$02 ; period high
sta $4003
lda #$bf ; volume
sta $4000
forever:
jmp forever

А теперь представьте, что вам нужно было, чтобы сыграть, к примеру, простенькую гамму.


  • Разместить где-то в памяти частоты и длины нот (а перед этим посчитать их).
  • Записать в нужные регистры нашего квадратичного канала все необходимые данные.
  • Организовать перехват процессорного прерывания от Frame Counter’а, где проверять, заглохла ли музыка через регистр $4015 (он же Status).
  • Перейти к следующей ноте, и завернуть это в цикл.
  • Не забываем, что это всё надо проделать отнюдь не на Python.

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

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


© Habrahabr.ru