OPL синтез на AY (часть 2)
djnzx48
8-битные сэмплы
Теперь я объясню способ вывода, который
я использовал в этом проигрывателе.Если вы
знакомы с микросхемой AY, используемой в
Spectrum 128, вы знаете, что каждый канал,
которых всего три, может воспроизводить
только 16 отдельных уровней громкости ( 31
для YM, но это более сложно и требует ис─
пользования огибающих). Итак, как это поз─
воляет нам выводить 8-битные сэмплы? Мы
можем сделать это, объединив уровни гром─
кости из трёх доступных каналов. Поскольку
расстояние между соседними уровнями гром─
кости варьируется, мы можем комбинировать
их для создания промежуточных уровней и
получения 8-битного значения.
У этого метода есть и недостатки. Одним
из таких недостатков является то, что каж─
дая выборка требует изменения трех регист─
ров, а не одного, а несколько регистров не
могут быть изменены мгновенно. Пока регис─
тры находятся в промежуточном состоянии,
возникают нежелательные промежуточные уро─
вни громкости, вызывающие искажения. Ещё
одним недостатком является то,что этот ме─
тод работает только для монофонического
аудиовыхода с AY. Для получения правильных
результатов на стерео требуется отдельная
4-битная процедура вывода.
Для эффективности мы можем использовать
таблицу, которая дает соответствующую ком─
бинацию уровней для каждого 8-битного зна─
чения выборки. Я пробовал несколько разных
подходов к созданию такой таблицы. Базовая
таблица принимает 0 + 0 + 0 как самый низ─
кий уровень и f + f + f как самый высокий
- я пробовал подбирать значения вручную,
чтобы выбрать более подходящий диапазон,
который вносит меньше искажений, но я не
думаю, что результаты были настолько хоро─
ши. Также попытался использовать существу─
ющий отсчёт звука в качестве входного дан─
ного для взвешивания отсчётов,чтобы наибо─
лее часто используемые значения отсчётов
были ближе всего к их идеальным значениям.
Независимо от метода, который я использо─
вал, некоторые искажения были неизбежны, в
основном те,которые вызваны промежуточными
уровнями громкости при переключении с од─
ного уровня на другой.
Если каждая выборка рассчитывается как
a + b + c, то общая разница между двумя
последовательными выборками может быть
смоделирована как
|a1 - a2| + |b1 - b2| + |c1 - c2|
если каналы обновляются последовательно. Я
обнаружил,что если один канал (скажем, A )
применён для наибольшего из трех значений,
другой канал (B) с средним значением, а
оставшийся канал (C) с наименьшим значени─
ем, тогда общее расстояние будет минималь─
ным для всех возможных комбинаций значений
каналов.
Программа вывода
Пример кода,использующего описанный ал─
горитм для достижения 8-битного звука,
близкий к используемому в плеере:
setup:
; выбираем AY регистры
ld bc,#fffd ;порт регистров AY
ld a,7 ;регистр AY
out (c),a
; выключаем генерирование тона и шума
ld b,#bf ;порт данных AY
ld a,#3f ;выключаем тон и шум
out (c), a
; ...
play_sample:
;предполагаем, что отсчёт сгенерирован и
;хранится в регистре А
;таблица уровней громкости,по одному байту
;на три канала AY для каждого 8-битного
;значения отсчёта:
ld h,HIGH output_table
ld bc,#fffd ;порт регистра AY
ld a,8 ;выбираем канал A
out (c),a
ld b,#bf ;порт данных AY
ld a,(hl) ;берём значение для канала A
out (c),a ;устанавливаем туда значение
inc l
ld b,#ff ;регистр порта AY
ld a,9 ;выбираем канал B
out (c),a
ld b,#bf ;порт данных AY
ld a,(hl) ;берём значение для канала B
out (c),a ;устанавливаем туда значение
inc l
ld b,#ff ;регистр порта AY
ld a,10 ;выбираем канал C
out (c), a
ld b,#bf ;порт данных AY
ld a,(hl) ;берём значение для канала C
out (c),a ;устанавливаем туда значение
inc l
Используя этот метод, мы можем получить
простой 8-битный вывод. Но этот код не
идеален, если мы собираемся выводить тыся─
чи выборок в секунду. Обратите внимание,
как мы должны переключаться между портом
выбора регистра и портом данных каждый
раз, когда мы записываем в канал. Эти пор─
ты:
11-- ---- ---- --0- выбор регистра
10-- ---- ---- --0- данные
Уровни громкости, которые мы отправляем
в AY, представляют собой четыре младших
бита, которые не конфликтуют с двумя бита─
ми,используемыми для различения портов AY.
Таким образом, мы можем закодировать часть
адреса порта AY в значениях таблицы значе─
ний и получить следующее:
play_sample:
; наш отсчёт в регистре А
ld h,HIGH output_table
ld bc,#fffd ;порт регистра AY
ld a,8 ;выбираем канал A
out (c),a
ld a,(hl) ;берём значение для канала А
out (#fd),a ;устанавливаем туда знач-е
inc l
ld a,9 ;выбираем канал B
out (c),a
ld a,(hl) ;берём значение для канала B
out (#fd),a ;устанавливаем туда знач-е
inc l
ld a,10 ;выбираем канал C
out (c),a
ld a,(hl) ;берём значение для канала C
out (#fd),a ;устанавливаем туда знач-е
inc l
Теперь мы устранили необходимость за─
гружать в регистр B требуемый адрес порта,
сэкономив 38 тактов на отсчёт. Но есть ещё
одно улучшение, которое мы можем сделать.
Когда мы пишем в порт #fffd, чтобы выб─
рать желаемый регистр AY для записи, выб─
ранный регистр запоминается. Если мы запи─
сываем только в один регистр AY, это изба─
вляет от необходимости выбирать один и тот
же регистр несколько раз. В настоящее вре─
мя мы записываем данные в каналы в порядке
A, B, C, A, B, C, что требует выбора ре─
гистра для каждого нового канала. Но что,
если мы будем чередовать порядок каналов?
Примерно так:
отсчёт 0: вывод A, B, C
отсчёт 1: вывод C, B, A
отсчёт 2: вывод A, B, C
отсчёт 3: вывод C, B, A
отсчёт 4: вывод A, B, C
...и так далее.При этом последний регистр,
выбранный во время каждой выборки, совпа─
дает с первым регистром,выбранным во время
следующей выборки. Мы можем сделать это,
имея две разные процедуры вывода, которые
мы чередуем. Вот так:
play_sample0:
; наш отсчёт в регистре А
ld a,(hl) ;берём значение для канала А
out (#fd),a ;устанавливаем туда знач-е
inc l
ld a,9 ;выбираем канал B
out (c),a
ld a,(hl) ;берём значение для канала B
out (#fd),a ;устанавливаем туда знач-е
inc l
ld a,10 ;выбираем канал C
out (c),a
ld a,(hl) ;берём значение для канала C
out (#fd),a ;устанавливаем туда знач-е
inc l
; ...
play_sample1:
; наш отсчёт в регистре А
ld a,(hl) ;берём значение для канала C
out (#fd),a ;устанавливаем туда знач-е
dec l
ld a,9 ;выбираем канал B
out (c),a
ld a,(hl) ;берём значение для канала B
out (#fd),a ;устанавливаем туда знач-е
dec l
ld a,8 ;select channel A
out (c),a
ld a,(hl) ;берём значение для канала А
out (#fd),a ;устанавливаем туда знач-е
dec l
Теперь мы выиграли дополнительно 19 та─
ктов, при только 5 OUT на выборку, а не 6.
Есть еще одно преимущество: нам больше не
нужно перезагружать адрес таблицы значений
громкости,а просто увеличивать и уменьшать
указатель.(Это важно!Если мы изменим поря─
док каналов,но прочитаем таблицу громкости
в одном и том же порядке для каждого отс─
чёта,мы вызовем резкое жужжание, поскольку
каналы A и C быстро меняют свои уровни.)
Осталось только одно последнее дополне─
ние к нашей программе вывода.Мы должны по─
лучить данные формы волны из буфера в па─
мяти, и пока мы это делаем, мы также можем
микшировать сэмплы ударных из смещения IY.
В более ранней версии я копировал сэмплы
ударных в буфер с развернутыми LDI, но это
оказалось слишком медленным. Вот оконча─
тельная пара процедур, каждая из которых
занимает в общей сложности 134 такта и 25
байт:
;такты ;байты
sample_out_routine_ay_mono_0:
; получаем данные отсчёта
ld a,(hl) ;7 / 7 ;1 / 1
inc l ;4 / 11 ;1 / 2
add a,(iy+0) ;19 / 30 ;3 / 5
ld e,a ;4 / 34 ;1 / 6
; вывод канала A
ld a,(de) ;7 / 41 ;1 / 7
out (#fd),a ;11 / 52 ;2 / 9
inc d ;4 / 56 ;1 / 10
; вывод канала B
ld a,#09 ;7 / 63 ;2 / 12
out (c),a ;12 / 75 ;2 / 14
ld a,(de) ;7 / 82 ;1 / 15
out (#fd),a ;11 / 93 ;2 / 17
inc d ;4 / 97 ;1 / 18
; вывод канала C
ld a,#0a ;7 / 104 ;2 / 20
out (c),a ;12 / 116 ;2 / 22
ld a,(de) ;7 / 123 ;1 / 23
out (#fd),a ;11 / 134 ;2 / 25
sample_out_routine_ay_mono_1:
; получаем данные отсчёта
ld a,(hl) ;7 / 7 ;1 / 1
inc l ;4 / 11 ;1 / 2
add a,(iy+0) ;19 / 30 ;3 / 5
ld e,a ;4 / 34 ;1 / 6
; вывод канала C
ld a,(de) ;7 / 41 ;1 / 7
out (#fd),a ;11 / 52 ;2 / 9
dec d ;4 / 56 ;1 / 10
; вывод канала B
ld a,#09 ;7 / 63 ;2 / 12
out (c),a ;12 / 75 ;2 / 14
ld a,(de) ;7 / 82 ;1 / 15
out (#fd),a ;11 / 93 ;2 / 17
dec d ;4 / 97 ;1 / 18
; вывод канала A
ld a,#08 ;7 / 104 ;2 / 20
out (c),a ;12 / 116 ;2 / 22
ld a,(de) ;7 / 123 ;1 / 23
out (#fd),a ;11 / 134 ;2 / 25
Другие методы вывода
Наряду с методом вывода для моно чипов
AY, я разработал программу,позволяющую ис─
пользовать и другие методы вывода. К ним
относятся подпрограмма вывода для SpecDrum
(по сути, 8-битного ЦАП, обеспечивающего
более высокое качество вывода), одна для
одного канала микросхемы AY (предназначена
для достижения монофонического воспроизве─
дения на стереочипе, где объединение нес─
кольких каналов больше не работает), а
также по одному для левого и правого кана─
лов стерео микросхемы AY (тональные каналы
A, B и C слева и канал выборки D справа).
Каждый дополнительный метод вывода развёр─
нут, чтобы сделать его точно такой же дли─
ны и времени выполнения, что и первый ме─
тод вывода, 25 байт и 134 такта. Это упро─
щает внесение изменений в каждой програм─
ме, в которой она используется, во время
выполнения (нет необходимости в перекомпи─
ляции).
Тайминги
Чтобы достичь того, что проигрыватель
занимает строго постоянное время, порядок
выполнения должен быть тщательно спланиро─
ван.Некоторые ветки требуют для выполнения
больше тактов, чем другие, поэтому инст─
рукции,не имеющие никакой другой цели,кро─
ме как тратить время,оказались очень кста─
ти. Из всех инструкций, доступных на Z80,
EX (SP),HL выполняется за самое долгое
время по отношению к размеру в байтах ( 19
тактов к 1 байту), следующая - EX (SP),IX
( 23 такта к 2 байтам).
Самой универсальной,на мой взгляд,инст─
рукцией была ADD HL,HL. Среди её полезных
свойств: использует только один байт памя─
ти,выполняется за 11 тактов, не изменяются
регистры, кроме HL, и нет обращения к про─
извольному адресу памяти.
Другими инструкции, которые я нашёл по─
лезными,были RLD и RRD (18 тактов,по 2 ба─
йта каждая),расположенные попарно,потенци─
ально нежелательные эффекты сдвига влево
отменяются последующим сдвигом вправо.
Обычно мне нужны задержки в 5 тактов, но
их можно было получить только с помощью
условного RET с ложным условием. Для таких
случаев была проведена тщательная провер─
ка, чтобы убедиться, что условие не может
быть истинным!
Эта таблица демонстрирует список полез─
ных инструкций для синхронизации в порядке
эффективности (измеряемой соотношением та─
ктов к байтам).
Команда такты байты эффективность портит
======= ===== ===== ============= ======
EX (SP),HL 19 1 19 (SP),HL
ADD HL,rr 11 1 11 HL,F
RRD/RLD 18 2 9 (HL),AF
CPI 16 2 8 HL,BC
CP (HL) 7 1 7 F
LD A,(rr) 7 1 7 A
INC rr 6 1 6 rr
JR $+2 12 2 6
RET cc 5 1 5
LD A,R 9 2 4.5 AF
NOP 4 1 4
Использование памяти
Наряду с тактами память также является
дефицитным ресурсом,и её необходимо эффек─
тивно использовать, чтобы хранить более
нескольких 8-битных аудиосэмплов. Когда
скорость имеет приоритет, некоторая память
неизбежно будет использована развёрнутыми
циклами, но я всё же нашел способы сэконо─
мить несколько сот байт в разных местах.
Из всех вещей, которые могут тратить
впустую память,таблицы с выровненными дан─
ными,вероятно,являются одними из самых ос─
новных.Таблица с выравниванием на 256 байт
тратит до 255 лишних байт памяти, или в
среднем 127,5 байт.
Чтобы сгладить проблему, я переместил
все таблицы, размер которых был равен 256
байт (или кратный ему) в начало банка па─
мяти. Это позволяет держать их вместе, не
тратя лишнего места.
А как насчет выровненных таблиц длиной
менее 256 байт? Размещение их рядом друг с
другом создает бесполезное неиспользуемое
пространство. В моём случае я хотел иметь
возможность выполнять эффективную индекса─
цию с младшим байтом адреса (чтобы избе─
жать дорогостоящей арифметики),поэтому эти
таблицы должны были уместиться в пределах
256 байт.Однако я понял,что ни одна из них
на самом деле не нуждается в выравнивании
по началу 256-байтного сегмента.
Таким образом,я смог разместить эти та─
блицы более или менее в любом месте прог─
раммы. Макрос ассемблера выдаёт предупреж─
дение, если таблица случайно пересекает
256-байтовую границу, это даёт возможность
узнать, когда надо искать другое место для
размещения таблицы. Единственная особен─
ность, связанная с таким подходом,заключа─
ется в том, что индексы в этих таблицах не
совсем предсказуемы и не основаны на базе,
равной 0,но поскольку ассемблер генерирует
эти индексы на этапе компиляции,то в прин─
ципе это не является большой проблемой. В
целом,оставление невыровненных таблиц спо─
собствовало значительной экономии памяти.
Выводы
Этот проект получился не совсем таким,
каким я наивно его представлял в начале
разработки два года назад, но в некоторых
отношениях он оказался всё же лучше, и я
многому научился в процессе разработки.
Его истинный потенциал ещё предстоит
должным образом использовать, в основном
из-за отсутствия трекера (музыку приходи─
лось вручную записывать в виде инструкций
DB ). Но рано или поздно это может измени─
ться.
Будут ли дальнейшие эксперименты со
звуком для Speccy? Оставайтесь с нами, и
узнаете! (А может, и нет...)
Other articles: