
Сегодня я решил поведать миру очередную историю одного из множества моих восьмибитных деяний дней давно минувших. Это одновременно обзор и своего рода «постмортем» довольно успешного проекта, правда, припозднившийся на добрых полтора десятка лет. Погреться в лучах славы былых успехов никогда не поздно!
Речь пойдёт о некогда созданной мной библиотечкe, музыкальном «драйвере» или движке под названием «FamiTone», предназначенной для озвучки любительских (homebrew) игр для 8-битной игровой приставки Famicom, она же NES, она же «Денди». По пути разберёмся, что это, как устроено, кому и зачем вообще могло понадобиться, и пригодилось ли в итоге. Спойлер: да.
Официальная разработка
Подход к разработке игр для приставок третьего поколения, к которым принадлежат и Famicom с NES, сильно отличался от того, что является давно уже привычным в наше время. Не было не только готовых универсальных игровых «движков», как нынешние Unity и Unreal, но и никакой операционной системы, файлов и стандартных форматов. Только голое железо, только хардкор: память, регистры устройств, процессор, и код на ассемблере. И никаких «девкитов», SDK, и даже технической документации!

Сейчас это кажется странным, но тогда были другие времена. Индустрия видеоигр только зарождалась, и на рынке были другие правила игры. Nintendo не жаловала сторонних разработчиков на своей платформе, видя в них конкурентов и угрозу собственному бизнесу, и не желала помогать им в разработке игр. Напротив, даже выбор непопулярного в то время в Японии центрального процессора консоли MOS 6502 был обусловлен надеждой, что местные конкуренты не смогут быстро разобраться с ним. А особенности процесса разработки игр в стенах самой Nintendo целенаправленно скрывались, и поэтому сохранилось очень мало исторических свидетельств того, как это вообще осуществлялось.

Такая странная ситуация сохранялась на протяжении всего жизненного цикла консоли, с 1983 по 1994 год. Даже когда консоль вышла на рынки США и Европы, и была введена строгая лицензионная политика, и издание игр было поставлено под строгий контроль Nintendo, местные разработчики по сути были предоставлены сами себе — как хотите, так и делайте эти ваши игры.
В результате каждый отдельно взятый разработчик был вынужден сначала изучать железо методом реверс-инжиниринга, потом прилаживать коммерческие кросс-ассемблеры, создавать свои in-house инструменты для разработки и движки. В общем, изобретать свои собственные велосипеды. И, конечно, это касалось и звука.
Музыкальные драйверы
Если формат графических данных в играх хотя бы частично определялся устройством видеосистемы, и был схож между играми, стандарта на музыкальные данные не существовало. Собственно, у консоли был только звуковой синтезатор, способный издавать простейшие звуки, но не знающий ничего о музыке. Поэтому, вопреки расхожим современным суждениям, основанным на опыте современности, никакого готового музыкального формата, подобного, например, MIDI, на игровых консолях тех лет не было. Складывать отдельные звуки в подобие музыки было необходимо усилиями программистов, чисто программными средствами.
Решалась эта задача кодом, который современные исследователи вопроса называют «музыкальным драйвером» или «звуковым движком». Несмотря на название, этот драйвер не имеет ничего общего с современными драйверами устройств, например, тех же звуковых карт, и реализует иную функцию. Здесь подразумевается набор процедур, разбирающих сценарий закодированных событий — какая нота когда должна прозвучать, а также формирующих более сложные, изменяющиеся во времени звуки, для создания подобия разных «инструментов».
За отсутствием стандартных решений каждый разработчик изобретал свой собственный драйвер со своими особенностями и возможностями, и в результате их насчитывается добрая сотня штук. Конечно, драйверы могли потом повторно использоваться в пределах стен одной компании или работ одного и того же наёмного композитора, частенько по совместительству автора драйвера собственной разработки, а также развиваться со временем, из-за чего код и формат данных в каждой следующей игре мог отличаться.
Хотя характер звучания во многом определяется возможностями звукового чипа, от драйвера зависит, насколько полно и эффективно раскрываются эти самые возможности. Порой разница может быть очень радикальной, от простеньких тембров Super Mario Bros. до плотной, насыщенной аранжировки в Super Spy Hunter. Разные драйверы обладают довольно-таки узнаваемым, легко различимым на слух характером звучания. Подтверждением этого тезиса является звук в играх от компаний Capcom, Konami, Sunsoft, Natsume и Software Creations — один и тот же набор возможностей звукового чипа раскрывается в них очень разными гранями.
С технической же точки зрения драйверы, помимо собственно набора возможностей, отличаются и менее заметными параметрами, однако, влияющими на сами игры. Прежде всего это расход времени процессора, основного ОЗУ, а также компактность музыкальных данных. Чем более эффективно спроектирован код звукового драйвера, тем больше ресурсов системы остаётся для самой игры. Это довольно важно, так как известны примеры, когда не очень эффективный код драйвера расходовал до одной десятой времени процессора. Не так уж мало, учитывая, насколько скромными ресурсами обладает Famicom/NES.
Отдельно стоит упомянуть, как создавалась музыка для коммерческих игр. Вообще, это предмет для отдельного большого разговора, который может быть весьма любопытен для гиков, и как-нибудь я расскажу ��ро это гораздо более подробно.

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

Главным следствием такого подхода было то, что музыку нельзя было услышать в её финальном звучании во время сочинения, а процесс получения желаемого результата был весьма трудоёмок и некомфортен, и далеко не у каждого разработчика хватало терпения или иных ресурсов, чтобы добиться от своих драйверов и музыки действительно качественного звучания.
Homebrew-игры
Выход игры The Lion King в мае 1995 года ознаменовал завершение коммерческой жизни платформы. Больше игр с легитимной надписью «Licensed by Nintendo» на ней никогда не выходило. Однако, история платформы на этом вовсе не закончилась. Дальше за дело взялись специалисты без лицензии — коммерческие разработчики, не успевшие запрыгнуть в последний вагон, китайские компьютерные пираты, а вскоре и простые энтузиасты.
Любительские игры для устаревших игровых консолей известны под общим названием «homebrew», которым ранее называли пиво домашнего изготовления. В общем, своего рода «самогон» от мира видеоигр. За давностью лет и размытостью определений, пожалуй, уже не удастся определить, когда и как появилась первая любительская игра. Движение энтузиастов зародилось в 1990-х годах, привело к появлению эмуляторов, любительских переводов, реплик игр, а со временем и оригинальных разработок. Примечательно, что среди самых ранних примеров этой деятельности есть немало продуктов с отечественными корнями. Но про это я обязательно расскажу в другой раз, а сегодня на повестке дня у нас музыка.
Любители создавали свои первые игры, пользуясь примерно таким же подходом, как коммерческие разработчики прошлого: сами изучали железо, приспосабливали имеющиеся средства разработки и изобретали свои собственные. Хотя поначалу энтузиастов было немного, сила комьюнити и свободного обмена опытом через Интернет позволила форсировать процесс и избежать многократного изобретения велосипедов: в отличие от коммерческой разработки, однажды созданное любителями решение имело шансы стать доступным для использования более широким кругом энтузиастов.
За десятилетия своего существования сцена любительской разработки очень сильно разрослась, особенно в недавнюю декаду. Если в 2010-х годах выходили считаные игры, как правило, очень простенькие, самой успешной из которых в своё время Battle Kid, сейчас созданием homebrew занимаются сотни людей, выходят многие десятки игр в год, включая совершенно умопомрачительные разработки типа игры Former Dawn.
И, конечно же, в любительских играх тоже была нужна музыка. Для её создания и воспроизведения также были созданы кое-какие решения.
Музыка для homebrew
Конечно, поначалу разработчики-любители шли по стопам своих коммерческих собратьев: изобретали свои собственные звуковые процедуры, набивали музыку в виде последовательности байт в ассемблерных исходниках, и своими процедурами не делились. Пожалуй, это было весело, но тормозило общий прогресс.
В любительской разработке на домашних компьютерах, типа Commodore 64, ZX Spectrum, Amiga и прочих, где она исторически всегда была сильно развита, нормальной практикой было создавать готовые решения (программы, код) для различных задач и свободно делиться ими, что в конечном счёте было выгодно всем. Впрочем, попытки реализовать подобный подход тоже случались.

Первое готовое решение, созданное энтузиастами для озвучки своих разработок, появилось довольно рано, аж в 1999 году. Это программа Nerd Tracker II, созданная, впрочем, для уже устаревшей на тот момент операционной системы MS-DOS. Это вполне полноценный кросс-трекер для звукового чипа Famicom/NES, в котором можно было создавать очень хорошо звучащую музыку и экспортировать её в виде NSF-файла. Также прилагался исходник драйвера-проигрывателя, содержащий примерно 2000 строк на ассемблере.
Впрочем, у этой программы были свои, мягко говоря, особенности, требующие особого рода терпения для её применения на практике. Интерфейс работал в текстовом режиме и не поддерживал мышь. Не было функционала для элементарного копирования фрагментов, то бишь копипаста, который очень нужен в музыкальных редакторах, и даже диалога открытия файлов. И в таком виде она была заброшена авторами.
К тому же код проигрывателя изначально был написан для довольно редкого кросс-ассемблера (к 2003 году адаптирован под CA65), и его разрешалось использовать только в некоммерческих разработках. Специфика же разработки под «картриджные» консоли типа NES подразумевает издание любительских игр на реальных картриджах, которые стоят денег, и, как ни крути, их придётся продавать. Для многих энтузиастов это было критически важно, пройти путь полностью от разработки до издания игры и ощутить хотя бы копеечную финансовую отдачу.

Следующим готовым решением стал знаменитый ныне редактор FamiTracker, разработка которого началась в 2005 году. Впрочем, сейчас в Интернете самая ранняя из доступных версий — 0.3.5, датируемая 2009 годом. Эта программа для Windows обладала значительно более дружественным интерфейсом, стандартным, красивым, со всеми необходимыми возможностями, включая копирование, и я бы сказал, что в целом является одним из лучших и удобных трекеров в истории.
Конечно, у раннего FamiTracker’а были и свои недостатки, часть которых не решена по сей день (редактор ордер-листа). Зато из достоинств было отсутствие лицензионных ограничений и наличие исходного кода проигрывателя для более современного и широко распространённого ассемблера CA65. Правда, код этот довольно сложный, так как он поддерживает множество продвинутых возможностей трекера и различные дополнительные звуковые чипы. Итого почти 6000 строк кода в полутора десятках файлов.

В 2010 году появилось и третье готовое решение, также для Windows, изначально носившее название PornoTracker. Позже автор, между прочим, создатель знаменитой демки High Hopes, испугался собственной смелости и переименовал проект в MuseTracker. Независимо от имени, это одна и та же программа. И хотя она обладала некоторыми продвинутыми и даже уникальными возможностями, типа поддержки PCM-сэмплов, интерфейс её был далеко не столь дружественным.
Первая версия процедур для встраивания музыки MuseTracker в игры появилась только в 2011 году. Причём они никогда не были доступны в формате исходного кода, хотя и поставлялись в исходных файлах для трёх популярных ассемблеров. Однако, код процедур в них имел вид списка директив byte и word, без каких-либо опкодов и меток. По сути это бинарный блоб в текстовом формате на добрых 7000 строк. Очень странное решение, явно не пошедшее проекту на пользу.
Казалось бы, неплохой арсенал инструментов, есть из чего выбрать, да и проигрыватели доступны в исходниках. Однако, существование всех замечательных разработок не вполне закрывало проблему озвучки игр.
Дело в том, что все они по современной терминологии являются «гипертрекерами», выжимающими из системы наилучший возможный звук, не считаясь с затратами ресурсов — времени процессора, расхода ОЗУ и ПЗУ. Их процедуры-драйверы не были оптимизированы для применения в играх. Ну а главной проблемой процедур являлось отсутствие поддержки звуковых эффектов, воспроизводимых одновременно с музыкой — возможность, критически необходимая в любой игре.
Всё это, плюс ограниченная доступность и довольно высокая сложность (большой объём) исходного кода драйверов, которые были ориентированы только на определённые ассемблеры, привело к тому, что на момент 2010 года проблема озвучки игр всё ещё не имела готового, легкодоступного решения, которое можно было просто взять и внедрить в свой проект. Но почва была уже заложена, редакторы существовали, и оставалось только сделать их применимыми для решения задачи.
Тут-то и включился в игру я.
Путь FamiTone
К разработке любительских игр для NES я пришёл в конце 2010 года через увлечение ZX Spectrum и всевозможными игровыми консолями в 1990-х и начале 2000-х годов.
На Спектруме я привык к тому, что для решения всех вопросов всегда были какие-никакие готовые решения: ArtStudio для графики, Wham The Music Box для биперной музыки, позже Sound Tracker и Pro Tracker 3 для AY-музыки. Для NES же в области звуковых решений к моменту появления моего интереса был только устаревший NerdTracker для MS-DOS и свежий FamiTracker, не имевшие поддержки звуковых эффектов.
Самая первая моя поделка, новогоднее интро 2011 года, не требовала звуковых эффектов и обошлась использованием бинарного NSF-файла, экспортированного из FamiTracker — дикий костыль, прибитый к фиксированным адресам в памяти, для интро прокатило. Но задуманные будущие игры требовали более продвинутого и удобного решения.
Я стал искать и спрашивать на форумах. Оказалось, что один музыкальный драйвер есть, но только его надо просить лично у определённого человека, и не факт, что он даст, и музыку надо набирать в hex. В целом на сцене тех лет доминировал именно такой подход: люди предпочитали изобретать велосипед и набивать музыкальные данные вручную, чтобы сполна ощутить себя в шкуре древних разработчиков. Мне же показалось это дикостью при наличии прекрасного FamiTracker, который я на тот момент давно освоил.
Так совпало, что в тот момент аналогичная проблема озаботила не только меня, и некто Gradualore, позже прославившийся разработкой ряда успешных любительских игр, попытался её решить через добавление в FamiTracker системы плагинов-экспортеров. Это был поворотный момент, превративший FamiTracker в открытую систему — до этого сохранить можно было только бинарный блоб проекта в неизвестном науке формате либо экспортировать бинарный же файл в формате собственного драйвера. Ни то, ни другое не поддавалось парсингу. Плагины же позволяли создавать пользовательские дополнения, имеющие доступ ко всей структуре данных музыкального трека и выводящие их наружу в любом необходимом формате.
Я быстро решил взять ситуацию в свои руки и сделать музыкальный драйвер для собственных проектов. Но если Gradualore пошёл путём экспорта данных в формате своего будущего собственного драйвера (который появился чуть позже и также со временем завоевал некоторую популярность), опыт работы с Vortex Tracker II для ZX Spectrum подсказывал мне более оптимальный путь. Я сделал свой простенький плагин-экспортер трека в банальном человеко-читаемом текстовом формате. Дальше этот текст уже обрабатывала моя консольная утилита-парсер и преобразовывала в понятный драйверу формат.
Таким образом, я мог произвольно менять формат данных и возможности своего драйвера, и мне не нужно было каждый раз пересобирать плагин и вообще трогать FamiTracker. Совместимый экспорт в таком же текстовом формате был вскоре добавлен и в PornoTracker его автором. Позже аналогичная по сути функция экспорта текста была реализована в FamiTracker штатно, и я поддержал эту вариацию формата в своём парсере.

