Pull to refresh

Разработка игр под NES на C. Главы 14-16. Работа со звуком

Reading time 7 min
Views 8.1K
Original author: Nesdoug

В этой части базовая информация о работе со звуком. Звуковая подсистема NES весьма низкоуровневая, ее описание весьма запутано и использует специфическую терминологию, так что описание местами может быть не очень внятное.
<<< предыдущая следующая >>>
image
Источник


Начало работы со звуком


Обзор инструментов, которые представляет нам платформа NES. Впрочем, дальше мы уйдем на более высокий уровень и будем использовать библиотеку Famitracker.


Проще всего пощупать звуковые возможности консоли можно с помощью демки Sound Test, разработанной SnoBrow. Она совместима не со всеми эмуляторами, но FCEUX поддерживается.


Кнопка Селект переключает звуковые каналы, Старт включает их. Доступны 4 канала:
1 — меандр 1
2 — меандр 2
3 — треугольный сигнал
4 — шум


Звуковой сопроцессор (APU) управляется через регистры $4000-$4017.


$4000-$4003 = Меандра 1
$4004-$4007 = Меандра 2
$4008-$400B = Треугольного сигнал
$400C-$400F = Канала шума
$4010-$4013 = DMC, канал с дельта-модуляцией
$4015 = Управление каналами
$4017 = Счетчик кадров


Меандр 1
Канал управляется битами в регистре $4000 по схеме DDLC VVVV.


D — скважность. 10 — плавный звук, 01 или 11 — терпимый, 00 — противный
L и C — режимы работы канала
V — громкость


Режимы работы канала

L=0; C=0:
Получаем генератор огибающей. Громкость будет затухать, а V соответствует длительности звучания.


L=1; C=0:
Теперь V регулирует интервал между повторениями ноты на полной громкости.


L=0; C=1:
V управляет громкостью канала. Длительность ноты регулируется битами L в регистре $4003.


L=1; C=1:
V также управляет громкостью, нота играет непрерывно до новой записи в регистр $4000. Обычно длительность звучания завязана на счетчик кадров, переходы делаются таким же покадровым затуханием громкости.


Канал выключается обнулением громкости — пишем 0x30 в регистр $4000.


$4001 – Регистр качания частоты, биты: EPPP NSSS
E — включает эффект
P — период качания
N — направление. 0 — вниз, 1 — вверх
S — еще одно управление периодом, но другое


Если этот эффект включен, то нота играется только до его окончания. В комбинации с L=1 в регистре $4000 могут получаться интересные эффекты. Бит N влияет на низкие частоты, даже когда эффект выключен. Некоторые игры используют этот хак.


$4002 — 8 младших бит таймера, задающего частоту ноты
$4003 — LLLL LTTT — 5 бит таймера длительности ноты (работает только если хотя бы один из параметров L и C обнулен в регистре $4000) и 3 старших бита таймера частоты. Чем меньше значение таймера длительности, тем дольше звучит нота. По непонятной причине, частота ноты ограничена значением 000-00001000. Более высокий звук — то есть с более коротким периодом колебания — сделать не получится. Впрочем, и так это будет противный писк.


По адресам $4004-$4007 расположены абсолютно аналогичные регистры управления вторым каналом.


$4008-$400B — треугольный канал


$4008 – CRRR RRRR. C — флаг постоянно включенной ноты. R — непонятный регистр. Теоретически, он должен влиять на длительность ноты — 0xFF для постоянно включенной и 0x80 для выключенной. Но при установке здесь 0x7F длительность будет регулироваться через биты L в регистре $400B.
$4009 – не используется.
$400A — младшие биты таймера частоты ноты
$400B — LLLL LTTT. 5 битов длительности и 3 старших бита таймера частоты. Логика такая же, как и канала меандра.
Громкость канала не регулируется. Кроме того, он играет звук на 1 октаву ниже, чем меандр с той же настройкой. Ограничения по частоте тут нет, так что можно получить очень высокий писк.


