Программирование для Famicom/NES/Денди в Nesicide+ca65: музыка и звуки (7)

Если вы еще совсем не в курсе о принципах на которых работает звуковой чип в Famicom/NES/Денди, то можете сперва провести небольшой ликбез по моей статье «О звуке». И надо же так совпало, что в день публикации этой статьи на хабре появилось очень подробное рассмотрение азов тут: habr.com/ru/post/482916/ Рекомендую ознакомится. Итак — в денди у нас есть набор примитивных осцилляторов-каналов: один треугольный, два прямоугольных, один шумовой и один однобитный DCPM. Программируются они через порты которые мы уже описывали в neslib.inc — группа портов с префиксов APU_*. Описание того как каждый канал настраивается, какие параметры имеет и самое главное — эксперименты потом со всем этим великолепием отнимут на мой взгляд слишком много времени у нас и это тот самый случай когда надо воспользоваться уже готовыми решениями. Мы так и поступим и не будем писать звуковой драйвер и редактор для него с нуля — а воспользуемся уже готовым открытым звуковым драйвером для Famicom/NES/Денди — FamiTone2 от Shiru. Драйвер и набор утилит к нему позволяет воспроизводить музыку и звуки созданные в популярном редакторе FamiTracker.

Но сначала немного о грустном…
Неприятности с Nesicide
Хотя IDE Nesicide разрабатывается уже несколько лет в ней до сих пор встречаются довольно жёсткие баги. Скорее всего когда вы будете это читать многие из них уже будут исправлены. Но возможно появятся и новые. :) Поэтому буквально пара слов о том как с этими багами сейчас уживаюсь я. Возможно это будет всё еще актуально.
1. Самое неприятное — периодическое разрушение файла с проектом .nesproject. Выстреливает случайно при закрытии среды и по словам Кристофера (автора) вероятно связано с конкуренцией потоков. Так или иначе бороться несложно — после создания и добавления файлов в проект я просто делаю копию этого файла с расширением .backup и при встрече с багом восстанавливаю его оттуда. В текущей версии этот баг гарантированно встречается если закрыть среду в момент когда в ней есть открытая закладка с редактором музыки, поэтому я решил упомянуть об этом всём именно сейчас.
2. Весьма раздражающая фигня происходит иногда с переводами строк — вместо байтов 0D0A (CRLF) Nesicide начинает подставлять 0D0D0A. При этом в самом редакторе всё выглядит вполне нормально, но при попытке что либо куда либо копировать вдруг возникают двойные переводы строк. Этот баг сообщался и лечился неоднократно, но почему то до сих пор иногда выстреливает. Устав ждать окончательного исправления я написал на C++ программку 0D0D0Afixer которая сейчас и в исходнике и в скомпилированном виде (но возможно потребуются библиотеки MinGW!) лежит в корне архива с готовыми примерами: yadi.sk/d/_THxg1gxuCCVNw
Эта утилита при запуске сканирует все файлы с расширениями .inc, .s и .c начиная с текущей папки и рекурсивно для всех её подпапок и проверяет нет ли в них последовательности 0D0D0A и если находит такие — выводит их в консоль. Если же запустить её с параметром fix, то она не только ищет эти файлы, но и исправляет этот баг в них на нормальные 0D0A. 0D0D0Afix_run.bat запускает её именно в таком варианте. В общем если вдруг заметите такое — не стесняйтесь запускать этот файл.

FamiTone2 и FamiTracker

Итак, для вывода звука и музыки мы будем использовать библиотеку FamiTone2 за авторством Shiru. Он выложил её и все материалы с ней в полный свободный доступ (т.е. можно использовать как угодно как в некоммерческих так и в коммерческих работах) за что ему конечно как говорится респект и уважуха. Тут будет уместно заметить, что и все исходные программные коды за моим авторством в этой серии уроков обладают той же лицензией: использовать их можно как угодно в любых целях, никаких гарантий не гарантируется, указание авторства приветствуется, но не является обязательным.
Скачать FamiTone2 можно как на сайте автора: shiru.untergrund.net/files/src/famitone2.zip так и на сайте FamiTracker: famitracker.com/downloads.php
После скачки и распаковки архива создадим новый проект опять таки скопировав все основные файлы из предыдущего (или взяв уже готовый пример Example04 из архива yadi.sk/d/_THxg1gxuCCVNw — он создан по пути c:\devel\nes\Example04).
Прежде всего немного исправим neslib.inc во фрагменте где объявляются макросы poke_vpage и fill_vpage_line добавив перед ними новый макрос и немного изменив тела предыдущих макросов:

; locate_in_vpage - выставить в PPU_ADDR адрес байта в 
; указанной странице page на координаты тайла (cx, cy)
;   page - PPU_SCR0 или PPU_SCR1
;   cx - от 0 до 31
;   cy - от 0 до 29
; портит аккумулятор!
; Заметьте, что параметры заключаются в скобки потому что иначе при 
; подстановке сложных выражений они могли бы неправильно развернуться.
.macro	locate_in_vpage page, cx, cy
	fill_ppu_addr (page) + (cx) + ((cy) * 32)
.endmacro

; poke_vpage - записать байт value по координатам (cx, cy) 
; в указанной странице page.
; портит аккумулятор!
.macro	poke_vpage page, cx, cy, value
	locate_in_vpage page, cx, cy
	store PPU_DATA, value
.endmacro

; fill_vpage_line - записать байт value times раз начиная с
; координат (cx, cy) в указанной странице page. 
; times не может быть больше 255!
; портит: A, X
.macro	fill_vpage_line page, cx, cy, times, value
	.local mloop
	locate_in_vpage page, cx, cy
	lda value
	ldx times
mloop:	sta PPU_DATA
	dex
	bne mloop
.endmacro

Мы тут выделили из макросов poke_vpage и fill_vpage_line строку fill_ppu_addr (page) + (cx) + ((cy) * 32) в отдельный макрос locate_in_vpage т.к. он нам будет полезен для выставления адреса в PPU_ADDR тайла с координатами ( cx, cy ) нужной видеостраницы.

Далее из папки Famitone копируем в папку /src проекта файл famitone2.s — это исходный код драйвера для ассемблера ca65.
Однако во первых — нам не нужно добавлять его в дерево проекта в силу того как он будет подключаться.
А во вторых — его надо будет всё-таки отредактировать для чего придётся открыть его в каком то другой редакторе ибо в Nesicide я не нашёл как открыть файл без добавления его в проект.
Исправить надо следующие строчки в самом начале:
было

	.if(FT_PAL_SUPPORT)
	.if(FT_NTSC_SUPPORT)
FT_PITCH_FIX = (FT_PAL_SUPPORT|FT_NTSC_SUPPORT)			;add PAL/NTSC pitch correction code only when both modes are enabled
	.endif
	.endif

надо сделать:

FT_PITCH_FIX = (FT_PAL_SUPPORT&FT_NTSC_SUPPORT)			;add PAL/NTSC pitch correction code only when both modes are enabled

Т.е. убрать все .if и .endif и | заменить на &. Тут дело в том, что FamiTone разрабатывался сразу для трёх разных ассемблеров и поэтому адаптирован под три разных синтаксиса тем, что основной код пишется для ассемблера NESASM, а два других синтаксиса включая CA65 поддерживаются автоматической утилитой трансляции (она тоже есть в архиве Famitone2 — nesasmc.exe). Насколько я понял синтаксис CA65 в этих строках лёг на общую схему неудачно и для полноценной работы требуется данное исправление. Всё работает и без него если одновременно включить и поддержку PAL и NTSC и видимо это скрывало баг от мгновенного обнаружения.

Далее сделаем в папке нашего проекта подпапку sounds. В неё нужно переписать из папки FamiTone следующие файлы:
  • tools/nsf2data.exe (утилита конвертации звуков в ассемблерный код)
  • tools/text2data.exe (утилита конвертации музыки в ассемблерный код)
  • src/demo/danger_streets.ftm (музыка в формате Famitracker)
  • src/demo/sounds.ftm (звуки в формате Famitracker)
Эти файлы надо копировать без папок — т.е. у нас должно получится 4 файла в папке sounds без подпапок.
Кроме этого создадим в папке sounds еще файл make_sounds.bat со следующим содержимым:

text2data.exe danger_streets.txt -ca65 -ch4
nsf2data.exe sounds.nsf -ca65 -ntsc

Это запуск утилит конвертации, но запускать этот файл нам еще рано. Утилиты конвертации музыки и звуков не работают напрямую с файлами .ftm — им нужны другие форматы данных которые можно получить экспортом из программы Famitracker. И если вы уже приготовились качать эту программу — то не надо.
Одним из достоинств среды Nesicide является то, что Famitracker уже встроен в неё в виде плагина!
Вернёмся в дерево проекта IDE и раскроем в нём ветку Sounds/Music. На неё нажмём правую кнопку мыши, выберем пункт Add Existing -> Music File и добавим оба наших danger_streets.ftm и sounds.ftm в дерево проекта. Теперь двойным щелчком вы сможете открывать закладку редактора с интегрированным редактором Famitracker. Будьте только осторожны: на момент написания статьи если закрыть IDE когда открыта хоть одна закладка с Famitracker, то это приводило к порче файла проекта .nesproject. Так что не держите их открытыми без надобности. Впрочем нам они нужны сейчас ненадолго — только для экспорта.
Во первых надо открыть danger_streets.frm и экспортировать его как текст: в меню закладки с Famitracker выбираем File -> export text… и сохраняем файл как danger_streets.txt в папку sounds проекта. Можно теперь закрыть эту закладку. Музыку надо сперва экспортировать в этот текстовый формат.
Открываем теперь закладку со звуками — sounds.ftm. Если музыка в файле была одна, то звуков как понятно должно быть много. Это реализуется через то, что Famitracker может сохранять несколько фрагментов музыки в одном файле. Если у вас не открыта в закладке Famitracker панель большая панель с кучей элементов управления, то выберите View->Conrol Panel. IDE может вылететь (увы), тогда просто открывайте снова. Так вот разные фрагменты здесь можно выбирать в поле выбора «Songs». Сам состав фрагментов и их количество можно изменить в меню Famitracker Module -> Module Properties. Но изучать сам Famitracker мы тут не будем. Хотя стоит заметить, что в файле музыки тоже может быть больше одного трека — это поддерживается.
В отличие от музыки звуки надо экспортировать в формат .nsf. Делается это через меню File -> Create NSF… в открывшемся диалоге смысла указывать что-либо нет, сразу жмём кнопку Export и сохраняем файл как sounds.nsf в нашу папку sounds.
Если всё получилось, то можно запустить двойным щелчком файл make_sounds.bat (из проводника) и убедиться, что в папке sounds появились два новых файла — sounds.s и danger_street.s.
Это файлы в синтаксисе ассемблера CA65 описывающие данные нашей музыки и наших звуков. Напрямую подключать их в дерево проекта Nesicide тоже не нужно.

Вместо этого мы открываем файл main.s (если он взят из предыдущего проекта — очищаем его целиком) и начинаем наполнять его новым кодом.
main.s — начало

; Подключаем заголовок библиотеки Famicom/NES/Денди
.include "src/neslib.inc"

; Сегмент векторов прерываний и сброса/включения - находится в самых
; последних шести байтах адресного пространства процессора ($FFFA-FFFF)
; и содержит адреса по которым процессор переходит при наступлении события
.segment "VECTORS"	
	.addr nmi	; Вектор прерывания NMI (процедура nmi ниже)
	.addr reset	; Вектор сброса/включения (процедура reset ниже)
	.addr irq	; Вектор прерывания IRQ (процедура irq ниже)

.segment "ZPAGE": zp	; Сегмент zero page, это надо пометить через ": zp"
vblank_counter:	.byte 0	; Счётчик прерываний VBlank
famitone_vars:	.res 3	; 3 байта в zero page для библиотеки FamiTone
music_is_on:	.byte 0	; Флаг того играет ли музыка ($80 или $00)

.segment "RAM"		; Сегмент неинициализированных данных в RAM

.segment "ROM_L"	; Сегмент данных в ПЗУ картриджа (страницы $C000-$FFFF)
palettes:		; Подготовленные наборы палитр (для фона и для спрайтов)
	; Повторяем наборы 2 раза - первый для фона и второй для спрайтов
	.repeat 2	
	.byte $0F, $00, $10, $20	; Черный, серый, светло-серый, белый
	.byte $0F, $16, $1A, $11	; -, красный, зеленый, синий
	.byte $0F, $1A, $11, $16	; -, зеленый, синий, красный
	.byte $0F, $11, $16, $1A	; -, синий, красный, зеленый
	.endrep
  
.segment "ROM_H"	; Сегмент кода в ПЗУ картриджа (страницы $C000-$FFFF)

; irq - процедура обработки прерывания IRQ
; Пока сразу же возвращается из прерывания как заглушка.
.proc irq
	rti		; Инструкция возврата из прерывания
.endproc

; nmi - процедура обработки прерывания NMI
; Обрабатывает наступление прерывания VBlank от PPU (см. процедуру wait_nmi)
.proc nmi
	inc vblank_counter	; Просто увеличим vblank_counter
	rti			; Возврат из прерывания
.endproc

; wait_nmi - ожидание наступления прерывания VBlank от PPU
; Согласно статье https://wiki.nesdev.com/w/index.php/NMI ожидание VBlank
; опросом верхнего бита PPU_STATUS в цикле может пропускать целые кадры из-за
; специфической гонки состояний, поэтому правильнее всего перехватывать прерывание,
; в нём наращивать счётчик (процедура nmi выше) и ожидать его изменения как в коде ниже.
.proc wait_nmi
	lda vblank_counter
notYet:	cmp vblank_counter
	beq notYet
	rts
.endproc

; fill_palettes - заполнить все наборы палитр данными из адреса в памяти
; вход:
;	arg0w - адрес таблицы с набором палитр (2 * 4 * 4 байта)
.proc fill_palettes
	fill_ppu_addr $3F00	; палитры в VRAM находятся по адресу $3F00
	ldy # 0			; зануляем счётчик и одновременно индекс
loop:
	lda (arg0w), y		; сложный режим адресации - к слову лежащему в zero page
				; по однобайтовому адресу arg0w прибавляется Y и 
				; в A загружается байт из полученного адреса
	sta PPU_DATA		; сохраняем в VRAM
	iny			; инкрементируем Y
	cpy # 2 * 4 * 4		; проверяем на выход за границу цикла
	bne loop		; и зацикливаемся если она еще не достигнута
	rts			; выходим из процедуры
.endproc

; fill_attribs - заполнить область цетовых атрибутов байтом в аккумуляторе
; адрес в PPU_ADDR уже должен быть настроен на эту область атрибутов!
.proc fill_attribs
	ldx # 64		; надо залить 64 байта цветовых атрибутов
loop:	sta PPU_DATA		; записываем в VRAM аккумулятор
	dex			; декрементируем X
	bne loop		; цикл по счётчику в X
	rts			; возврат из процедуры
.endproc

; zstr_print - записывает в PPU_DATA ASCIZ-строку начиная с адреса arg0w
; останавливается на нулевом символе.
.proc zstr_print
	ldy # 0			; Y должен быть равен 0
loop:	lda (arg0w), y		; Грузим в A байт из адреса в слове arg0w
	beq exit		; если он нулевой - выходим
	sta PPU_DATA		; сохраняем байт в VRAM
	inc arg0w + 0		; увеличиваем младший байт адреса
	bne skip_inc_high	; если он не провернулся в 0 - идём дальше
	inc arg0w + 1		; если да, то надо увеличить старший байт адреса
skip_inc_high:
	jmp loop		; возвращаемся в начало цикла
exit:	rts			; выходим из процедуры
.endproc

Что тут появилось нового: во первых в zero page мы выделили 3 байта в переменной famitone_vars и создали там же переменную music_is_on — флаг того играет сейчас музыка или нет. Дальше идёт всё как в прошлом уроке вплоть до новой процедуры zstr_print. Это печать строки — она берёт строку по адресу в параметре argw0 и перегоняет её в PPU_DATA пока не встретит символ с кодом 0. Предполагается, что PPU_ADDR уже настроен на нужный адрес в памяти видеостраниц, что мы и будем делать макросом locate_in_vpage который добавили в neslib.inc в начале статьи.

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

; Перед подключением FamiTone2 надо настроить её параметры
FT_BASE_ADR		= $0100	; Страница с переменными Famitone2, должна быть вида $xx00
				; Мы отдадим библиотеке начало страницы стека
FT_TEMP			= famitone_vars	; 3 байта в zero page для быстрой памяти Famitone
FT_DPCM_OFF		= $C000	; Начало звуковых эффектов. Должно быть адресом $c000..$ffc0 64-байтными шажками
FT_SFX_STREAMS		= 4	; Число звуковых эффектов проигрываемых одновременно (от 1 до 4)

FT_DPCM_ENABLE		= 0	; 1 - DMC включен, 0 - выключен
FT_SFX_ENABLE		= 1	; 1 - звуковые эффекты включены, 0 - выключены
FT_THREAD		= 0	; 1 - если ф-я звуковых эффектов вызывается из другого потока, 0 - иначе
FT_PAL_SUPPORT		= 0	; 1 - если поддерживается PAL, 0 иначе
FT_NTSC_SUPPORT		= 1	; 1 - если поддерживается NTSC, 0 иначе

.include "famitone2.s"			; Включаем тело библиотеки прямо в код
.segment "ROM_L"			; переключимся на сегмент данных
.include "sounds/danger_streets.s"	; Подключим файл с музыкой
.include "sounds/sounds.s"		; Подключим файл со звуками
; Зададим строки меню на экране:
str1:	.byte "START - play/stop music", 0
str2:	.byte "LEFT  - score", 0
str3:	.byte "RIGHT - splash", 0
str4:	.byte "UP    - coin", 0
str5:	.byte "DOWN  - beep", 0

.segment "ROM_H"		; Переключимся обратно в сегмент кода

Сперва вы определяем блок адресов и параметров. FT_BASE_ADR — адрес вида $XX00 страницы в памяти начало которой мы отдаём под массив переменных Famitone. Сам этот массив имеет размер примерно 130 байт ($8C в той конфигурации что использована здесь — это можно определить по идентификатору FT_BASE_SIZE). Тут мы вспомним, что сегмент стека у нас в начале преимущественно пустует, а сам стек в вершине вряд ли когда нибудь будет иметь размер больше пары-тройки десятков байт. Поэтому мы просто отдадим Famitone начало страницы стека и забудем про неё.
Но кроме основной памяти Famitone еще для скорости нужны 3 последовательных байта в zero page. Мы зарезервировали место под них в zero page в массиве famitone_vars и его то и укажем в параметре FT_TEMP. FT_SFX_STREAMS — число одновременно проигрываемых звуков от 0 до 3. На самом деле одновременное проигрывание на наших пяти каналах звучания штука весьма условная. Если разные звуки будут использовать одни и те же каналы, то «побеждать» будет звук у которого громкость по этому каналу больше или у коого больше приоритет (так как у канала треугольного осциллятора громкость всегда одна, то он разруливается только по приоритету). Всё это касается и музыки тоже — звуки конкурируют с ней на тех же принципах. Тем не менее такие правила порождают довольно неплохое звучание, иллюзия одновременного звучания довольно неплохая.
Параметр FT_DPCM_OFF — это начало данных для DPCM в верхних 16Кб ПЗУ консоли. Таковы технические ограничения этого канала. Однако с этим каналом однобитного оцифрованного звука связанно немало геморроя и я на данный момент сам не до конца понял некоторые моменты с ним связанные так что я не буду его использовать, но расскажу что именно с ним вызывает проблемы. Активация этого канала приводит к тому, что APU начинает периодически считывать из ПЗУ очередной байт для звука и в первых ревизиях консоли это приводило к неприятному багу: когда процессор считывал данные из порта ввода-вывода, то иногда в этот процесс вклинивался APU и порождал как бы второе считывание этого порта уходящее «в молоко». Это нестрашно для портов ввода у которых нет особого поведения. Но для таких портов как PPU_STATUS или JOY_PADx считывание автоматически очищает некоторые биты порта или продвигает биты состояния кнопок дальше в регистре сдвига. Таким образом эти биты начинают просто систематически выпадать и с немалым шансом во время работы DPCM. Это, как говорится, вызывает озабоченность.
С PPU_STATUS это особых проблем у нас вызывать не должно, т.к. мы не используем его опросы для проверки флага PPU_STAT_VBLANK который подвержен данной «коррозии». Но вот с JOY_PADx всё становится сложнее — распространённым решением стало опрашивание кнопок 3 раза подряд и выборка из полученного наиболее стабильного результата. Я с этим связываться сейчас не хочу, так что не использую канал DPCM. Главное что звук надо расположить обязательно по адресам в указанном диапазоне, поэтому понадобится колдовать с сегментами в файле nes.ini, что опять таки усложняет затею.

