Top.Mail.Ru

Как заставить Arduino петь как ZX Spectrum. Часть 2: музыка Dizzy IV на Arduino Nano

В этой части статьи мы перейдем к самому интересному — будем разбирать музыкальный модуль Dizzy IV по винтиками и воспроизводить мелодию сначала на Windows, а потом и на Arduino Nano.

О чем идет речь, я подробно объяснял в первой части статьи.

Disclaimer:
Хочу отметить, что проект я реализовал исключительно для фана, понимая, сколько лет прошло с момента, когда спектрум был актуален. Хотелось вспомнить былое и прикоснуться к коду, написанному для восхитительного процессора Z80. Для меня этот процессор был первым. 

Музыкальную тему для Dizzy 4 написал Lyndon Sharp, а не David Whittaker, как указано на многих ресурсах, посвященных игре. Из кода плеера видно, что автором заложена кроссплатформенность — в нем используется буфер для регистров звукогенератора. Вместо прямого доступа через порты ввода-вывода, значения для регистров звукогенератора сначала записываются в буфер в памяти, что кажется оверхедом. Данные из буфера в регистры переносятся в простом цикле. Процесс создания музыки на другой платформе, а также существование игры с той же музыкой для Amstrad CPC, объясняет наличие такого буфера.

Разбираем музыкальный модуль

Я извлек музыкальный модуль из tap-файла и подверг анализу в эмуляторе Unreal Speccy и в дизассемблере IDA Pro. Полный листинг модуля с моими комментариями можно найти тут.

Если вкратце, это модуль длиной 0x4000 (или 16384, ах, какие числа) байт. Он загружается по адресу 0xC000 и имеет две мелодии с адресами 0xC000 и 0xD300. Обе мелодии укомплектованы идентичным плеером, так что сначала идет код плеера, а сразу за ним располагаются данные позиций, паттерны и инструменты. Думаю, так поступали в большинстве случаев — один плеер на несколько мелодий делали редко, несмотря на возможную экономию.

Сам плеер интересен тем, что для каждой позиции позволяет настроить свой банк инструментов, таким образом один и тот же паттерн может звучать по-разному, в зависимости от того, какие инструменты настроены. Это можно в какой-то мере сравнить с цветовой палитрой в старых графических режимах.

Модуль состоит из следующих блоков (где это возможно, будет указан и адрес окончания блока):

  • 0xC000 — 0xC01B — (init) — блок инициализации, здесь происходит установка начальной скорости, адресов позиций и инструментов.
  • 0xC01C — 0xC032 — (play_loop) — главный цикл. В нем благодаря команде ‘halt’ осуществляется задержка в 20 мс, т.е. тело цикла выполняется 50 раз в секунду. Здесь же осуществляется управление мелодией и проверка, не нажали ли в данный момент клавишу пробел, чтобы выйти из плеера. Этот код нужен был, чтобы можно было прослушать мелодию без дополнительного кода, но в играх он был бесполезен, потому что там был свой код, выполняющийся по прерыванию каждые 20 мс и вызывающий код плеера, чтобы тот сменил состояние мелодии. Так мелодии можно было проигрывать в фоне, не блокируя выполнение кода игры.
  • 0xC033 — (play_frame) — именно это место должно вызываться 50 раз в секунду, чтобы звучала мелодия. Здесь идет подсчет количества вызовов. Если оно превышает скорость, заданную автором композиции, происходит смена нот. В этих мелодиях скорость всегда постоянна, а переход к следующей ноте происходит каждое седьмое прерывание, т.е. скорость всех мелодий в модуле — 7. Современную музыку для AY-3-8912 стараются писать со скоростью 3, так можно уместить больше деталей. Очевидно, чем меньше число, задающее скорость, тем чаще происходит смена нот.
  • 0xC043 — (init_next_note) — код вызывается при каждой смене нот.
  • 0xC051 — (init_next_position) — если ноты в паттерне заканчивается, то надо перейти к следующей позиции или начать мелодию сначала.
  • 0xC06C — (init_pattern) — код инициализации следующего паттерна.
  • 0xC082 — (init_next_note_in_current_pattern) — большой кусок кода по инициализации следующей ноты в паттерне. Здесь четырежды вызывается подпрограмма init_note_in_channel, которая читает управляющие команды и высоту нот, которые надо проиграть из данных паттерна, и сохраняет эти данные в оперативной памяти. Замечу, что четвертый канал отсутствует в звукогенераторе. Очевидно, этот псевдо канал задумывался для специальных команд управления состоянием. Не ясно, почему эти команды нельзя было помещать в оставшиеся три канала, сэкономив память. Кстати, здесь широко используются индексные регистры ix и iy, которые выгодно отличали процессор Z80 от его прародителя Intel 8080. При должном использовании они являлись своего рода контекстом исполнения, эдакий this, если угодно. Использование iy также указывает, что плеер не старались сделать универсальным, потому что этот регистр использовался во встроенном бейсике и изменять его в прерываниях (а плеер мог вызываться и через прерывания) не рекомендовалось.
  • 0xC0B4 — (manage_current_note) — если сменять ноты надо было не каждые 20 мс, а в соответствии с заданной скоростью, то текущие проигрываемые ноты нуждались в обслуживании постоянно. Этот код трижды вызывает подпрограмму manage_current_note_in_channel, обслуживающую инструменты, — по одному разу для каждого канала. Напомним, что инструменты могут динамически менять частоту ноты (сэмпл) или высоту тона ноты (орнамент).
  • 0xC0F5 — 0xC1E2 — (manage_current_note_in_channel) — обслуживание инструмента для данной ноты в канале. Наряду с инструментом, здесь же обрабатываются и шумовые эффекты. По результатам вычисления нового состояния для инструментов в этой процедуре заполняется буфер регистров, который позже передается в регистры звукогенератора.
  • 0xC1E3 — (ay_out) — перенос данных из буфера в регистры звукогенератора.