$400C-$400F — канал шума
$400C — xxLC VVVV — так же как и меандр, но нет скважности.
$400D — не используется
$400E — ZxxxTTTT. Z — тип шума. 0 дает белый шум, а единица — металлический лязг. T — таймер частоты шума, чем меньше значение, тем выше тон.
$400F — LLLL Lxxx. Длительность ноты. Биты L и C из регистра $400C работают как для меандра.
Для выключения канала надо обнулить громкость ($400C = 0x30)


Канал DMC позволяет проиграть несжатый звук из памяти. Частота сэмплирования регулируется, и тут надо пойти на компромисс. Большая частота дает приличное качество, но требует неприлично много памяти для хранения звука. Низкая частота сильно портит качество и добавляет неприятный свист. Этот инструмент хорош для коротких звуков типа озвучки отдельных слов ("Fight!") или линии барабанов.


$4010 – ILxx RRRR. I включает прерывания канала, L — зацикливание сэмпла, R — частота.
$4011 – xDDD DDDD — громкость. Если дергать этот регистр в нужное время, то в принципе можно получить приличный по качеству звук.
$4012 — адрес начала сэмпла. 8 бит дополняются до 16 вот таким образом: 11AA AAAA AA00 0000. Так что получаем диапазон доступных адресов от $C000 до $FFC0.
$4013 – длительность сэмпла в байтах. Тоже дополняется, но до 12 бит: LLLL LLLL 0001. Получаем разрешенный размер от $11 до $FF1 байт. Если этого не хватит, то обычно можно сцепить несколько сэмплов и проиграть их подряд.


$4015 — xxxDNT21. Включает каналы.
D — DMC
N — шум
T — треугольный канал
2 — второй меандр
1 — первый меандр


$4017 — теоретически это счетчик кадров, но он редко используется. Большинство игр пишут сюда 0x40 при старте и этим ограничиваются.


DMC включается при записи соответствующего бита в регистр $4015 и выключается после окончания сэмпла. Чтобы проиграть его повторно, надо еще раз дернуть за бит.
Остальные каналы включаются записью старшего по адресу из их регистров ($4003 для первого меандра, и т.д.). Если писать туда каждый кадр, то будут неприятные щелчки.


Некоторые мапперы добавляют свои каналы звука. VRC7 вообще имеет FM-синтезатор, но он используется только в одной игре — Lagrange Point.


PCM-звук использовать в принципе можно, но мне не приходилось. Это критически затратно и по памяти, и по процессорному времени — ресурсов хватит разве что на статичную заставку.


Сделаем какую-нибудь простейшую пищалку. Ее можно добавить к какой-нибудь из демок в предыдущих уроках. Только проверьте, что звуковые каналы включены.


Простейшие эффекты
*((unsigned char*)0x4015) = 0x0f;

// Бип
if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
*((unsigned char*)0x4000) = 0x0f;
*((unsigned char*)0x4003) = 0x01;
}

// Звук для прыжка
if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
*((unsigned char*)0x4000) = 0x0f;
*((unsigned char*)0x4001) = 0xab;
*((unsigned char*)0x4003) = 0x01;
}

// А теперь пошумим
if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
*((unsigned char*)0x400c) = 0x0f;
*((unsigned char*)0x400e) = 0x0c;
*((unsigned char*)0x400e) = 0x00;
}

Когда наиграетесь с этим кодом и SoundTest, немедленно все забудьте и открывайте Famitracker.


Добавляем музыку


Известный туториал Nerdy Nights намекал, что надо писать свой музыкальный движок. Оказывается, все уже написано — Famitracker и Famitone2.


Famitracker. Подойдет самая свежая бинарная версия. Нюанс: иногда надо импортировать MIDI-файл, тогда надо ограничиться версией 0.4.2. Импорт всегда работал плохо, и после этой версии его сломали окончательно.


Famitracker умеет всё, но медленно и громоздко. Так что лучше использовать Famitone2.


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


… здесь идет описание реализации всяких музыкальных тонкостей, которые мне вообще неведомы. Оригинал.


Главное ограничение — меньший диапазон нот, от С1 до D6. Лимиты можно подкрутить в ассемблерном исходнике. Вот таблица пересчета частоты звука в отсчеты таймера.


Хороший видеомануал по трекеру:



… описание кнопок в трекере опускаю.


Главное — упаковать все треки в один файл. Далее он конвертируется в ассемблерный:
text2data TestMusic.txt -ca65
И вставляется в код инициализации:


