01 апреля 2021

 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:


Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Similar articles:
BBS - list of stations BBS ZXNet.
Retro - 40 best procedures: the list of variables.

В этот день...   21 November