Info Guide
#13
01 апреля 2021 |
|
Music - софтовый движок OPL синтеза для AY (часть 2)
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? Оставайтесь с нами, и узнаете! (А может, и нет...)
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября