Двоичная модуляция - часть 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) - способ представления аналогового сигнала при по─ мощи импульсов фиксированной длительности и амплитуды, изменяется лишь расстояние между такими импульсами - прим. пер. ]