Двоичная модуляция - часть 1
Двоичная модуляция: новости из бипера
utz
translated by Lord Vader
В прошлый раз я обрисовал общие тренды
развития на 1-битной музыкальной сцене. В
этот раз я больше пройдусь по технике и
буду писать конкретнее.
Оцифровение
Большую часть первой половины2016 года
я экспериментировал с синтезом цифрового
сэмплированного звука.
Общий принцип по существу такой же, как
и в методе 'чередования каналов'(pulse-
interleaving), используемом в таких движ─
ках, как "WHAM! The Music Studio" (автор
Mark Alexander ), Savage Engine (автор
Jason C. Brooke ), Tritone (автор Shiru ).
Выходной бит переключается с частотой вы─
ше,чем может с полной амплитудой двигаться
диффузор громкоговорителя, и он таким об─
разом устанавливается в некотором среднем
положении.Таким способом можно имитировать
целый набор "громкостей",требуемых для ми─
кширования нескольких каналов. Простейшая
реализация представлена ниже.
(пример 1)
loop
add hl,de;HL - сумматор канала 1,
;DE - частота канала 1
ld a,h
cp #80 ;сравнить сумматор с
;требуемым коэффициентом заполнения
sbc a,a;заполнить A флагом переноса
out (#fe),a;вывести в бипер
add ix,bc;IX - сумматор канала 2,
;BC - частота канала 2
ld a,ixh;...
cp #80
sbc a,a
out (#fe),a
jr loop
Вышеприведённый код смешивает два меан─
дра с коэффициентом заполнения 0.5 каждый
(то есть каждый из каналов будет в состоя─
нии "1" ровно половину времени, как на
AY ). Естественно,в этом коде можно задать
произвольный коэффициент.
Проблема с таким синтезом в том, что на
обработку всех каналов отводится мало вре─
мени, что ограничивает количество каналов.
По мере увеличения этого времени становит─
ся заметным (т.е. попадает в слышимый диа─
пазон) паразитный шум на частоте дискрети─
зации. На Спектруме обычно период частоты
дискретизации делают равным 224 тактам
(т.е. длительности1 строки). Это помогает
выравнивать все командыOUT по границе 8
тактов (так как эти команды на оригиналь─
ном 'резиновом' Спектруме подвержены тор─
можению, избежать которого можно, если они
выровнены по8 тактам). Когда я только на─
чал писать биперные движки,я этого не знал
и получал довольно хреновый звук.
Итак, теперь вы знаете, как смешивать
два канала с меандрами, но как применить
это знание для настоящего сэмплированного
звука? Для начала можно считать эти каналы
не каналами, а уровнями сигнала. В примере
выше уже есть 3 уровня сигнала: 0 (оба
канала выводят 0),1 (один из двух выводит
1) и 2 (оба выводят 1). Таким образом, у
нас уже получился некоторый сэмплированный
звук (который является просто последовате─
льностью уровней сигнала, меняющихся с
фиксированной частотой - например,44.1
кГц ). Однако понятно,что нам нужно неско─
лько больше уровней сигнала, чем 3, на─
пример, стандартные65536 уровней из обыч─
ного 16-битного wav-файла. Тут-то и появ─
ляются серьёзные проблемы.
Спустимся с небес на землю - вряд ли
получится достичь 65536 уровней сигнала
на бипере. Как я уже упоминал,микширование
должно укладываться в224 такта, и каждая
командаOUT выровнена по 8 тактам, так что
абсолютный максимум составляет 224/8=28
уровней сигнала [на самом деле 29 - прим.
пер.]. В теории. На практике существуют
и другие ограничивающие факторы, умень─
шающие предельно достижимое количество
уровней. Например, самая быстрая команда
OUT уже выполняется11 тактов. Кроме того,
для нескольких микширующихся каналов необ─
ходимо проводить побочные вычисления, на─
пример, обновление канального сумматора,
счётчиков длин нот и т. д. Мои ранние экс─
перименты были похожи на следующий код:
(пример 2)
ld c,#fe
loop
add ix,de;прибавляем частоту DE
;к сумматору IX
sbc a,a;adc a,b;HL указывает в
;256-байтный сэмпл,
;выровненный по 256 байт
add a,l;при переполнении сумматора
;делаем шаг в сэмпле
ld l,a
ld a,(hl);в байтах сэмпла - биты,
;которые мы будем выдвигать в бипер
out (c),a;%00010000 = уровень 1,
;%00110000 = уровень 2 итд:
;кол-во "1" в байте задаёт уровень
rlca
out (c),a
rlca
out (c),a
rlca
...
jr loop
Такой код более-менее работает, остав─
ляя множество возможностей для улучшения.
Основная его проблема - неравномерное рас─
пределение OUT'ов по времени выполнения
цикла. Можно постараться и распределить
OUT'ы равномерно по всему циклу (см., на─
пример, мои биперные движки qaop, yawp и
wtfx ). Однако потом вам станет ясно, что
всё это не сильно помогает, если только вы
не начнёте жертвовать количеством уровней
сигнала. Что же дальше?
Во-первых, мы можем немного соптимизи─
ровать лукап по таблице (сэмплу), взяв её
индекс непосредственно из старшего байта
сумматора канала, например:
(пример 3)
add hl,de
ld c,h ;BC указывает на сэмпл
ld a,(bc)
Я перенял этот трюк у Sorchard'а с фо─
румовworldofdragon.org. Поначалалу я сом─
невался - сработает ли такая магия? Ведь
этот подход приведёт к ограничению частот─
ной разрешающей способности до8 бит, что,
как мы знаем, плохо! Ну, не совсем. Биты
индекса в таблице также следует добавлять
к этим битам, и на самом деле в примере2
разрешение по частоте составляет24 бита -
что уже чересчур. С оптимизированным лука─
пом(как в примере 3) мы получаем 16-бит─
ное разрешение, что как раз то, что нам
нужно.
Во-вторых, нет необходимости обновлять
все канальные сумматоры за единственный
проход цикла. Достаточно лишь просто выво─
дить корректный (скомбинированный из всех
каналов) уровень сигнала в данном прохо─
де цикла. (Респект Alone Coder'у за этот
трюк.)
Итак, мы сделали все возможные оптими─
зации, но наш код всё ещё хреновый. Что
теперь? Если, например,вы пишите графичес─
кий код, и он у вас оказывается медленным,
то в первую очередь вы разворачиваете цик─
лы.Что-то подобное и мы сделаем для нашего
звукового кода - а именно, напишем отдель─
ное 'ядро' для каждого уровня сигнала:
(пример 4)
ld l,0
ld b,#ff;ld b,1 для КМОП Z80, т.к.
;"out (c),0" там работает как out (c),#FF
ld c,#fe
org #8100;выравниваемся на 256 байт
coreO ;громкость 0
out (c),0;выключаем бит бипера
... ;обновляем канальные счётчики
... ;вычисляем уровень для
;следующей итерации
... ;H = #81 + уровень сигнала
jp (hl)
org #8200
core1 ;громкость 1
out (c),b;включаем бит бипера
... ;тратим 4 такта
out (c),0;выключаем бит бипера
;(он был включен ровно 16 тактов)
... ;обновляем канальные счётчики
... ;вычисляем уровень для
;следующей итерации
... ;H = #81 + уровень сигнала
jp (hl)
org #8300
core2 ;громкость 2
out (c),b;включаем бит бипера
... ;тратим 20 тактов
out (c),0;выключаем бит бипера
;(он был включен ровно 32 такта)
... ;обновляем канальные счётчики
... ;вычисляем уровень для
;следующей итерации
... ;H = #81 + уровень сигнала
jp (hl)
org #8400
coreЗ
...
И так далее, и тому подобное. Вам при─
дётся немного поиграть в тетрис, расстав─
ляя код обновления счётчиков и т.п.,но всё
решаемо. Зато теперь мы можем достичь аж 8
уровней сигнала! Однако появляется другая
проблема - для уровня1 мы не можем пере─
ключать бит бипера достаточно быстро -
минимальная задержка между переключениями
составляет11 тактов (OUT (C),0:OUT (#FE),
A). Но11 тактов - не очень хорошо,так как
в этом случае мы можем напороться на тор─
можение I/O-циклов УЛой. Одно из решений
состоит в том, что мы просто забиваем на
это, т.к. при уровне сигнала1 ULA-тор─
можение не сильно повлияет на звук. Другой
метод - полагаем, что при уровне сигнала0
мы всё равно выводим импульс в16 тактов
на бипер (всегда выводим импульс и никогда
не выводим импульс короче). При этом в
примере 4 core1 станет coreO и т.д. Такой
подход хорошо работает на реальном железе
- но, к сожалению,не в эмуляторах. Поэтому
мой биперный движок zbmod (который играет
сэмплы неограниченной длины в3 каналах с
21 уровнем громкости) просто имеет две
версии - одна для реального железа, где
уровень 0 соответствует импульсу в 16
тактов на бите бипера, другая для эмулято─
ров, где при уровне громкости1 нарушается
выравнивание циклов вывода по8 тактам.
Недостаток вышеописанного 'многоядерно─
го' метода очевиден - он жрёт огромное ко─
личество памяти.И что хуже,память теряется
впустую - из-за необходимости выравнивать
куски кода по 256 байт. Можно, конечно,
заполнить потерянные кусочки чем-то полез─
ным. В zbmod, например,там лежит код,кото─
рый подгружает очередные данные трека во
время работы основного цикла - чуть ниже я
предложу ещё одну идею для заполнения этих
кусочков.Но перед этим я расскажу о другом
методе создания16 чистых уровней сигнала
- используя всего лишь6 команд OUT и без
излишнего расходования памяти кодом, как в
примере4.
Внимание: я придумал такой код сравни─
тельно недавно и недостаточно протестиро─
вал его. Тем не менее,я думаю,что он будет
хорошим дополнением к этой статье.
Итак.
Конечно же, вы знаете,что в3 битах мо─
жно закодировать8 чисел (0..7) . Что если
мы применим это наблюдение к нашим уровням
сигнала?
(пример 5)
ld c,#fe
loop
... ;обновление всего-чего-надо
;за 40 тактов
out (c),x;переключаем бит бипера
;через 64t
... ;делаем ещё что-нибудь за 20t
out (c),x;переключаем бит бипера
;через 32t
... ;что-то на 4t
out (c),x;вывод через 16 тактов -
;начало вывода канала 1
... ;что-то на 52t
out (c),x;вывод через 64t
... ;что-то на 20t
out (c),x;вывод через 32t
... ;что-то на 4t
out (c),x;вывод через 16t -
;начало вывода канала 2
jr loop
Такой код даёт нам2 * 2^3 = 16 уровней
сигнала. И весь цикл исполняется ровно за
2*(64+32+16) = 224 такта (случайно так по─
лучилось). Прикол!
Этот фокус пока не имеет официального
названия, назовём его"n-bit ladder".
Кстати, есть одна проблема, которую я
до сих пор не разрешил. Когда цикл вывода
сэмплированного звука некоторое время
подряд выводит отсчёты с высокой громко─
стью, усреднённый уровень на динамике тоже
увеличивается, создавая неприятный эффект
перегруза. Это можно услышать, напри─
мер, в демонстрационной мелодии из движка
Octode2k16 (который суммирует 8 каналов
меандра и выводит все возможные суммы
через9 разных вариантов цикла).
Я предполагаю, что это происходит из-за
того, что диффузор динамика не успевает
возвратиться в нейтральное состояние и
потому громкости суммируются и увеличиваю─
тся.Я даже пробовал использовать эту фишку
для создания звука, похожего на AY-огибаю─
щие,но к сожалению, мне не удалось надёжно
воспроизводить такой эффект. Если у вас
появятся какие-то идеи по этому поводу -
буду рад о них услышать.
Фильтры
Выше я пообещал с пользой применить
дырки в раскранченном коде 'многоядерного'
движка. Как насчёт ... фильтров? ФНЧ,ФВЧ -
всё это вотчина DSP, да. И конечно же, нам
не стоит и надеяться запилить даже прими─
тивный фильтр... хотя...мы УЖЕ играем сэм─
плы на бипере, так что помешать нам может
только низкая скорость Z80. И оказывается,
что ФНЧ и ФВЧ вовсе не требуют особых вы─
числительных ресурсов.
Формула для простейшего ФНЧ с бесконеч─
ной импульсной характеристикой такова:
y[i] = y[i-1] + a·(x[i] - y[i-1])
Где i - номер отсчёта, x - входной
(нефильтрованный) сигнал, y - выходной
(отфильтрованный) сигнал иa - некий ко─
эффициент от 0 до 1. Меньшее значение a
соответствует большему фильтрующему эффек─
ту. В 'многоядерном' движке(см. пример 4)
эта формула легко внедряется следующим
образом:
(пример 6)
;H = #81 + уровень предыдущей
;итерации (т.e. y[i-1])
ld a,#81
add a,h
ld h,a ;H = y[i-1];
... ;обновление сумматоров
... ;A = уровень следующей
;итерации, т.е. x[i]
sub h ;A = x[i] - y[i-1]
srl a ;A = 0.5·(x[i] - y[i-1])
add a,h;A = y[i-1]+a·(x[i]-y[i-1])=
;= y[i]
Довольно просто, не так ли? Это пусть и
не самый лучший в мире,но всё же настоящий
ФИЛЬТР низких частот.
Кстати, я обычно делаю 2 сдвига (rrca:
rrca:and #3f), в качестве компромисса меж─
ду скоростью кода и качеством звука.
ФВЧ делаются немного сложнее. Можно вы─
честь результат ФНЧy[i] из входного сиг─
налаx[i], а можно взять такую формулу:
y[i] = a·(y[i-1] + x[i] - x[i-1])
Так или иначе, расчёт на Z80 требует на
одну операцию больше, чем ФНЧ. Но и это не
главная проблема. Главная проблема состоит
в том, что в отличие от случая ФНЧ, расчёт
ФВЧ даёт отрицательные числа. Это значит,
придётся или добавлять 'ядра' (core-1,
core-2 и т. д.), которые будут выводить
такие же уровни громкости, какcore1,core2
и т. д. - ведь невозможно вывести на бипер
отрицательные уровни сигнала! - или прове─
рять результат вычислений на отрицатель─
ность, выполняяcpl:inc при необходимости.
Ничего приятнее я придумать, к сожалению,
не смог,но уверен,что есть красивый метод.
[ФВЧ просто убирает постоянную составляю─
щую и выдаёт отсчёты вокруг нуля. Правиль─
ный способ тут был бы такой: к результату
ФВЧ надо прибавить половину максимально
выводимого отсчёта движка и обрезать пере─
полнение - прим.пер. ]
Действие таких фильтров вы можете услы─
шать в моём биперном движке Beepertoy.
Метод "Squeeker"
Поэкспериментировав с проигрыванием сэ─
мплов на бипере в течение нескольких меся─
цев, я немного заскучал. Как видно изпри─
мера 4, такого рода движки не столько сло─
жны, сколько муторны в написании. Потому,
написав несколько таких движков ( Beeper─
toy, fluidcore, Octode2k16, zbmod ), я ре─
шил заняться чем-то новеньким.
Мне всегда нравился движок ZilogatOr'а
под названием squeeker. Он не понравится
любителям чистого и ясного звука. Однако,
когда я слушаю такие движки, как например
Fuzz Click (он же Special FX ) или движки
Фоллина, мне кажется, что они звучат как
грязная искажённая рок-гитара. И ничто не
имитирует такую гитару лучше, чем старый
добрый squeeker. Единственное, что меня
останавливало от написания музыки в этом
движке - отсутствие нормального редактора.
Ну, точнее, он был,написанный на Бейсике,и
это даже хуже, чем писать музыку в ассемб─
лере.
Но ZilogatOr прислал мне сорцы своего
движка несколько лет назад, а недавно я
наконец смог сделать для него конвертер из
форматаXM.
И как же этот движок работает? Очень
просто: вначале вычисляются состояния (0
или1 ) каналов с использованием коэффици─
ента заполнения - похожим например 1 спо─
собом. Однако далее squeeker не выводит
состояние каждого каналаOUT'ом по очере─
ди, создавая иллюзию нескольких уровней
громкости. Вместо этого состояния всех ка─
налов совмещаются при помощиOR. И поэтому
в цикле только лишь1 команда OUT.
(Пример 7)
loop
ld b,0;тут будем накапливать
;состояния каналов, вначале 0
ld de,xxxx;частота канала 1
add hl,de;аккумулятор канала 1
ld a,h
add a,#20;коэффициент заполнения
rl b ;перенос запоминаем в B
ld de,xxxx;то же самое для канала 2
add ix,de
ld a,ixh
add a,#20
rl b
ld de,xxxx;и для канала 3
add iy,de
ld a,iyh
add a,#20
rl b
ld a,b ;взяли все 3 бита
add a,#f;если B был 0, то бит 4
;останется нулём и после этого
;- иначе установится
out (#fe),a;и наконец!
jr loop
На первый взгляд, это всё выглядит глу─
пой идеей. И если вам важен чистый звук,то
так оно и есть. Но если вам нравится рок и
тяжёлый митол,то это - замечательная идея.
Кроме того, для экзотических железок, где
звук тупо генерируется на одной фиксиро─
ванной частоте (например, компьютеры Sharp
Pocket или консоль Fairchild Channel F ),
данный метод позволяет избавиться от этого
паразитного аппаратного тона, в отличие от
движков, похожих например 1.
Какие же в целом преимущества и недос─
татки данного метода? Для начала, основное
преимущество состоит в единственной коман─
де OUT на весь цикл. И так как теперь не
приходится уже заботиться о смешивании ка─
налов на диафрагме динамика, цикл вывода
можно сделать медленнее.300-400 тактов на
такой цикл - в порядке вещей, и можно даже
во время этого цикла выводить какую-то
графику. Кроме того,по мере добавления ка─
налов в такой движок, громкость каждого
отдельного не будет уменьшаться, в отличие
от метода,описанного в начале статьи. Дан─
ный факт делает подходящим данный метод
для комбинации звука AY и бипера, что про─
демонстрировано в squeekAY.
Основной недостаток метода - каналы мо─
гут блокировать друг друга. С 4-канальным
движком лучше не использовать коэффициенты
заполнения более#20, иначе начнутся выпа─
дения звука каких-либо каналов.С коэффици─
ентами меньше такие выпадения тоже изредка
наблюдаются, но гораздо реже,чем может по─
казаться в результате изучения кода.
Итак, разобравшись в этом методе и на─
писав XM-конвертер для него, я взял и сде─
лал движок под названием Squeeker Plus, в
котором я добавил ударные,шум и огибающие.
Огибающие? Ну, на самом деле это огибающие
для коэффициентов заполнения. Всё равно в
методе squeeker не получаются чистые меан─
дры, и поэтому можно имитировать уровни
громкости изменением коэффициентов запол─
нения,точно так же,как это делается в PFM-
движках: Qchan, Fuzz Click, Stocker и т.д.
[PFM (pulse-frequency modulation) - способ
представления аналогового сигнала при по─
мощи импульсов фиксированной длительности
и амплитуды, изменяется лишь расстояние
между такими импульсами - прим. пер. ]
Other articles: