Здравствуйте!
В предыдущей части данного цикла статей мы закончили работу над критически важными компонентами нашего эмулятора. Для полноты картины в данной статье мы рассмотрим звуковую систему DMG.
Пишем эмулятор Gameboy, часть 1
Пишем эмулятор Gameboy, часть 2
Пишем эмулятор Gameboy, часть 3
Прежде чем начать, вот ссылка на репозиторий Cookieboy, где можно найти его исходный код и последнюю сборку.
Оглавление
Звуковая система
Звуковые каналы 1 и 2
Звуковой канал 3
Звуковой канал 4
Реализация
Тестирование
Что дальше
Заключение
Звуковая система
DMG позволяет выводить стереозвук с помощью смешивания 4 независимых звуковых каналов. К каждому каналу подключены компоненты модуляции, которые и может контролировать игра для вывода необходимого звука. Всего компонентов три и назначение для всех каналов у них идентично:
- Sweep unit. Осуществляет изменение частоты звука с заданным периодом и шагом.
- Length counter. Контролирует длительность вывода звука.
- Envelope unit. Осуществляет изменение громкости звука с заданным периодом.
Каждый канал дает в распоряжение ряд регистров, которые позволяют контролировать эти компоненты и сам канал. Они (регистры) нумеруются определенным образом – NRXY, где X – номер канала (1, 2, 3, 4), Y – номер регистра. Где необходимо я буду опускать номер канала и просто писать X.
Следующие звуковые каналы доступны на DMG:
- Прямоугольная волна. Содержит все три компонента модуляции.
- Прямоугольная волна. Содержит компоненты контроля громкости и длительности.
- Волна произвольной формы. Содержит только компонент контроля длительности. Громкость устанавливается в одном из регистров вручную.
- Генератор шума. Содержит компоненты контроля громкости и длительности.
Первые два канал идентичны и отличаются лишь набором компонентов модуляции.
Третий канал позволяет воспроизводить сигнал произвольной формы из специальной области памяти в секции I/O ports – Wave Pattern RAM. Таким образом, можно воспроизводить цифровой звук произвольного содержания путем своевременного обновления указанной области памяти. Некоторые умудряются воспроизводить что-то похожее на речь.
Четвертый канал позволяет генерировать шум различного характера. Хорошо подходит для озвучивания различных спецэффектов.
Вот упрощенная схема формирования звука в DMG:
После прохождения всех компонентов модуляции звуковой сигнал попадает в микшер, который смешивает различные каналы и выводит в один из выходов. S01 – правое ухо. S02 – левое ухо. Операция смешивания сводится к простому сложению сигналов ото всех источников для конкретного выхода – NR51 указывает, куда и какие каналы должны быть выведены. Далее учитывается громкость для каждого из выходов – сигнал после смешивания умножается на значение громкости данного выхода в регистре NR50 плюс 1.
Не стоит пытаться полностью разобраться в этой схеме – по ходу дела все, что на ней нарисовано, будет рассмотрено подробнее.
NR50 и NR51 являются общими регистрами. Кроме них есть общий регистр NR52, который содержит флаг отключения всего звука, а так же биты, показывающие статус звуковых каналов. Изменять можно только флаг отключения звука. Биты статуса доступны только для чтения и постоянно обновляются.
Если в регистре NR52 отключен звук, то происходит следующее:
- Все регистры обнуляются, кроме счетчиков Length counter. Это означает, что нужно обнулить только биты, относящиеся к Duty cycle (далее станет понятно, о чем я).
- Запрещается запись во все регистры, кроме NRX1. Причем, запись может осуществляться только в те биты, которые относятся к Length counter.
К слову о доступности битов регистров. Обращайте внимание на то, какие биты не используются или недоступны для чтения. При попытках прочитать регистры извне нужно установить в единицу все биты, которые не используются или недоступны для чтения. Тестовые ROM’ы проверяют это. Изменять сам регистр не нужно ни в коем случае. Звуковые компоненты, естественно, имеют полный доступ к регистрам.
Все компоненты, формирующие звук, синхронизированы с тактовым генератором. Для генерации звуковых волн определенной частоты используется сам тактовый генератор. Для модулирующих компонентов выделен отдельный тактовый генератор Frame Sequencer, работающий на частоте 512 Гц. Он так же работает от основного тактового генератора, но позволяет генерировать низкочастотные отсчеты. Для Sweep Unit используется частота 128 Гц. Для Length Counter – 256 Гц. Для Envelope Unit — 64 Гц. Вот как выглядит процесс работы этого тактового генератора, где каждая строка означает один отсчет Frame Sequencer’а:
Length Counter | Envelope unit | Sweep unit |
отсчет | - | отсчет |
- | отсчет | - |
отсчет | - | - |
- | - | - |
отсчет | - | отсчет |
- | - | - |
отсчет | - | - |
- | - | - |
В таблице отмечено, какие отсчеты Frame Sequencer’а генерируют отсчет для компонентов модуляции. Получается, что он циклично проходит такую последовательность отсчетов, что дает нам 8 возможных состояний Frame Sequencer (пронумеруем их от 0 до 7). Здесь важно учитывать, с какой фазой происходят отсчеты для компонентов. Так же стоит учесть, что при запуске звука (флаг в регистре NR52) Frame Sequencer стартует с состояния 1. Очень важно при этом дать понять компонентам модуляции, что вы изменили состояние Frame Sequencer. Одно время я еле нашел эту ошибку, из-за которой не мог пройти один из тестовых ROM’ов.
Разобравшись с общим устройством, перейдем к рассмотрению каждого конкретного канала.
Звуковые каналы 1 и 2
Сначала рассмотрим каналы, генерирующие прямоугольные волны. Здесь не имеет смысла разделять 1 и 2 каналы. Рассмотрев канал 1, можно будет реализовать канал 2 простым урезанием функционала, поскольку они идентичны за исключением Sweep unit.
И так, что такое прямоугольная волна. Внизу на рисунке изображена как раз такая волна.
Не особенно важно, где находится временная ось. В моем эмуляторе используется волна, которая генерирует отрезки «есть сигнал» (1-2, 3-4, 5-6), «нет сигнала» (0-1, 2-3, 4-5). Можно было бы сделать по-другому и поместить временную ось посередине, но это лишь усложнит реализацию, а результат будет идентичным.
На этом рисунке скважность сигнала равна 2, поскольку отрезки с разной амплитудой имеют одинаковую длительность. DMG позволяет генерировать прямоугольные волны с различными значениями скважности, хотя в документации принято использовать величину обратную скважности – коэффициент заполнения (duty cycle). Он кстати и более нагляден, им и будем пользоваться. Выбор дается из 4 различных значений коэффициента заполнения – 0.125, 0.25, 0.5, 0.75. Коэффициент заполнения никак не влияет на частоту, а лишь на характер сигнала. На рисунке ниже показаны различия сигналов при различных коэффициентах заполнения и одинаковой частоте.
Предоставлено 4 значения, хотя, по сути, значений 3 – коэффициенты заполнения 0.25 и 0.75 дают разные на вид волны, но звучание у них идентичное. При воспроизведении звука значение имеет изменение амплитуды, которое имеет одинаковый характер при коэффициентах заполнения 0.25 и 0.75.
Значение коэффициента заполнения содержится в регистре NRX1 в двух верхних битах.
Естественно нам надо знать, какой частоты должен генерироваться сигнал. Для этого используются регистры NRX3 и NRX4. Частота указывается числом длиной 11 бит – младшие 8 бит содержатся в регистре NRX3, старшие 3 бита в регистре NRX4. Таким образом, частота может лежать в пределах от 0 до 2047, но эти значения не относятся к реальной частоте звука. Для перевода этих значений в реальную частоту необходимо воспользоваться следующей формулой:
F = 4194304 / (32 * (2048 – X)) Гц,
где X – частота из регистров NRX3 и NRX4, F – частота звука.
Таким образом, частота звука лежит в пределах от 64 Гц до 131 072 Гц. Не стоит беспокоиться о таких высоких частотах – мало того, что нам будет довольно сложно должным образом сгенерировать звук такой частоты (по теореме Котельникова частота дискретизации должна быть больше 262 144 Гц); так все усложняется тем, что наша техника не способна воспроизвести такое, а наши уши не способны услышать. Более реальный диапазон ограничивается 22 000 Гц – это примерно соответствует верхней границе динамического диапазона человеческого слуха и совсем не случайно и большинства акустических систем. А для таких частот нам достаточно привычной частоты дискретизации в 44 100 Гц.
Формула выше обычно дается в документации как данное, но неплохо бы понимать, почему вычисляется именно так. Посмотрим еще раз на схему работы звуковой системы, там есть компонент Wave generator. В нем содержится таймер, с помощью которого генерируется волна нужной частоты. Период этого таймера равен 4 * (2048 – X). Чтобы волна прошла полный период, таймер должен сделать 8 отсчетов, что и дает нам заветные 32 * (2048 – X) – это значение полного периода волны.
Упомянутый таймер при должной реализации позволит вам не беспокоиться о переводах частот. Если таймер в эмуляторе будет синхронизирован с процессором так же, как и все остальные компоненты,
то все будет работать само собой. Формула 4 * (2048 – X) дает период таймера в тактах.
За 8 отсчетов этого таймера звуковая волна пройдет полный период. Теперь вернемся к коэффициенту заполнения. Он диктует характер изменения волны в течение ее периода. В документации приводятся следующие значения (1 и 0 в правом столбце означает, соответственно, «есть сигнал» и «нет сигнала»):
Коэффициент заполнения | Форма одного периода сигнала |
0.125 | 00000001 |
0.25 | 10000001 |
0.5 | 10000111 |
0.75 | 01111110 |
Помимо частоты в регистре NRX4 хранятся и другие данные. Вот кстати его структура:
Биты | Назначение |
7 | Перезапуск канала |
6 | Бесконечное/конечное воспроизведение |
2-0 | Нижние 3 бита частоты |
Если бит 6 сброшен, то звук воспроизводится бесконечно. Если бит установлен, то в дело вступает Length Counter.
Бит 7 перезапуска именно это и делает. Если в него записывается 1, то канал перезапускается. Это может показаться странным для канала, который воспроизводит бесконечный периодический сигнал, но в действительности это имеет большее значение для компонентов модуляции. О них позже. Помимо этого, упомянутые выше формы сигналов позволяют генерировать правильный сигнал – при перезапуске канала период сигнала тоже стартует с самого начала согласно указанным формам.
Это все, что нужно знать о каналах 1 и 2 в общем. Далее в ход вступают компоненты модуляции. Несмотря на то, что каждый канал содержит свои компоненты модуляции, их принцип работы идентичен. Сейчас мы рассмотрим все компоненты модуляции (канал 1 их все содержит), чтобы больше уже не повторяться.
Sweep unit
Данный компонент контролирует частоту сигнала. Он работает в двух режимах – увеличение или уменьшение частоты. Поддерживаются различные периоды и размеры шага. Контролируется компонент посредством регистра NRX0. Вот его структура:
Биты | Назначение |
6-4 | Период: 000 – компонент выключен 001 – 1/128 с 010 – 2/128 с 011 – 3/128 с 100 – 4/128 с 101 – 5/128 с 110 – 6/128 с 111 – 7/128 с |
3 | Режим: 0 – увеличение частоты 1 – уменьшение частоты |
2-0 | Шаг |
Периоды указаны в миллисекундах, и их следовало бы перевести в такты для отсчета времени, но при должной реализации Frame Sequencer нам это не понадобится. Sweep unit работает на частоте 128 Гц, поэтому периоды не случайно вычисляются относительно 1/128 — это и позволяет забыть о ручном подсчете тактов. Так дело обстоит и с остальными компонентами – Frame Sequencer все считает, остальным ни о чем не надо «беспокоиться».
Теперь шаг. Объяснять его бессмысленно, проще привести формулу, по которой вычисляется следующее значение частоты при очередном отсчете:
F(t) = F(t – 1) ± F(t – 1) / 2n,
где F(t) – следующее значение частоты, F(t – 1) – текущее значение частоты, n – значение шага из регистра NRX0. Замечу, что нам нужно использовать не деление, а битовый сдвиг вправо на число шагов из регистра NRX0.
На рисунке ниже изображена работа Sweep unit при NRX0 = 0x61:
Изменение частоты происходит постоянно до тех пор, пока не будет достигнут один из пределов для значения частоты или кто-то не отключит Sweep unit. Если включен режим уменьшения частоты и очередная частота получилась отрицательной, то сохраняется предыдущее значение и вычисления прекращаются. Если включен режим увеличения частоты, и она перевалила за максимальное значение (2047), то канал останавливается, а в соответствующем бите статуса регистра NR52 записывается ноль, сигнализирующий о том, что канал остановлен.
На этом заканчиваются простые вещи и начинаются неочевидные детали. Sweep содержит несколько скрытых регистров, недоступных извне – флаг активности (internal enabled flag) и регистр-буфер для частоты (frequency shadow register). Так же он содержит счетчик для соблюдения установленного в регистре NRX0 периода.
Я уже упоминал, что бит перезапуска в регистре NRX4, который еще называют trigger, имеет значение для компонентов модуляции. При его установке в Sweep Unit происходит следующее:
- Частота канала (NRX3 и NRX4) копируется в регистр-буфер частоты.
- Счетчик сбрасывается. Для этого из регистра NRX0 нужно скопировать биты 6-4, т.е. в счетчике окажется число отсчетов на частоте 128 Гц. Frame Sequencer будет генерировать отсчеты на этой частоте, поэтому и счетчик должен ей соответствовать. Как видите, нет лишних преобразований, если все делать правильно.
- Флаг активности устанавливается, если период или шаг не равны нулю. Иначе сбрасывается.
- Если шаг не равен нулю, то тут же происходит вычисление новой частоты и ее проверка на переполнение (не более 2047), но новая частота не сохраняется – все делается лишь ради проверки на переполнение.
И так, что же происходит, когда счетчик «говорит», что пора обновить частоту. Сначала мы сбрасываем счетчик. Затем проверяем флаг активности – если он установлен, то происходит вычисление новой частоты согласно формуле выше с той разницей, что в качестве F(t – 1) выступает регистр-буфер частоты. Тут же происходит проверка на переполнение – если новая частота превысила 2047, то канал отключается.
Если переполнения не было и шаг не равен нулю, то новое значение частоты записывается в регистры NRX3 и NRX4, а так же в регистр-буфер частоты. Сразу же происходит еще одно вычисление новой частоты и проверка на переполнение, но эта частота не сохраняется – это все делается только ради еще одной проверки переполнения.
Регистр-буфер частоты здесь опустить не получится. Его присутствие приводит к тому, что полноценный ручной контроль частоты канала во время работы Sweep unit невозможен. Мы можем изменить частоту сами, но она будет таковой до тех пор, пока не будет произведен отсчет Sweep unit – поскольку он использует в вычислениях регистр-буфер частоты, то наше значение частоты будет проигнорировано и перезаписано, а вычисления продолжатся, как ни в чем не бывало.
Теперь две странности, которых у компонентов модуляции в DMG предостаточно:
- Как было упомянуто ранее, период со значением 0 означает, что Sweep unit отключен. Это логично и должно было бы быть именно так, но в реальности для DMG период со значением 0 в регистре NRX0 означает, что период равен 8. Никаких вычислений частоты не происходит, просто Sweep unit работает вхолостую. Тестовые ROM’ы это проверяют.
- Представим такой сценарий. Игра установила режим уменьшения частоты. Прошло некоторое время и успело произойти вычисление новой частоты. Если после этого игра попытается установить режим увеличения частоты, то канал будет тут же отключен. Таким образом, если в режиме уменьшения частоты произошло хоть одно вычисление новой частоты, то смена режима на увеличение частоты отключает канал.
Length counter
Данный компонент является простейшим счетчиком. Он отмеряет определенное кол-во отсчетов, а затем отключает подключенный к нему звуковой канал. Данный компонент присутствует у всех каналов и использует для своей работы регистр NRX1 – в нем хранится длительность воспроизведения канала. Приводить его структуру не буду – у всех каналов отведено различное число бит под значение длительности. Помимо этого для Length Counter имеет значение бит 6 в регистре NRX4, который вкл/откл данный компонент.
Frame Sequencer генерирует отсчеты для этого компонента на частоте 256 Гц. Значения в регистре NRX1 указаны в отсчетах на такой частоте, что, в очередной раз, означает свободу от конвертирования. При каждом отсчете изменения записываются обратно в регистр – регистр NRX1 и является счетчиком.
Перед использованием значения длительности из регистра NRX1 его надо преобразовать по следующей формуле:
Counter = (~NRX1 & Mask) + 1,
где Counter – счетчик, Mask – маска, с помощью которой из регистра извлекается только значение длительности (в нем иногда еще находится коэффициент заполнения). Это и даст нам число отсчетов на частоте 256 Гц.
Вроде бы все предельно очевидно – элементарный счетчик. При очередном отсчете проверяем флаг в регистре NRX4 на истинность и в случае успеха помечаем отсчет в счетчике компонента. Если счетчик достиг конечного значения, то канал отключается. Сложности возникают при реализации очередных странностей – все они сосредоточены в обработчике изменений регистра NRX4:
- Если компонент был выключен и сейчас включается посредством бита 6, счетчик не достиг нуля, а текущее состояние Frame Sequencer осуществляет отсчет нашего компонента (все четные состояния), то мы тут же осуществляем отсчет. Это может привести к тому, что счетчик достигнет нуля и канал будет отключен, но тут надо учитывать еще одно условие – отключение канала здесь происходит только в случае, если не осуществляется перезапуск канала, т.е. бит перезапуска равен нулю.
- Если осуществляется перезапуск и счетчик достиг нуля, то в регистр NRX1 записывается максимально возможное значение длительности, т.е. все биты длительности обнуляются (см. формулу выше). Если при этом осуществляется включение компонента (предыдущее значение не важно) и текущее состояние Frame Sequencer осуществляет отсчет нашего компонента, то тут же производим отсчет. Тут уже условий никаких не надо – отключился канал, значит отключился.
Envelope unit
Данный компонент контролирует громкость звука, уменьшая или увеличивая ее с постоянным шагом с определенным периодом. Громкость в данном случае означает амплитуду генерируемого сигнала. Контролируется данный компонент посредством регистра NRX2:
Биты | Назначение |
7-4 | Начальное значение амплитуды |
3 | Режим: 0 – уменьшение 1 – увеличение |
2-0 | Период |
Frame Sequencer генерирует отсчеты для этого компонента на частоте 64 Гц. Значения в регистре NRX2 указаны в отсчетах на такой частоте. При каждом отсчете происходит отсчет внутреннего счетчика периода. Когда он отсчитает один период, то значение амплитуды увеличивается/уменьшается на единицу. При достижении граничных значений вычисления прекращаются. При амплитуде 0, очевидно, канал заглушен, но активен.
На рисунке выше показан график изменения амплитуды сигнала при работе Envelope unit со значением NRX2 равным 0x55. Начальное значение амплитуды устанавливается при перезапуске канала (trigger) – в данном случае оно равно 5. В процессе работы оно больше не используется и не модифицируется. Далее с каждым периодом амплитуда уменьшается на один до тех пор, пока не достигнет нуля.
Теперь очередные странности. Сначала при модификации NRX4:
- Как и для Sweep unit период 0 означает, что период равен 8. Опять же, никаких вычислений не происходит – компонент работает вхолостую.
- Если канал перезапускается, и следующее состояние Frame Sequencer генерирует для Envelope отсчет, то счетчик периода устанавливается на единицу больше, чем должен быть.
- Если канал перезапускается, начальное значение амплитуды равно нулю и выставлен режим уменьшения амплитуды, то канал тут же отключается.
Теперь при модификации NRX2:
- Если текущее (новое значение пока не записали) значение NRX2 содержит период, равный нулю, а счетчик периода еще не закончил отсчет (мы же помним, что период 0 означает 8), то необходимо увеличить на единицу текущее значение амплитуды. В противном случае, проверяем текущий режим – если это уменьшение, то амплитуду надо увеличить на 2. Т.е. это все приведет к немедленному повышению амплитуды генерируемого каналом сигнала.
- Если происходит смена режима, то амплитуда устанавливается равной 16 минус значение амплитуды.
- После всех операций значение амплитуды обрезается до младших 4 бит.
Envelope unit имеет наиболее странное поведение из всех, но, к сожалению, документированы лишь упомянутые странности. Поведение на реальном DMG намного сложнее, но ни один эмулятор не может похвастаться его точной реализацией.
Звуковой канал 3
Данный звуковой канал генерирует волну согласно содержимому в Wave Pattern RAM, которая расположена в области памяти I/O ports. Wave Pattern RAM имеет длину 16 байт и содержит 32 сэмпла. Каждый байт содержит по 2 сэмпла – первый сэмпл в старших 4 битах, второй в младших 4 битах. Содержимое памяти проигрывается циклично с заданной в регистрах NR33 и NR34 частотой (структура этих регистров идентична 1 и 2 каналам).
Этот канал содержит только Length Counter из компонентов модуляции – его работа идентична остальным каналам и контролируется с помощью регистра NR31. Амплитуда сигнала регулируются вручную. Значение амплитуды устанавливается с помощью регистра NR32, в котором используются только 6 и 5 биты. Они могут иметь следующие значения:
- 00: канал заглушен, но активен.
- 01: Wave Pattern RAM воспроизводится как есть.
- 10: Wave Pattern RAM воспроизводится с предварительным сдвигом каждого сэмпла на 1 бит вправо.
- 11: Wave Pattern RAM воспроизводится с предварительным сдвигом каждого сэмпла на 2 бита вправо.
Помимо этого в регистре NR30 в бите 7 содержится флаг, разрешающий воспроизведение звука (другие биты не используются). Если он равен 0, то запрещено. Иначе – разрешено. Важно понимать, что этот флаг не имеет однозначного соответствия с битом статуса в регистре NR52 для этого канала. Если бит 7 в NR30 установлен, то звук может быть воспроизведен – бит статуса для этого канала в NR52 так и может остаться нулевым, а звук не будет выводиться. Воспроизведение разрешено, но не запущено. Если же флаг сбрасывается, то это приводит и к отключению канала, что ведет к сбросу бита статуса в NR52.
Таймер в Wave generator работает с периодом 2 * (2048 – X), где X – частота из регистров NRX3 и NRX4. В данном случае частота означает не частоту звука, а частоту, с которой происходит чтение очередного сэмпла из Wave Pattern RAM.
Канал 3 помимо всего прочего содержит указатель на текущий сэмпл и буфер сэмплов – их присутствие в эмуляторе обязательно для точной эмуляции. При очередном отсчете таймера указатель передвигается на новую позицию, а в буфер сэмплов копируется текущий байт (очевидно, что байт будет один и тот же на протяжении каждых двух отсчетов). Далее в дело вступают очередные странности:
- При перезапуске канала указатель обнуляется, но в буфер сэмплов не копируется первый байт из Wave Pattern RAM – это произойдет только при следующем отсчете таймера. Это означает, что сначала будет проигран первый сэмпл из буфера сэмпла, который все еще содержит старый байт; при следующем отсчете таймера в буфер будет скопирован первый байт из Wave Pattern RAM, указатель передвинут на второй сэмпл, а значит будет проигран второй сэмпл из Wave Pattern RAM. Далее все продолжится как должно. В итоге, при перезапуске первый сэмпл не будет проигран до тех пор, пока вся Wave Pattern RAM не будет проиграна хотя бы один раз.
- Если канал 3 работает, то игра имеет доступ к Wave Pattern RAM только, если в этот же момент канал 3 производит чтение из нее. Иначе операции чтения возвращают 0xFF, а операции записи игнорируются. Если канал 3 работает и происходит чтение или запись в момент, когда канал 3 читает сэмпл, то операции происходят только на байте, на который указывает указатель текущего сэмпла — не имеет значение, откуда происходит чтение или запись. Операции чтения и записи работают нормально только при отключенном канале 3 – тогда можно читать и записывать в любом месте Wave Pattern RAM.
- Перезапуск канала 3 во время чтения сэмпла приводит к повреждению четырех первых байт в Wave Pattern RAM. Если указатель текущего сэмпла в пределах первых четырех байтов, то первый байт Wave Pattern RAM будет переписан содержимым буфера сэмплов. Если указатель текущего сэмпла в другой позиции, то будут переписаны все 4 первых байта содержимым той четверки байтов (4-7, 8-11, 12-15), где находится указатель. Например, если указатель на 10 байте, то содержимое первых четырех байт будет переписано байтами 8-11.
С первым пунктом все элементарно. Остальные уже не так уж просто реализовать, тем более, когда в интернете нет никаких упоминаний о тонкостях реализации. Их реализация в CookieBoy – следствие практически случайных попыток манипуляции со счетчиком таймера, перемещающего указатель текущего сэмпла. Вот, что мне удалось раскопать.
И так. Ключ к реализации последних двух пунктов – понимание того, что происходит при перезапуске канала (trigger) посредством регистра NR34. Очевидно, что нам надо сбросить счетчик таймера и указатель текущего сэмпла. Указатель сэмпла сбрасываем согласно первому пункту выше – тут все просто. Со счетчиком все не так просто, тут и кроется ключ к решению проблемы.
Обнуление счетчика при перезапуске канала – это очевидное и неправильное решение. На самом деле счетчик инициализируется таким образом, что происходит задержка перед тем, как начинается обновление позиции текущего сэмпла. Задержка равняется периоду таймера (формулу я уже приводил) плюс некая константа (скорее всего не больше 8 тактов), которую вам придется подобрать самим. Т.е. вместо того, чтобы отсчитать один период и обновить позицию указателя, таймер отсчитывает два периода плюс некая константа. После этого таймер работает в штатном режиме, отсчитывая положенный один период.
Вот как это работает в моем эмуляторе. Переменная ClockCounter является счетчиком тактов. Она имеет знаковый тип. Как только она достигает значения, равного периоду таймера, я обновляю позицию указателя текущего сэмпла и сбрасываю счетчик (вычитаю из него значение периода). При перезапуске канала посредством NR34 я устанавливаю ClockCounter = -Period — 3, где Period – значение периода таймера в тактах согласно приведенной ранее формуле, 3 – та самая магическая константа. Это дает необходимую задержку и позволяет узнать, в какой момент времени можно производить чтение/запись Wave Pattern RAM. Если в момент чтения или записи в Wave Pattern RAM переменная ClockCounter равна 3, то эти операции доступны. Иначе – возвращаем 0xFF.
Теперь указатель сэмплов. При перезапуске я записываю в него 1. Именно эта комбинация задержки и значения указателя сэмплов при перезапуске позволяет пройти тестовые ROM’ы. Не забываем только про тот факт, что второй проигрываемый сэмпл после перезапуска – это второй сэмпл в Wave Pattern RAM. Из-за задержки будет два раза проиграно старое содержимое буфера сэмплов (см. первую странность), а затем третий сэмпл из Wave Pattern RAM. Такова особенность моей реализации, поэтому, как только таймер после перезапуска канала пройдет всю задержку (станет неотрицательным), я обновляю содержимое буфера сэмплов.
С повреждением четырех первых сэмплов все элементарно, только теперь ClockCounter должен быть равен 1, чтобы происходило повреждение и перезапись первых байтов Wave Pattern RAM.
Не забываем, что перезапуск канала – это не просто запись в NR34. Все перечисленное и сам перезапуск происходит только тогда, когда в старший бит NR34 записывается 1 и регистр NR30 разрешает воспроизведение (старший бит установлен).
Звуковой канал 4
Этот канал генерирует шум. К нему подключены Length counter и Envelope unit – их поведение ничем не отличается от такового в других каналах. Под них отведены те же самые регистры – NR41 и NR42 соответственно. Данный канал не содержит частоты в привычном понимании – NR43 используется для совершенно других целей, а NR44 содержит все привычные флаги, но биты под частоту не используются.
Генератор шума основан на так называемом LFSR – Linear Feedback Shift Register или регистр сдвига с линейной обратной связью. Это генератор псевдослучайной битовой последовательности. Принцип его работы достаточно прост.
Регистр сдвига представляет собой хранилище для битовой последовательности определенной длины (в DMG регистр сдвига может иметь длину 7 или 15 бит). Определенные биты регистра сдвига отмечаются как отводы (taps) – именно благодаря им генерируется последовательность. В DMG отводами являются 0 и 1 биты регистра сдвига. Для непрерывной работы LFSR используется тактовый генератор, который генерирует отсчеты для вычисления очередного бита псевдослучайно последовательности.
В начале регистр сдвига инициализируется любой ненулевой битовой последовательностью – если все биты будут равны нулю, то на выходе LFSR мы всегда будем получать ноль. При очередном отсчете происходит следующее:
- Отводы суммируются по модулю 2 (операция XOR), и результат сохраняется для дальнейших операций.
- Крайний правый бит (бит 0) регистра сдвига берется в качестве очередного значения генерируемой последовательности.
- Регистр сдвига сдвигается вправо на один бит.
- В освободившийся крайний левый бит записывается результат суммы отводов по модулю 2, который мы сохранили на первом шаге.
На выходе получается псевдослучайная битовая последовательность. Псевдослучайная она по той причине, что она имеет период – с определенного момента вся последовательность зацикливается. Длина периода (T) вычисляется по следующей формуле:
T = 2N – 1,
где N – длина регистра сдвига в битах. Период определяется максимальным числом различных состояний регистра сдвига кроме одного, когда все биты равны нулю. Таким образом, для 7-битного регистра период будет равен 127, а для 15-битового равен 32767. Это приводит нас к вопросу – вычислять все честно или же использовать заранее сгенерированные последовательности. Результат будет идентичен, поскольку LFSR гарантированно зацикливается. Я использовал второй подход. Последовательности можно найти в файлах LFSR7.inc и LFSR15.inc.
Для управления LFSR используется регистр NR43. Вот его структура:
Биты | Назначение |
7-4 | Сдвиг частоты таймера: 0000: 1/2 0001: 1/22 0002: 1/23 0003: 1/24 … 1101: 1/214 1110: не используется 1111: не используется |
3 | Длина регистра сдвига: 0: 15 бит 1: 7 бит |
2-0 | Множитель частоты: 000: 2 001: 1 010: 1/2 011: 1/3 100: 1/4 101: 1/5 110: 1/6 111: 1/7 |
С длиной регистра сдвига все понятно. Остальные биты используются для вычисления частоты тактового генератора LFSR. Вычисляется она (F) по следующей формуле:
F = f * Shift * Ratio,
где f = 4194304 Гц, Shift – сдвиг частоты таймера (значения указаны в таблице выше), Ratio – множитель частоты (значения указаны в таблице выше). Если биты сдвига частоты равны 1110 или 1111, то LFSR не получает отсчетов, а значит канал 4 заглушен.
Реализация
Для реализации звука я выбрал SDL. Эта библиотека имеет предельно простой API для генерации процедурного звука – указываем параметры звука, длину буфера сэмплов, callback-функцию и все. SDL автоматически вызывает эту функцию, где мы и «скармливаем» ей очередную порцию сэмплов. После их воспроизведения функция вызывается еще раз и т.д. Помимо простого API еще одно преимущество SDL – хорошая работа с предельно малыми буферами сэмплов, а для нас как раз очень важна латентность.
Я не буду вдаваться в подробности реализации самих компонентов звуковой системы. Теоретическая часть содержит все необходимое. Лишь коснусь проблемы синхронизации.
Проблема заключается в том, что теперь нам нужно поддерживать не только темп обновления экрана, но и темп генерации сэмплов. SDL вызывает callback-функцию через равные (хотя гарантий этого в документации я не видел) промежутки времени и «ожидает», что мы запишем новую порцию сэмплов. Если этих сэмплов не будет в нужный момент, то мы получим прерывистый звук. В тоже время, может оказаться, что темп эмуляции слишком высокий и очередные порции сэмплов надо будет где-то сохранить для воспроизведения позже.
Для хранения сэмплов лучше всего подходит кольцевой буфер. Эмулятор пишет в него порции сэмплов, а callback-функция забирает их при необходимости. Кольцевой буфер решает сразу несколько проблем:
- нам не надо следить за границами массива – данная структура данных все сделает за нас. Бонусом является еще и то, что кольцевой буфер имеет фиксированный размер;
- хранение порций сэмплов в правильном порядке;
- упрощение реализации ускоренной эмуляции. Дело в том, что при отсутствии достаточного количества свободного места кольцевой буфер перезаписывает уже имеющиеся данные. При ускоренной эмуляции порции сэмплов будут поступать быстрее, чем мы сможем их воспроизвести. Поскольку мы никого не ждем и пишем поверх старого, то темп звука будет соответствовать темпу эмуляции;
- поддержание темпа эмуляции. Это несколько противоречит предыдущему пункту, но дело вот в чем. Если поступила очередная порция сэмплов, а в кольцевом буфере нет для нее места, то вместо перезаписи мы ждем, пока callback-функция не заберет из буфера порцию сэмплов. Только после этого мы добавляет очередные сэмплы в буфер и продолжаем эмуляцию.
Последний пункт дает интересный побочный эффект – мы можем полностью отказаться от ручного поддержания темпа эмуляции (60 Гц). Необходимые задержки в эмуляции обеспечит ожидание вызова callback-функции. Для этого в SDL есть условные переменные (SDL_cond). С помощью них поток переходит в режим ожидания и ждет сигнала от другого потока о том, что можно продолжить работу. Для нас ждущим потоком является поток эмуляции – он ждет, когда другой поток (callback-функция) заберет из кольцевого буфера сэмплы и тем самым освободит место для очередной порции. Когда же нам нужна максимально возможная скорость эмуляции, мы никого не ждем и пишем в кольцевой буфер. Естественно не забываем про мьютексы.
Все так хорошо работает по одной простой причине – генерация сэмплов происходит с тем же темпом, с каким работает процессор DMG.
Тестирование
Как и для других компонентов, для звука тоже есть тестовые ROM’ы. Есть только одна хитрость – для DMG и Gameboy Color комплекты тестов разные и запускать стоит их все. Тесты DMG должны быть пройдены без ошибок, а вот тесты для Gameboy Color реальный DMG проходит с ошибками и выводит на экран следующее:
Если при запуске всех тестов сразу их выполнение не прекращается, а зацикливается, то беспокоиться не о чем. Это именно тот случай, когда ROM пытается установить банк ROM’а, которого не существует. Если обрезать номер банка, как это делаю я, то тесты зацикливаются. Тоже самое наблюдается и у Gambatte, а ему доверять можно.
Рекомендую сразу вооружиться исходным кодом тестов и разобраться в том, как они работают. Это намного ускорит процесс, а иной раз это единственный способ понять, что вам нужно сделать. На экран хоть и выводится описание ошибки, но порой сложно понять, что конкретно от вас требуется и какие регистры задействованы.
И так, самые простые тесты – это «01-registers» и «11-regs after power». Первый тестирует то, как происходит чтение и запись регистров звука. Я уже упоминал про то, как стоит учитывать неиспользуемые и недоступные для чтения биты регистров – именно это и проверяет тест. Помимо этого, проверяются результаты операций при вкл/выкл звуке. Второй тестирует поведение регистров при выключении звука. В исходном коде написано подробнее, что конкретно тестируется.
«02-len ctr» тестирует поведение Length counter в граничных условиях. Тестирование происходит для каждого канала отдельно, в процессе выводится номер протестированного канала.
«03-trigger» еще один тест для Length counter, но теперь проверяется его поведение при модификации регистра NRX4. В основном, именно здесь тестируются все упомянутые странности Length counter.
«04-sweep», «05-sweep details» и «06-overflow on trigger» тестируют Sweep unit. Помимо нормальной работы здесь тестируются все упомянутые странности компонента. Для прохождения теста «06-overflow on trigger» в ходе него на экран должно быть выведено:
«07-len sweep period sync» тестирует правильность синхронизации Sweep unit и Length counter. Если Frame Sequencer реализован правильно, то проблем с этим тестом быть не должно.
«08-len ctr during power» тестирует поведение Length counter при выключении звука. Для прохождения теста в ходе него на экране должно быть выведено:
«09-wave read while on» тестирует операции чтения во время работы канала 3. Для прохождения теста на экране должно быть выведено:
На экран через раз выводится значение из Wave Pattern RAM и FF из-за запрета на чтение. Они чередуются (00 FF, 11 FF и т.д.), но в начале операция чтения не проходит два раза и выводится FF FF. Как раз этого добиться сложнее всего, и потребовало у меня перебора разных значений константы и счетчика (определение момента, когда операция чтения разрешена).
«10-wave trigger while on» тестирует повреждение первых байтов Wave Pattern RAM при перезапуске канала 3 во время чтения сэмпла. На экран выводится очень много информации, и полностью она не помещается. Вот результат пройденного теста:
Даже по этому куску видно, где должно происходить повреждение Wave Pattern RAM.
«12-wave write while on» тестирует операции записи в Wave Pattern RAM при работе канала 3. Тут тоже выводится слишком много информации, вот результат пройденного теста:
Вполне видно, где и что искать – тест пытается записать значение 0xF7.
Стоит сказать, что прохождение этих тестов лично мне ничего не дало в протестированных мной играх. И без их прохождения звук был нормальный, а судя по другим эмуляторам с завидной совместимостью, проваливающих все тесты звука – это вообще не нужно. Хотя приятно осознавать, что эмулятор работает так, как реальное железо. Если и попадется игра, зависимая от тонкостей железа, то она будет работать корректно.
Что дальше
На данный момент наш эмулятор поддерживает все обязательные и не совсем функции DMG. Естественно есть куда развиваться, а именно:
- Поддержка последовательного порта или Game link.
- Поддержка малораспространенных MBC-контроллеров – Pocket Camera, Bandai TAMA5, Hudson HUC-3, Hudson HUC-1, картриджи с вибрацией.
Напоследок остается одна особенность DMG, которую ни один из мне известных эмуляторов не поддерживает, даже самые точные вроде Gambatte. Это баг железа, который приводит к записи мусора в область памяти OAM. Эта особенность практически никак не документирована и довольно коварна в реализации.
Баг происходит при исполнении определенных инструкций процессора с определенными значениями операндов. Это простая часть. Сложнее эмулировать тот факт, что мусор записывается совсем не случайно и имеет определенное содержание. Еще сложнее эмулировать то, что данный баг происходит только в определенные промежутки времени работы LCD-контроллера. Именно для этих деталей я не нашел никакой документации – есть только тестовые ROM’ы, которые не очень помогают. С ними можно реализовать этот баг лишь частично.
Смысла в реализации этого бага минимум – вряд ли какой-то разработчик сознательно его использовал или оставил в релизе игры. Теже баги звука из-за Wave Pattern RAM встречаются в играх, и в них есть практический смысл.
Заключение
На этом цикл статей подошел к концу. Как я и говорил, итогом является эмулятор с хорошей совместимостью и поддержкой всех самых важных функций. Такой не стыдно поставить рядом с другими реализациями. А благодаря использованию тестовых ROM'ов для реализации и тестирования можно говорить о высокой точности эмуляции железа, а не простой совместимости с играми.
Естественно, эмулятору есть куда развиваться. Помимо упомянутого, есть еще одно логичное направление развития эмулятора — эмуляция Gameboy Color (СGB). DMG и CGB являются не просто консолями одного семейства — они практически идентичны внутренне. К существующему эмулятору необходимо дописать буквально несколько модулей.
На данный момент Cookieboy не эмулирует CGB, но я планирую заняться этим в ближайшее время. Об этом тоже будет статья.
Если кто-то решится на реализацию своего эмулятора или же что-то непонятно в самих статьях, то можете обращаться с вопросами, буду рад помочь. В комментариях к статьям или в хабрапочту — неважно.
Автор: creker