.include "MUSIC/famitone2.s"

music_data:
.include "MUSIC/TestMusic.s"

В коде инициализации есть метка detectNTSC:, которая позволяет проверить версию консоли — NTSC (США/Япония), или европейский стандарт PAL. Это критично, потому что влияет на тайминги.


Надо кое-что добавить в reset.s для работы фамитоновской конструкции IF:


Скрытый текст
FT_BASE_ADR  =$0100 ;вообще это область стека, но он до этого места не дорастает

.define FT_THREAD       1
.define FT_PAL_SUPPORT 1
.define FT_NTSC_SUPPORT 1

FT_DPCM_OFF    = $C000
FT_SFX_STREAMS   = 1

.define FT_DPCM_ENABLE  0
.define FT_SFX_ENABLE   0 ;Гасим каналы DPCM и SFx

Метки для функций в Famitone2.s :


Скрытый текст
.export _Reset_Music, _Play_Music, _Music_Update

...

_Reset_Music:
lda NTSC_MODE
ldx #<music_data ; младший бит
ldy #>music_data ; старший бит
FamiToneInit:

...

_Play_Music:
FamiToneMusicPlay:

...

_Music_Update:
FamiToneUpdate:

Теперь можно объявить прототипы функций и дергать их из сишного кода:


void Reset_Music(void);
void __fastcall__ Play_Music(unsigned char song); // вызов через fastcall кладет аргумент в регистр, а не в стек
void Music_Update(void);

Так что теперь можно включить трек вызовом Reset_Music() и переключиться на другой через Play_Music(1). Раз в кадр надо вызывать Music_Update(). Можно еще импортировать функции паузы или остановки, но это проще делать вручную через управление громкостью:
*((unsigned char*)0x4015) = 0;
Выключить музыку можно пропустив вызов Music_Update. Для запуска снова надо записать 0x0F в $4015 и снова вызывать Music_Update. Канал шума надо будет перезапустить вручную, потому что Famitone включает его только при общей инициализации.


*((unsigned char*)0x4015) = 0x0f; // включение каналов
*((unsigned char*)0x400c) = 0x30; // звук в ноль
*((unsigned char*)0x400f) = 0x00; // включение канала шума

… или забить на это и все-таки использовать библиотечные функции Stop и Play.


Вот предыдущая демка с музыкой:
Дропбокс
Гитхаб


Звуковые эффекты


Каждый эффект был отдельным треком в Фамитрекере, с импортом в один файл.


Ограничения есть разве что на длительность эффектов. Каждый канал надо завершить эффектом C00, и сохранить все в 1 NSF-файл. Его надо положить в Famitone2/tools и конвертировать в ассемблер:
nsf2data SoundFx.nsf -ca65
и включить получившийся SoundFx.s в reset.s:


sounds_data:
.include “MUSIC/SoundFx.s”

а потом включить эффекты в движке:
.define FT_SFX_ENABLE 1
Надо также добавить соответствующие метки для импорта:


.export _Play_Fx

_Play_Fx:
ldx #0

FamiToneSfxPlay:

Используется только один канал SoundFx, поэтому в X пишется его номер. Если же каналов больше, то надо удалить эту строку и включать нужный канал перед вызовом FamiToneSfxPlay.


Эффект включается вызовом функции Play_Fx(), аргумент — номер канала, он же номер трека в NSF-файле, нумерация с нуля.



void __fastcall__ Play_Fx(unsigned char effect);

В нашей демке три эффекта, первый на прыжок, второй и третий на кнопки Вверх и Вниз соответственно



if (((joypad1old & UP) == 0) && ((joypad1 & UP) != 0))
Play_Fx(1);

Кнопка Старт переключает фоновые треки.
Дропбокс
Гитхаб


В туториале никак не освещена тема DPCM-сэмплов. Они круты для эффектов, хоть и требуют много памяти в картридже. Если так уж хочется, то можно импортировать wav-файл в Famitracker и конвертировать его в DMC, а потом обращаться к нему из Famitone2. Думаю, когда-нибудь напишу раздел на эту тему

Tags:
Hubs:
+19
Comments 4
Comments Comments 4

Articles