31 декабря 2017

Двоичная модуляция - часть 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:


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

Similar articles:
B.B.S. News - The work B.B.S. 'ca.
Coffee - on / off Beepera in Tasme
Solitude - Kq: "There are several degrees of loneliness in the big cities. First seen on the streets clogged up to the limit, which is not what step to step without preparation it is impossible - there is even a sip of air to be on tiptoes pre-climb "...
Advertising - Advertising and announcements.

В этот день...   13 May