Вторая проблема, которую я задумал решить — отсутствие среди энтузиастов единства по поводу того, какой кросс-ассемблер использовать. Дело в том, что хотя процессор один, в разных кросс-ассемблерах для MOS 6502 используется разный синтаксис, в том числе в самых базовых моментах, а также прилично отличаются директивы и прочие особенности. Я провёл опрос на форуме и выяснил, что хотя разные разработчики используют зоопарк из десятка различных ассемблеров, среди них с заметным отрывом лидирует три: древний, но очень простой в употреблении NESASM, свежий на тот момент и такой же простой ASM6, и довольно старый, но очень продвинутый и непростой в применении CA65 из пакета CC65. К слову, сейчас актуальность из них сохранил только CA65.
Исходный код своего драйвера я написал под NESASM, а потом с помощью специально написанного под данный случай конвертера из этой версии автоматически создавались версии исходного кода для Asm6 и CA65. Также я поддержал экспорт данных в формате этих трёх ассемблеров в своих конвертерах.
Название драйвера FamiTone было предложено одним из самых активных homebrew-энтузиастов тех лет, Memblers, в IRC-чате #nesdev, где я был завсегдатаем одно время. Составлено оно, как можно догадаться, из приставки Fami-, взятой от названия консоли Famicom (что само по себе является сокращением от Family Computer), и слова Tone.
Польза ограничений
Как я уже упоминал ранее, FamiTracker и его собратья являются «гипертрекерами» — они предо��тавляют композитору максимум возможностей, не считаясь с затратами ресурсов. В реальных же играх критически важно иметь достаточно скорости процессора и свободного ОЗУ, чтобы могла работать сама игра. Собственно, на звуковую часть обычно выделяется совсем небольшая часть доступных ресурсов.
Прежде, чем писать свой собственный драйвер, я попробовал оценить эффективность музыкальных драйверов в коммерческих играх. Сделать это оказалось довольно просто: музыка из почти всех игр давно извлечена в её исходном бинарном формате, вместе с оригинальным кодом и данными, и упакована в файлы формата NSF. Для воспроизведения таких файлов требуется проигрыватель, эмулирующий процессор 6502, и звуковой чип.
Я взял NSF-файлы с музыкой из различных игр, модифицировал исходный код плагина NSF-проигрывателя Nosefart, и с его помощью измерил, какое количество тактов процессора тратят в среднем за тысячу кадров (16 секунд) и в пике драйверы тех или иных игр. Пост на форуме NesDev сохранил для истории результаты этих измерений:
Игра |
Место |
Среднее |
Пиковое |
|---|---|---|---|
Batman |
первый уровень |
1085 |
3780 |
Battletoads |
заставка |
482 |
1820 |
Battletoads and Double Dragons |
заставка |
726 |
3344 |
Contra |
первый уровень |
1090 |
4554 |
Mega Man |
уровень Cutman |
2311 |
7639 |
Ferrari Grand Prix Challenge |
заставка |
1583 |
4317 |
Эти результаты были подтверждены и другими энтузиастами. Выяснилось, что самый медленный звуковой драйвер в игре Mega Man. Он тратит порядка 8 процентов времени процессора за один кадр в среднем, что не так плохо, но аж до 25 процентов в пике. Самым быстрым оказался драйвер компании Rare, показавший какие-то совершенно невероятные цифры в игре Battletoads: около 2 процентов в среднем и 6 в пике. Драйверы обеих этих игр отличаются скромными техническими возможностями в плане звука, но тем не менее, обладают замечательной музыкой. Наиболее же продвинутый драйвер из перечисленных, из игры Ferrari Grand Prix Challenge, показал неплохие средние результаты — 5 и 15 процентов.
Я посчитал, что значения пиковой нагрузки более важны, чем средняя температура по палате: они приводят к лагу в игре в некоторые моменты, и это заметно визуально. Для плавной работы более предпочтительна стабильная средняя нагрузка без сильных пиковых значений — уж если игра тормозит из-за музыки, то пусть делает это равномерно, а не рывками.
Чтобы добиться максимальной производительности движка и оставить как можно больше ресурсов для кода игры, я решил ограничить его возможности совершенно минимально необходимым для жизни набором. Благо, имея богатый опыт сочинения чиптюнов, в том числе для Famicom, я знал, что действительно нужно, а без чего можно обойтись. Этот шаг давал двойную выгоду: помимо скорости работы, менее разнообразные музыкальные данные требовали меньшего объёма для хранения, что было довольно актуально в эпоху ранних 32-килобайтных любительских игр.
Осетра я урезал радикально. Никаких «эффектов», то есть команд типа слайда и вибрато в музыкальном треке, кроме изменения скорости. Отсутствие поддержки громкости для каждой ноты, ограниченное количество инструментов, и даже ограниченный диапазон нот — пять полных октав, от C-1 до B-5. Из «огибающих» в инструментах была оставлена только огибающая громкости, арпеджио и грубого питча, огибающие точного питча и скважности были упразднены (скважность задавалась на весь инструмент), а логика работы оставшихся была упрощена.
Отказ от эффектов и громкости для каждой ноты был оправдан тем, что эти возможности дублируются огибающими, и при необходимости через них всё равно можно сделать и вибрато, и слайд, и разную громкость одного и того же инструмента (несколькими его копиями). Примерно такой набор возможностей был реализован в 1991 году в оригинальной версии Sound Tracker для ZX Spectrum, и никто не жаловался. Напротив, этого хватило, чтобы написать огромное количество замечательной музыки. Их хватало и мне, а я не считал себя сильно продвинутым музыкантом и полагал, что если с работой в рамках этих ограничений справляюсь я, справятся и другие. Так и вышло.
Впрочем, это вполне спорное решение было понятно не всеми пользователями — зачем так сильно урезать исходные возможности трекера. Но смысл прост: продвинутый по возможностям полноценный драйвер FamiTracker’а уже существовал, и смысла делать его копию не было. Как покажут цифры ниже, для разных версий его возможно было оптимизировать, но не очень значительно. Нужен был радикально более оптимальный движок, и достичь производительности можно было только этим путём. В движке компании Rare, к слову, возможности ещё более примитивные, и это один из секретов его скорости.
Немного забегая вперёд, приведу показатели, полученные в разных версиях полноценного драйвера FamiTracker и FamiTone, демонстрирующие разницу в подходах. Например, сравнение объёма данных музыкальных треков (в байтах):
трек |
FT 0.3.7 |
FT 0.4.6 |
FamiTone |
FamiTone2 |
|---|---|---|---|---|
After The Rain |
7847 |
6059 |
6315 |
4676 |
Danger Streets |
4825 |
4840 |
5522 |
4354 |
Lan Master (4 трека) |
12373 |
10062 |
12072 |
7753 |
Lawn Mower (5 треков) |
9248 |
6281 |
8594 |
4514 |
Расход ОЗУ и объём кода в разных движках (в байтах):
драйвер |
ОЗУ |
ZP |
ПЗУ |
|---|---|---|---|
FT 0.3.7 |
241 |
21 |
5128 |
FT 0.4.2 |
245 |
20 |
5547 |
FamiTone |
183 |
7 |
1493 |
FamiTone2 |
186 |
3 |
1636 |
Пиковое время в разных движках для одних и тех же треков (в тактах):
трек |
FT 0.3.7 |
FT 0.4.2 |
FamiTone |
FamiTone2 |
|---|---|---|---|---|
After The Rain |
6946 |
7439 |
3673 |
3103 |
Danger Streets |
5678 |
6020 |
3439 |
2827 |
Также ограничения коснулись и звуковых эффектов. Впрочем, система, которую я реализовал, позволила делать весьма продвинутые полифонические эффекты. Но я ограничил максимальный объём данных одного эффекта 255 байтами, просто чтобы адресовать все данные одиночного эффекта одним 8-битным индексным регистров — это давало выигрыш по скорости работы.
Для композитора следствием наличия всех этих ограничений является необходимость всегда помнить про них, и не использовать в композициях ничего, что не поддерживает драйвер. Программы-конверторы при необходимости сообщают, если в треке встречаются неподдерживаемые возможности.
Возможности APU
Где-то здесь самое время поговорить уже о предмете, имеющем непосредственное отношение к вопросу: о звуковом синтезаторе игровой консоли Famicom и её собратьев. Известен он под собирательным наименованием 2A03, хотя фактически это наименование специализированной БИС разработки компании Ricoh, в которой интегрировано ядро микропроцессора MOS 6502 — CPU (Central Processing Unit), звуковой синтезатор — APU (Audio Processing Unit), и ещё кое-какая периферия консоли.