Отступление про часто использовавшийся хак, который недоступен современным программистам

Рассмотрим код счетчика количества нот в паттерне, который по завершении паттерна переходит к следующей позиции:

По адресу 0xC043 в регистр `A` загружается значение 0x3A. Казалось бы, это бесполезное действие, потому что следующая команда уменьшает это значение на единицу. Почему бы сразу не загрузить в регистр `A` значение 0x39?

Идея заключается в том, что команда по адресу 0xC048 записывает уменьшенное (и, кстати, защищенное от переполнения благодаря инструкции `AND 3Fh`) значение по адресу операнда первой команды (0xC043), прямо в код программы, так что в следующий раз команда `LD A, …` считает значение на единицу меньше, через раз — еще меньше и так далее. Другими словами, по адресу 0xC044 хранится счетчик, который уменьшается на единицу, каждый раз при выполнении данного кода.

К счастью, сейчас память программ доступна только для чтения либо по аппаратным причинам, как например в микроконтроллерах, либо из-за запрета прямой записи в область кода операционными системами и процессорами.

Реализация на Windows

Я начал с того, что написал бОльшую часть кода сначала для Windows, чтобы было легче производить отладку. “Борьбу” с Arduino оставил на момент адаптации кода к аппаратным возможностям платы. Мою реализацию для Windows можно собрать самостоятельно (каталог win-implementation у меня на GitHub; понадобится компилятор от Visual Studio 2017) или запустить готовые сборки: мелодия 1мелодия 2.

Пара комментариев о том, как все реализовано.

Чтобы не возникло потом проблем с переносом, я старался писать код, ориентируясь на восьмибитную природу микроконтроллеров ATMega. Поэтому он изобилует типами ‘uint8_t’ и ‘uint16_t’.

Для преобразования исходного музыкального файла в код, пригодный для компиляции, я написал небольшой скрипт на python (каталог extractor у меня на GitHub), который копирует данные мелодии в массив байт и извлекает некоторые адреса:

  • адрес начала данных мелодии (без учета плеера),
  • адрес таблицы позиций,
  • адрес таблицы банков инструментов для позиций.

