Info Guide #12
31 декабря 2017

Музыка - биперные движки: Двоичная модуляция (часть 1).

Двоичная модуляция - часть 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 байт
core0       ;громкость 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
core3
     ...

   И так далее, и тому подобное. Вам  при─
дётся  немного поиграть в тетрис, расстав─
ляя код обновления счётчиков и т.п.,но всё
решаемо. Зато теперь мы можем достичь аж 8
уровней  сигнала! Однако появляется другая
проблема - для  уровня1 мы не можем пере─
ключать  бит  бипера  достаточно  быстро -
минимальная  задержка между переключениями
составляет11 тактов (OUT (C),0:OUT (#FE),
A). Но11 тактов - не очень хорошо,так как 
в этом случае  мы можем напороться на тор─
можение  I/O-циклов  УЛой. Одно из решений
состоит в  том, что мы  просто забиваем на
это, т.к. при  уровне  сигнала1  ULA-тор─
можение не сильно повлияет на звук. Другой
метод - полагаем, что при уровне сигнала0
мы  всё  равно выводим импульс в16 тактов
на бипер (всегда выводим импульс и никогда
не  выводим  импульс  короче). При  этом в
примере 4 core1 станет core0 и т.д. Такой
подход  хорошо работает на реальном железе
- но, к сожалению,не в эмуляторах. Поэтому
мой  биперный движок 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 ), я ре─ 
шил заняться чем-то новеньким.
   Мне  всегда нравился движок Zilogat0r'а
под  названием  squeeker. Он не понравится
любителям  чистого и ясного звука. Однако,
когда  я слушаю такие движки, как например
Fuzz Click (он же  Special FX ) или движки 
Фоллина, мне  кажется, что  они звучат как 
грязная  искажённая рок-гитара. И ничто не
имитирует  такую  гитару лучше, чем старый
добрый  squeeker.  Единственное, что  меня
останавливало  от написания  музыки в этом
движке - отсутствие нормального редактора.
Ну, точнее, он был,написанный на Бейсике,и
это даже хуже, чем писать музыку в ассемб─
лере.
   Но  Zilogat0r  прислал мне сорцы своего
движка  несколько  лет  назад, а недавно я
наконец смог сделать для него конвертер из
формата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) - способ 
представления  аналогового сигнала при по─ 
мощи  импульсов фиксированной длительности 
и  амплитуды, изменяется  лишь  расстояние 
между такими импульсами - прим. пер. ] 



Другие статьи номера:

Помощь - об оболочке: произошли некоторые изменения в кнопках.

Предисловие - от авторов: Прошедшие два года были очень насыщенными.

Комьюнити - ZX Spectrum: Как это было в Рязани (1980-е).

Комьюнити - ZX Spectrum: Как это было в Рязани (1991-1993).

Комьюнити - ZX Spectrum: Как это было в Рязани (1993-1995).

Комьюнити - ZX Spectrum: Как это было в Рязани (1995-1997).

Комьюнити - сценеры шутят.

Код - этюды: вызов процедур по списку адресов.

Код - 3D демы на ZX Spectrum: история развития 3д движков.

Код - 3D движок: оптимизация на прообразе 3D Construction Kit.

Код - 3D движок: фрагменты.

Код - Посекторный движок для 3D-шутера от Destr.

Код - 3D скролл на ZX Spectrum (часть 1).

Код - 3D скролл на ZX Spectrum: реализация (часть 2).

Графика - графические редакторы: Старый софт от Alone Coder'а.

Графика - палитра: Палитровые эффекты в играх.

Музыка - биперные движки: Двоичная модуляция (часть 1).

Музыка - биперные движки: Двоичная модуляция (часть 1).

Системки - история операционной системы CP/M для Спектрума (часть 1).

Системки - история операционной системы CP/M для Спектрума: ограничения (часть 2).

Системки - NedoLang: Начало - самый простой процедурный язык (часть 1).

Системки - NedoLang: Путь к самокомпиляции (часть 2).

Системки - NedoLang: Проклятие языка Си (часть 3).

Системки - NedoLang: Памяти под самокомпиляцию не хватало (часть 4).

Системки - NedoLang: ускорение (часть 5).

Системки - NedoLang: Куда плыть дальше (часть 6).

Металлолом - Знакомьтесь, ATM-turbo 3! ATM-turbo 3 (v8.0) - что это такое и с чем его едят.

Металлолом - Из истории Betadisk'а: Дисковый интерфейс от Technology Research был.

Дикий ум - Компрессия: Первые компрессоры графики на Speccy (часть 1).

Дикий ум - Компрессия: Фичи с эвристикой, Потоковая декомпрессия, Сжатие музыки (часть 2).

Игрушки - От редакции: 2017-й год вышел очень богатым на события.

Игрушки - интервью с автором игры Mickey the Basic game (Sergio).

Игрушки - квест "Неожиданное Путешествие" - взгляд изнутри.

Игрушки - Nomad: интервью с автором скролл-шутера Nomad (Hippiman).

Игрушки - Скроллинг в Evo SDK.

Игрушки - Hints & Tips: Mickey, Nomad.

Мыльница - Errata: ошибки в Info Guide #11, ACNews #65.

Письма - отзывы о журнале от: raver, destr, sirx, survivor, Ellvis, Utz и Николая Амосова.

Об авторах - Авторы журнала.


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

Похожие статьи:
TOP 20 - Лучшие 20 игр по итогам продаж на торговых точках Москвы.
Обзорчик - Обзор игровых программ: Bedlam, Xevious, Eric and the Floaters, Crazy Cars 1 & 2.
Зазеркалье - Из хроники исчезновений (продолжение).

В этот день...   25 мая