Подробно углубляться в возможности APU я не буду, так как это надолго. К этому я лучше вернусь в одной из будущих статей, посвящённой вопросу эмуляции чипа, там это будет более уместно. Здесь же коротко обозначу возможности синтезатора, для понимания, как они задействуются в моём драйвере.
APU представляет собой довольно типичный синтезатор своей эпохи, воспроизводящий несколько простых тембров. Такие известны под собирательным названием PSG, Programmable Sound Generator. Ближайшие его коллеги — SN76489, AY-3-8910, POKEY и SID. По возможностям он «среднячок» среди собратьев, примерно посередине этого списка. Однако, удачный баланс возможностей дал APU узнаваемое и довольно богатое звучание, а некоторые его особенности уникальны.
Самой заметной особенностью APU, выделяющей его среди прочих, являются разнородные каналы. Обычно звуковые чипы имеют 3-4 однотипных звуковых канала, которые могут иметь какие-то дополнительные уникальные опции. В APU же пять каналов, из которых абсолютно одинаковы только два, а остальные сильно различаются. То есть APU может одновременно воспроизводить пять звуков, и все они могут быть разного характера.

Первые два канала — так называемые Pulse. Они генерируют меандр. Можно выбрать скважность 12.5%, 25%, 50% и 75% (25% с инверсией фазы). На слух это звучит как чистый тон разной «толщины», или, если угодно, «остроты», что даёт неплохую тембральную вариативность. Игры используют эти каналы для основной мелодии. Есть 16 уровней громкости. Точность установки частоты 11 бит. Присутствуют дополнительные уникальные возможности, редко применяемые на практике за их малой полезностью: рудиментарная огибающая громкости, обеспечивающая автоматический спад громкости, но только с максимальной до нуля, и автоматическое плавное изменение частоты вверх или вниз.