В оригинальном спектруме звукогенератор AY-3-8912 производил сигналы прямоугольной формы, которые раньше мне казались грубоватыми. Изначально цели добиться аутентичного звучания передо мной не стояло, поэтому в первой версии я пробовал генерировать синусоидальные сигналы, вместо меандра. Оказалось, что вместе с прямоугольной формой сигнала ушли и все гармоники, из-за чего звук стал очень глухой. Так что от синусоидальной формы сигнала пришлось отказаться. Также в реализацию была добавлена таблица маппинга громкости. Напомню, что амплитуда выходного сигнала звукогенератора имеет 15 градаций. Энтузиасты, разрабатывающие эмуляторы AY-3-8912, установили, что зависимость между значением в регистре громкости и напряжением на выходе вовсе не линейная. Так появились таблицы для соответствующего преобразования. В “больших” эмуляторах такие таблицы 16-битные, в нашем же эмуляторе для быстроты расчетов используется 4 бита. Поэтому если заглянуть в исходники, можно увидеть, что уровни громкости от 0 до 3 вообще не воспроизводятся, а уровни от 4 до 9 имеют незначительную громкость на выходе. Действительно, субъективно я это почувствовал еще во время моих экспериментов по написанию музыки. Думаю, что инженеры, разрабатывающие звукогенератор, сделали это намеренно, в соответствии с законом Вебера-Фехнера (интенсивность ощущения пропорциональна логарифму интенсивности раздражителя).

В музыкальном модуле от Dizzy IV используются несложные шумовые эффекты. Их удалось реализовать при помощи простого генератора случайных чисел, работающего по алгоритму xorshift.

Реализация на Arduino

Поскольку проект делался исключительно для веселья, я выбрал плату Arduino Nano. У нее широко распространенный микроконтроллер, но он менее всего подходит для решения этой задачи. Можно было бы выбрать, например STM32, но там есть даже ЦАП, поэтому было бы совсем неинтересно, тем более на STM32 удалось запустить эмуляцию ZX Spectrum, включая генерацию видеосигнала. Что уж говорить о Raspberry Pi и аналогах, ведь на них можно эмулировать спектрум, не написав ни строчки кода.

Генерация ШИМ сигнала обеспечивается TIMER2, вывод OC2B или PD3 (или вывод D3 в терминологии Arduino). Частота ШИМ сигнала выбрана достаточно высокой — 31373 Гц, поэтому выход Arduino удалось подключить напрямую к портативной колонке без каких-либо фильтрующих цепей, посторонние призвуки отсутствовали.

На плате Arduino Nano можно управлять только двумя светодиодами. Я сделал так, что интенсивность одного из них зависит от громкости мелодии. Для простоты реализации, ШИМ-сигнал для этого светодиода формируется из прерывания второго таймера. Это происходит достаточно часто, но до тех пор пока нет цели сэкономить процессорное время, можно оставить так. Другой светодиод управляется через преобразователь uart -> usb, тут удалось вывести на него индикацию канала шума.

Выбор мелодии пока жестко задан программно, но его можно легко поменять, вызывая функцию ‘get_music_data_1’ или ‘get_music_data_2’. Для первой композиции главный голос находится в канале A, для второй — в канале B, это надо учитывать при визуализации мелодии на светодиодах.

Проект можно собрать из каталога arduino-sketch и послушать мелодии на своем Arduino или посмотреть получившийся результат на видео.

Видео мелодии 1:

Видео мелодии 2:

Как я уже писал, все исходники проекта можно найти на Github.

Автор статьи: Антон Дмитриевский, Максилект.

P.S. Когда-то мы с другом делали клон известной игры “Color Lines” (тогда было модно делать клоны именно этой игры, их тысячи) и там в дополнение к красивой графике у нас тоже была достойная, на мой взгляд, музыка. Представляю вашему вниманию интро для этой игры. А если возникнет желание послушать внутриигровую музыку, то придется запустить игру в эмуляторе — образ диска TR-DOS можно скачать здесь, после запуска необходимо нажать на пункт меню “Выбор мелодии”.

Видео с интро:

P.P.S. Выражаю благодарность всем авторам эмулятора Unreal Speccy, участникам проекта speccy.info.

Наши статьи по теме:

Все статьи

Связаться с нами

Мы свяжемся с вами в течение 24 часов.