Далее идёт набор битовых 0/1 флагов — включен какой то модуль внутри библиотеки Famitone2 или нет. FT_DPCM_ENABLE — выключенный у нас канал DPCM, FT_SFX_ENABLE — звуки, FT_PAL_SUPPORT — поддержка PAL, FT_NTSC_SUPPORT — поддержка NTSC и FT_THREAD — поддержка многопоточности. Музыка поддерживается всегда и выключить её нельзя.
Поддержка регионов PAL/NTSC нужна в силу того что частота развёртки и генерации прерываний VBlank в них составляет 60Гц для NTSC и 50Гц для PAL и т.к. звуковые осцилляторы меняют свои параметры с этими же частотами, то возникает заметная на слух разница в воспроизведении. Famitone2 умеет регулировать воспроизведение подстраивая под нужный регион так что как вы мелодию слышите в редакторе так и услышите её из консоли.
Поддержка многопоточности означает вот что: сейчас в обработчике прерывания wait_nmi мы только инкрементируем счётчик. Код в основном потоке выполнения ждёт инкрементов этого счётчика чтобы синхронизироваться с VBlank. В нём же мы будем вызывать процедуру FamiToneUpdate чтобы драйвер звука обновил параметры осцилляторам. Но что если когда-нибудь код ниже jsr wait_nmi не успеет выполнится до наступления следующего VBlank? Тогда мы проскочим и игра замедлится ровно в 2 раза. И вместе с игрой замедлится и звук, ведь он тоже базирован на частоте вызова функции FamiToneUpdate. Как понятно это нежелательно. Даже если сама игра замедлится нежелательно чтобы и звук при этом превращаться в аудиокисель. Это легко устранить разместив вызов FamiToneUpdate не в основном потоке выполнения, а в обработчике прерывания — в wait_nmi. Только сразу предупреждаю — если вы попытаетесь это сделать, то не забудьте сохранить все регистры в стеке и восстановить их при выходе из прерывания (кроме регистра флагов, т.к. механизм прерывания сам его запоминает в стеке и восстанавливает). Текущий обработчик wait_nmi этого не делает только потому что не портит никаких регистров кроме флагов.
Но если мы разместим вызов FamiToneUpdate в wait_nmi, то могут возникнуть проблемы если он совершается в момент когда вызывалась функция обновления звуков из основного потока — и именно чтобы проблем не возникало надо установить FT_THREAD в 1.

Определив все параметры для библиотеки мы просто включаем её тело как текст в текущий модуль директивой ассемблера .include «famitone2.s».

Сразу после этого мы переключаемся в сегмент данных и подключаем таким же образом тела файлов sounds/danger_streets.s и sounds/sounds.s. Если открыть их в каком нибудь другом редакторе, то мы увидим что это массивы директив ассемблера заполняющие текущий сегмент данными. Главное что нам нужно из них находится в самом начале — метки danger_streets_music_data и sounds — начала данных коллекций музыки и звуков соответственно. Кроме этого мы определим здесь несколько строк для формирования подсказок на экране и вернёмся к сегменту «ROM_H».
Начинаем писать основное тело программы:
main.s — третья часть