Третий канал — Triangle. Условно-треугольная форма волны, составляемая из 16 уровней, дающая округлый звук. Чаще всего игры задействуют этот канал для партии баса, но иногда он применяется и для мелодии. Контроля громкости нет, точность установки частоты 16 бит. Из уникальных возможностей есть управление длительностью звучания: можно выключить канал через заданное время, в том числе очень короткое, что полезно для создания необычных звуковых эффектов, но также в реальности используется редко.

Четвёртый канал — Noise, то бишь шум. Используется для шумовых эффектов. Может играть цифровой шум 16 разных условных «высот», отличающихся спектром, от низкочастотного гула до шипения. 16 уровней громкости и огибающая, как у Pulse-каналов. Также есть режим длинного и короткого периода псевдослучайной последовательности. Второй позволяет создавать металлические звуки, изредка применяемые в играх для особенных звуковых эффектов.
Пятый канал — так называемый DMC, Delta Modulation Channel. В части игр применяется для более реалистичных барабанов, а иногда и для необычных звуковых эффектов. По сути это 7-битный ЦАП, который можно задействовать для воспроизведения оцифрованных реалистичных звуков двумя способами.

Первый способ, PCM — можно просто слать в ЦАП байты силами центрального процессора, как на многих других компьютерах. Это может обеспечить хорошее качество звучания, но редко используется в играх, потому что загружает процессор полностью.
Второй способ, DPCM (Delta PCM, отсюда же и Delta Modulation) используется повсеместно. Это одновременно метод сжатия с потерями 8:1 и возможность воспроизведения звука без участия процессора. Сэмплы кодируются в однобитный поток, где каждый бит определяет, поднять или опустить выходной уровень. Это, конечно, радикально снижает качество представления высокочастотной части спектра. Читаются данные специальным модулем, обращающимся к памяти напрямую (только в пределах верхних 16 килобайт ПЗУ). Есть возможность управлять скоростью воспроизведения, 16 предустановленных значений.

Все перечисленные возможности контролируются набором из 20 регистров. Каждому каналу уделено 3-4 управляющих регистров, а также есть несколько регистров, управляющих общими возможностями, такими, как включение-выключение каналов. Также предусмотрено маскируемое прерывание для центрального процессора по специальному счётчику и по окончании воспроизведения DPCM-сэмпла.
Устройство движка
Я буду милосерден к читателю и не стану углубляться в детали кода FamiTone. Вместо этого разберу архитектуру драйвера на высоком уровне, что и зачем в нём происходит, из каких частей он состоит, и какие задачи решает, а также каким образом задействует весьма разношёрстные возможности разных каналов APU.
Данные
Музыка и звуки создаются в FamiTracker. Из экспортированных текстовых (для музыки) и NSF-файлов (для эффектов) программы-конверторы text2data и nsf2data формируют нужные наборы данных, которые могут быть представлены как бинарными файлами, так и ассемблерным исходником с массивом директив типа byte.
Структурно данные для драйвера сильно отличаются от представления как в редакторе, так и в его штатном драйвере. В них нет структуры типа «ордер» и «паттерн».

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

Отдельной сущностью является массив данных звуковых эффектов. Он имеет совершенно другой формат и обрабатывается отдельным кодом. Звуковые эффекты при этом являются коротенькими полифоническими композициями.
Также может присутствовать блок данных DMC-сэмплов, который из-за ограничений аппаратуры требуется располагать в определённом месте памяти, и сообщать драйверу его местонахождение.
Архитектура кода
Драйвер представлен одним файлом исходного кода, который содержит тысячу строк на ассемблере — довольно немного. Для его использования достаточно включить этот код в свой проект, а также добавить данные музыки и звуков. Реализовано простейшее API, с вызовами типа «обновить состояние звукового движка», «запустить музыкальный трек с нужным номером», «запустить звуковой эффект с нужным номером».

Архитектурно драйвер разделяется на две вполне самостоятельные части: одна для музыки и одна для звуковых эффектов. Форматы и подходы к реализации этих составляющих совершенно разные, чтобы обеспечить наилучшую производительность и одновременно предоставить возможность создания сложных полифонических эффектов.
При этом часть кода для звуковых эффектов может быть легко отделена и приспособлена к любому другому музыкальному драйверу. В коммерческих драйверах звуковые эффекты могут быть реализованы особым режимом работы музыкального движка, но как правило, такие решения сильно ограничены в получающихся звуках.
Драйвер парсит потоки музыкальных событий раз в несколько кадров. Далее каждый кадр обновляются огибающие. Выходные значения огибающих и потоков преобразуются в данные для регистров APU и записываются в промежуточный массив. Каждый кадр отрабатывает проигрыватель звуковых эффектов, перезаписывающий данные для регистров в промежуточном массиве. После этого массив пересылается в фактические регистры APU.
Огибающие
При каждом вызове обновления состояния драйвера, то есть каждый кадр, выполняется обновление программных «огибающих». В FamiTracker они называются «секвенциями».

