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

в 21:17, , рубрики: C, cc65, Nes, Nintendo Entertainment System, ненормальное программирование, разработка, разработка игр

В этой части базовая информация о работе со звуком. Оно весьма запутано и использует специфическую терминологию, так что внятно описать его не особо получается.
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.

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

Famitracker умеет всё, но медленно и громоздко. Так что лучше использовать Famitone2.
https://shiru.untergrund.net/code.shtml

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

… здесь идет описание реализации всяких музыкальных тонкостей, которые мне вообще неведомы. Оригинал: https://nesdoug.com/2015/12/02/15-adding-music/

Главное ограничение — меньший диапазон нот, от С1 до D6. Лимиты можно подкрутить в ассемблерном исходнике. Вот таблица пересчета частоты звука в отсчеты таймера:
http://wiki.nesdev.com/w/index.php/APU_period_table
Хороший видеомануал по трекеру:

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

Главное — упаковать все треки в один файл. Далее он конвертируется в ассемблерный:
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. Думаю, когда-нибудь напишу раздел на эту тему

Автор: Вадим Марков

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js