; reset - стартовая точка всей программы - диктуется вторым адресом в сегменте 
; VECTORS оформлена как процедура, но вход в неё происходит при включении консоли 
; или сбросу её по кнопке RESET, поэтому ей некуда "возвращаться" и она 
; принудительно инициализирует память и стек чтобы работать с чистого листа.
.proc reset
	; ***********************************************************
	; * Первым делом нужно привести систему в рабочее состояние *
	; ***********************************************************
	sei			; запрещаем прерывания
	ldx # $FF		; чтобы инициализировать стек надо записать $FF в X
	txs			; и передать его в регистр вершины стека командой 
				; Transfer X to S (txs)
	; Теперь можно пользоваться стеком, например вызывать процедуры
	jsr warm_up		; вызовем процедуру "разогрева" (см. neslib.s)
	
	store_addr arg0w, palettes	; параметр arg0w = адрес наборов палитр
	jsr fill_palettes	; вызовем процедуру копирования палитр в PPU
	
	; ***************************
	; * Нарисуем на экране меню *
	; ***************************
	fill_page_by PPU_SCR0, # ' '	; Заполним экран пробелами
	lda # 0
	jsr fill_attribs		; С нулевой палитрой
	
	; Выведем строки меню одну под одной начиная со строки 10
	locate_in_vpage PPU_SCR0, 0, 10	; Нацелимся в PPU_ADDR на тайл с координатами (0, 10)
	store_addr arg0w, str1		; arg0w = адрес строки str1
	jsr zstr_print			; вызываем процедуру вывода строки
	locate_in_vpage PPU_SCR0, 0, 11	; и так 5 раз...
	store_addr arg0w, str2
	jsr zstr_print
	locate_in_vpage PPU_SCR0, 0, 12
	store_addr arg0w, str3
	jsr zstr_print
	locate_in_vpage PPU_SCR0, 0, 13
	store_addr arg0w, str4
	jsr zstr_print
	locate_in_vpage PPU_SCR0, 0, 14
	store_addr arg0w, str5
	jsr zstr_print

	; ********************************
	; * Инициализируем музыку и звук *
	; ********************************
	; Загрузим в X и Y нижний и верхний байты адреса музыки соответственно
	ldx # < danger_streets_music_data
	ldy # > danger_streets_music_data
	lda # 1					; 1 для NTSC, 0 для PAL
	jsr FamiToneInit			; Инициализируем музыкальный движок
	
	; Загрузим в X и Y нижний и верхний байты адреса звуков соответственно
	ldx # < sounds
	ldy # > sounds
	jsr FamiToneSfxInit			; Инициализируем звуковой движок


Начало стандартное — разогреваемся и заполняем палитры. Далее заливаем первую экранную область пробелами с нулевой палитрой и выводим подряд пять строк-подсказок новой функцией zstr_print.
Далее мы просто инициализируем музыкальную (это обязательно сделать) и звуковую подсистемы Famitone. Первое делает функция FamiToneInit. Ей передаются следующие параметры: В регистрах X и Y адрес музыкальных данных (в X младший байт адреса, в Y — старший), а в аккумуляторе 0 для системы PAL и 1 для NTSC. Звуки инициализирует функция FamiToneSfxInit — её обязательно надо вызвать если звуки включены в параметрах. Ей так же в регистрах X и Y передаётся адрес начала уже звуковых данных.
В игре может быть несколько наборов музыки и звука и между ними надо переключаться вызовом этих функций.
Идём дальше:
main.s — последняя часть

	; **********************************************
	; * Стартуем видеочип и запускаем все процессы *
	; **********************************************
	; Включим генерацию прерываний по VBlank и источником тайлов для спрайтов
	; сделаем второй банк видеоданных где у нас находится шрифт.
	store PPU_CTRL, # PPU_VBLANK_NMI | PPU_BGR_TBL_1000
	; Включим отображение спрайтов и то что они отображаются в левых 8 столбцах пикселей
	store PPU_MASK, # PPU_SHOW_BGR | PPU_SHOW_LEFT_BGR
	cli			; Разрешаем прерывания
	
	; ***************************
	; * Основной цикл программы *
	; ***************************
main_loop:
	jsr wait_nmi		; ждём наступления VBlank

	store PPU_SCROLL, # 0	; Перед началом кадра выставим скроллинг
	store PPU_SCROLL, # 0	; в (0, 0) чтобы панель рисовалась фиксированно
	
	; ********************************************************
	; * После работы с VRAM можно заняться другими вещами... *
	; ********************************************************

	jsr FamiToneUpdate	; Проведём шаг библиотеки FamiTone2

	jsr update_keys		; Обновим состояние кнопок опросив геймпады
	
	; Если сейчас нажали на START...
	jump_if_keys1_was_not_pressed KEY_START, skip_start
	bit music_is_on		; проверяем флаг того играет ли уже музыка
	bmi stop_music		; если да, идём останавливать её
	lda # 0			; иначе выбираем музыку номер 0
	jsr FamiToneMusicPlay	; и запускаем её воспроизведение
	store music_is_on, # $80	; помечаем во флаге что музыка играет
	jmp skip_start		; и идём дальше