Это последовательность цифр, определяющих изменение того или иного параметра канала во времени: спад и нарастание громкости, отклонение высоты от базовой ноты. С помощью нескольких огибающих создаются различные «инструменты».
Всего в FamiTone реализовано 11 огибающих. Оба канала Pulse и Triangle имеют по три огибающих: громкость (для Triangle включение-выключение), отклонение высоты в полутонах и отклонение высоты в условных небольших шагах (фактически в единицах для счётчиков-делителей этих каналов). Канал Noise имеет две огибающие: только громкость и отклонение высоты тона, так как у шума всего 16 вариантов «высоты тона», и ему хватает одного вида отклонения для создания любых видов звучания. У DMC-канала огибающих нет.

Все огибающие однотипны и обновляются общим циклом, с одинаковой логикой. Своё конкретное назначение каждый из выходов огибающих приобретает позже. Ф��рмат их данных очень простой, для максимально быстрой обработки. Это последовательность байт, по сути представляющих собой фактические выходные значения в диапазоне -64..63, плюс простенькое RLE-кодирование:
значение |
назначение |
|---|---|
<127 |
число повторений предыдущего выходного значения |
127 |
конец огибающей, следующий за ним байт указывает на точку зацикливания |
128..255 |
выходное значение плюс 192 |
Также в музыкальных данных есть список инструментов, содержащий указатели на данные огибающих. Одни и те же огибающие могут использоваться в разных инструментах и для разных ролей — для громкости и для арпеджио. Это в особенности касается нулевой огибающей, по умолчанию, которая всегда присутствует в любом треке и просто держит стабильный 0 на выходе.
Проигрыватель музыки
Помимо обновления огибающих, с некоторой периодичностью, зависящей от темпа музыки (примерно раз в 3-6 кадров), выполняется логика потоков музыкальных данных. Эти данные определяют, какую ноту с каким инструментом нужно запустить в очередной момент времени.
Всего одновременно работает пять потоков, состояние которых обрабатывается общим циклом с одинаковой логикой. Данные потоков одинаковы по структуре для разных каналов, но могут отличаться содержанием. Так, например, на DMC канале не бывает смены инструмента. Упакованы потоки подобием LZ-сжатия, с прямыми и обратными ссылками на фрагменты других данных, в том числе данных других каналов и композиций.
Формат данных следующий:
бинарное значение |
назначение |
|---|---|
%00nnnnnn |
нота, 0..59 для октав 1-5, 63 глушение |
%01iiiii0 |
инструмент, 0 тишина |
%10rrrrrr |
пауза для следующих строк, до 63 |
%11eeeeee |
скорость $01..19 |
%11111110 |
конец потока, далее следует двухбайтовый абсолютный указатель на точку зацикливания |
%11111111 |
ссылка на повторяемый фрагмент, далее следует указатель и длина повторяемого фрагмента |
Результатом работы каждого потока является номер текущей установленной в каждом канале ноты или признак глушения канала, а также установка новых огибающих при смене инструмента и их перезапуск в начале новой ноты.
После обновления потоков и огибающих из их выходных значений формируются значения для записи в регистры APU.
Например, для канала Pulse 1 читается номер ноты первого потока. К нему добавляется знаковое значение выхода огибающей смещения в полутонах (арпеджио). По полученному итоговому номеру ноты из таблицы выбирается соответствующий период для счётчика-делителя. К нему добавляется знаковое значение выхода огибающей смещения частоты. Результат сохраняется для последующей записи в регистры $4000/4001. Также читается выход огибающей громкости, накладывается скважность текущего инструмента, и сохраняется для последующей записи в регистр $4003.
Аналогичным образом подготавливаются данные для каналов Triangle и Noise, с учётом их особенностей. Запуск DMC-сэмплов происходит сразу из цикла обновления потоков, когда он обрабатывает пятый поток.
Звуковые эффекты
Звуковые эффекты хранятся в формате дампа значений, уже готовых к записи в регистры — это гораздо быстрее, чем готовить их «на лету». Данные упакованы в кадры, за один кадр может обновляться сразу несколько регистров APU. Это позволяет делать полифонические эффекты.

Движок поддерживает до четырёх звуковых эффектов, одновременно накладываемых на музыку. Опциями препроцессора можно настроить, сколько будет потоков данных эффектов — чем их больше, тем медленнее драйвер. Практика показала, что даже четыре потока вполне жизнеспособны в реальных играх.
Наложение эффектов на продолжающую звучать музыку выполнено методом приоритета по громкости: если канал звукового эффекта громче, чем музыка в этом канале, эффект заглушает музыку: в массив регистров перезаписывается его громкость, период и прочие параметры. Это довольно топорное решение, но при грамотном планировании эффектов и музыки оно работает очень даже прилично.
NTSC vs PAL
Игра должна вызывать функцию обновления каждый телевизионный кадр, обычно в конце обработчика NMI. Случается это 60 раз в секунду на Famicom и американской NES, и 50 раз в секунду на европейской версии консоли.
С этой разницей в частоте кадров связана одна довольно большая проблема: как заставить одни и те же музыкальные данные звучать плюс-минус одинаково там и там. Ведь для лучшего охвата довольно скромной аудитории любителей homebrew-игр важно поддерживать все версии консоли.
Во-первых, отличие в частоте кадров влияет на темп музыки. Для решения этой проблемы принят следующий компромисс: музыка готовится для NTSC, а в PAL огибающие продолжают обновляться каждый кадр, когда как парсер потоков вызывается чуть чаще: каждый шестой кадр считается за два. Это приводит к некоторым, обычно малозаметным, отличиям в звучании, а также ограничивает самый быстрый темп обновления музыки для PAL в 2 кадра, чтобы было из чего вычитать лишний кадр.
Во-вторых, NTSC и PAL версии NES различаются тактовой частотой процессора и APU. Высота звука для одинаковых периодов у них разная, и это довольно заметно на слух. В оригинальной версии FamiTone я не стал решать этот вопрос никак, в PAL музыка просто звучит немного ниже. В FamiTone2 предусмотрено две таблицы периодов для обеспечения одинаковой высоты нот — это важно, если используется трюк с мелодичными инструментами в канале DMC, которые должны строить с прочими каналами...
Отдельная проблема возникает с высотой звучания эффектов. Ведь они хранятся в виде данных, готовых к записи в регистры APU, то есть их периоды уже рассчитаны заранее. В оригинальном FamiTone это тоже никак не решается, а в FamiTone2 предусмотрена возможность хранить по две копии каждого эффекта. Это удваивает расход ПЗУ, но так как эффекты обычно довольно компактные, это приемлемый компромисс для сохранения высокого быстродействия драйвера.
Наследие
FamiTone оригинальной версии был использован в моих первых играх — LAN Master, Lawn Mower, Alter Ego, Zooming Secretary, и других. Будучи полностью свободной, опубликованной под условной лицензией Public Domain (делайте что хотите без ограничений) библиотечка быстро обрела популярность и стала использоваться в проектах других энтузиастов.
Важным шагом, повлиявшим на дальнейший успех, было включение FamiTone в состав моей библиотеки neslib, предназначенной для написания игр для Famicom/NES на языке C. Примерно в то же время я популяризировал этот подход к разработке, прежде многим казавшийся неэффективным — никто просто не пытался, что привело к большому росту количества новых проектов. Большинство из них также использовали FamiTone, хотя была версия neslib и с полноценным драйвером от FamiTracker.
Примерно в 2013 году я сделал обновлённую версию, FamiTone2. Она стала быстрее и оптимальнее, решила проблему поддержки PAL/NTSC. Также было разрешено использовать чуть больше инструментов и DPCM-сэмплов. Главным образом это было нужно для реализации так называемого Sunsoft Bass, басовой партии на основе сэмплов, в игре Jim Power, о разработке которой я рассказывал однажды на страницах Хабра.
Так как исходный код был полностью свободен от лицензионных обязательств для любых применений, довольно быстро появились разнообразные улучшенные версии, созданные другими энтузиастами в личных или публичных целях.
Небезызвестный nesdoug, также энтузиаст разработки 8-битных игр на C, создал FamiTone3 (2016) с поддержкой громкости для каждой ноты, FamiTone4 (2018) с поддержкой эффектов слайда и вибрато и FamiTone5 (2021) с ещё большим количеством поддерживаемых эффектов. Как оказалось, смысл есть: в таком виде библиотека более функциональна, но в четыре раза меньше, чем код полноценного драйвера FamiTracker. Конечно, эти версии чуть помедленнее, чем FamiTone2.
Также существует необычный форк famitoneGB (2019) от того же nesdoug — версия, адаптированная для портативного Game Boy. Код библиотеки полностью переписан под местный 8080-подобный процессор SM83. Смысл существования этого решения в том, чтобы позволить создавать музыку для игр на Game Boy удобными и привычными инструментами, хотя и ценой отказа от раскрытия части его потенциала.
Сейчас FamiTone всех версий используется в сотнях игр. Точное их количество неизвестно. Впрочем, вероятно, время былой славы прошло, так как теперь можно считать более популярным драйвер GGSound от Gradualore, ставший частью популярной программы NESMaker. Но и его там теперь меняют на новомодный, ещё более оптимальный Sabre. Всё течёт, всё меняется.
Заключение
FamiTone — далеко не единственный мой проект, предназначенный для озвучки ретро-игр. О некоторых я уже рассказывал или упоминал в прошлых публикациях, а про последующий аналогичный проект SNESGSS для Super Nintendo, оставшийся почти неизвестным, возможно, расскажу в будущем.
© 2025 ООО «МТ ФИНАНС»