stop_music:
	jsr FamiToneMusicStop	; тут мы останавливаем музыку
	store music_is_on, # $00	; и помечаем во флаге что она остановлена
skip_start:
	; Если нажали LEFT
	jump_if_keys1_was_not_pressed KEY_LEFT, skip_left
	lda # 0			; Выбираем нулевой звук
	ldx # FT_SFX_CH0	; и канал CH0
	jsr FamiToneSfxPlay	; и запускаем воспроизведение звука
skip_left:
	; Если нажали RIGHT
	jump_if_keys1_was_not_pressed KEY_RIGHT, skip_right
	lda # 1			; Выбираем звук 1
	ldx # FT_SFX_CH0	; и канал CH0
	jsr FamiToneSfxPlay	; и запускаем воспроизведение звука
skip_right:
	jump_if_keys1_was_not_pressed KEY_UP, skip_up
	lda # 2
	ldx # FT_SFX_CH0
	jsr FamiToneSfxPlay	; Для UP звук 2
skip_up:
	jump_if_keys1_was_not_pressed KEY_DOWN, skip_down
	lda # 3
	ldx # FT_SFX_CH0
	jsr FamiToneSfxPlay	; И для DOWN звук 3
skip_down:

	jmp main_loop		; И уходим ждать нового VBlank в бесконечном цикле
.endproc

Как обычно стартовав видеочип мы начинаем крутится в бесконечно в игровом цикле синхронизированном с VBlank.
Главное что надо делать с завидной регулярностью — это jsr FamiToneUpdate. Вызов этой процедуры обновит состояние музыки (осцилляторов звукового чипа) и продвинет внутреннее состояние звуковых потоков музыки и звуков вперед на один шаг.
Далее мы смотрим на нажатые только что кнопки и останавливаем или запускаем музыку или один из четырёх звуков на звуковом канале FT_SFX_CH0.
Как видите всё просто и не вызывает проблем. Посмотрим что получилось:


Давайте я еще переведу описание всех функций FamiTone2 из файла readme.txt:
  1. FamiToneInit — инициализация музыки (обязательно вызвать для всей библиотеки в целом). Входные параметры: A = 0 для PAL и любое ненулевое значение для NTSC; X и Y — указатель на данные музыки (треков может быть несколько).
  2. FamiToneSfxInit — инициализация звуков (обязательно вызвать если они включены. Входные параметры: X и Y — указатель на данные звуков. Следует вызываеть после FamiToneInit всякий раз когда нужно сменить звуковые эффекты.
  3. FamiToneUpdate — обновить состояние музыки/звуков. Без параметров. Нужно вызвать с частотой VBlank.
  4. FamiToneMusicPlay — начать проигрывание музыки. Входной параметр: A — порядковый номер трека.
  5. FamiToneMusicStop — остановить воспроизведение музыки.
  6. FamiToneMusicPause — поставить или снять музыку с паузы. Входной параметр: A — ноль для снятия и не ноль для постановки на паузу.
  7. FamiToneSfxPlay — воспроизвести звук. Входные параметры: A — номер звукового эффекта (от 0 до 63); X — слот воспроизводимых звуков (от FT_SFX_CH0 до FT_SFX_CH3).
  8. FamiToneSamplePlay — воспроизвести оцифрованный (DCPM) звук. Входной параметр: A — номер цифрового звука. Вызов отменит воспроизведение текущего проигрываемого цифрового звука (единственный канал).

Ну что же. На данный момент мы изучили все нужные техники чтобы написать игру подобную Super Mario Bros. Но чтобы написать игру подобную Darkwing Duck или Contra Force понадобится нечто большее — мапперы. О них будет следующая статья.

В первую часть (оглавление)...

1 комментарий

avatar
Вот же совпадение. Именно сегодня на хабре появляется статья habr.com/ru/post/482916/
И кто комментирует её первым?

>8-D
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.