Info Guide
#13
01 апреля 2021 |
|
Music - софтовый движок OPL синтеза для AY (часть 1)
OPL синтез на AY djnzx48 Введение Серия звуковых чипов Yamaha OPL появи─ лась в 80-х годах и обеспечивала посредст─ вом FM-синтеза весьма узнаваемую музыку во многих известных компьютерных и аркадных играх той эпохи. В декабре 2018 года я попытался воссоз─ дать эту форму генерации звука с помощью 128K Spectrum, частично вдохновившись де─ монстрацией ATARIN того же года. Также по─ влияла попытка участников форума WOS в на─ чале 2000-х годов заставить Spectrum восп─ роизводить файлы MOD. После некоторых первоначальных неудач я начал проект заново в феврале 2020 года. В апреле программа проигрывателя была функ─ ционально завершена, с тех пор были добав─ лены некоторые рефакторинги и улучшения. Хотя используемый там PM синтез намного проще,чем у чипа Yamaha,который мой движок должен был имитировать,но он выдаёт три 8- битных канала звука и канал ударных / сэм─ плов. Для вывода используется встроенный звуковой чип AY-3-8912, но если требуется лучшее качество звука,также поддерживаются 8-битные ЦАП, такие как SpecDrum. Наконец, можно использовать интересные альтернатив─ ные формы синтеза, такие как унисон и син─ хронизация осциллятора,а также орнаменты и изменения высоты звука. Начало разработки - вывод звука Перед тем,как начать,надо было примерно определить характеристики, которые должен иметь плейер. Среди прочих требований я поставил использование 8-битных сэмплов для лучшего качества звука,чем при обычных для AY 4-битных. Я также хотел, чтобы час─ тота дискретизации составляла не менее 10 кГц. С Z80, работающим на частоте 3546900 Гц, это оставляет приблизительно 3546900 / 10000 = 354 такта на отсчёт. Экспериментальным путём я пришёл к вы─ полнению в 331 такт на отсчёт, что склады─ вается из: 134 такта на вывод отсчёта, 4 такта на EXX для переключения на альтерна─ тивный набор регистров, трижды по 63 такта для генерирования трёх отсчётов и финаль─ ные 4 такта на обратное переключение набо─ ра регистров. 134 Вывод отсчёта + 4 EXX + 63 Генерация отсчёта #1 + 63 Генерация отсчёта #2 + 63 Генерация отсчёта #3 + 4 EXX ------------------------- =331 Сумма Я уже не использую настолько строгие соотношения,так как подпрограммы генерации отсчётов занимают разное количество так─ тов и есть ветки,позволяющие выполнять об─ служивающий код. Однако я старался придер─ живаться описанного цикла строго в 331 такт. С единственным исключением:код,пере─ ключающий паттерны - он вызывается доста─ точно редко, и трата дополнительной сотни тактов не будет ощутима. Для данной частоты Z80 331 такт будет соответствовать частоте дискретизации при─ мерно 10.7 кГц. Структура основного цикла Начну с общего описания основного цикла воспроизведения,в течение которого в буфер вывода генерируется шестьдесят отсчётов. (В действительности есть два буфера выво─ да, которые переключаются так, чтобы во время воспроизведения одного возможно было заполнять другой.) 60 отсчётов оказались разумным компромиссом,позволяющим не зани─ мать слишком много памяти в развернутых циклах и при этом получить код,не перегру─ женный накладными расходами от управления каналом и состоянием мелодии. Отсчёт Операции ====== ======== 0-1 Чтение данных паттерна для канала событий,обработка событий/орнамента 1-3 Чтение данных паттерна для канала A, установка параметров 3-13 Генерирование отсчётов для канала А 13-15 Чтение данных паттерна для канала B, установка параметров 15-35 Генерирование отсчётов для канала B 35-37 Чтение данных паттерна для канала C, установка параметров 37-57 Генерирование отсчётов для канала C 57-59 Обновление позиции в мелодии,чтение данных паттерна для канала D, установка указателей сэмпла Следующая таблица описывает использова─ ние каждого из этих каналов: Канал Используется для ===== ================ A Канал басов (FM / унисон) B Канал тона (FM / синхронизация осцилляторов) C Канал тона (FM) D Канал сэмплов E Канал событий Каналы A,B и C являются тональными. Ка─ нал A уникален, так как он работает на по─ ловине частоты дискретизации двух других и предназначен для использования в качестве канала басов.Генерация отсчётов для канала А занимает лишь половину времени по срав─ нению с другими тональными каналами. Канал E был создан для того,чтобы собы─ тия (команды, влияющие на состояние проиг─ рывателя) могли быть выгружены в отдельный канал и не расходовали время выполнения других каналов. В начале основного цикла первой задачей является выдача отсчёта из выходного буфе─ ра, который пуст при первом выполнении ци─ кла. Для контроля используется несколько та─ ймеров. Таймер мелодии уменьшается каждый раз при прохождении основного цикла, а при достижении нуля проигрыватель переходит к следующей строке мелодии. Продвижение по строке мелодии уменьшает таймер паттерна, при достижении нуля проигрыватель перехо─ дит к следующему паттерну.У каждого канала также есть собственный таймер,указывающий, на сколько строк нужно продвинуться для проигрывания следующей ноты. И наконец, у каждого инструмента есть свой таймер,кото─ рый указывает, когда следует переключиться на проигрывание следующей строки инструме─ нта. Эти таймеры используются в основном ци─ кле для обработки канала событий.Новое со─ бытие будет обработано только тогда, когда таймер мелодии будет равен текущей скорос─ ти мелодии, а таймер канала событий равен нулю. Это будет указывать как на новую строку мелодии, так и на событие, присутс─ твующее в этой строке. В других случаях канал событий не обрабатывается, но вместо того, чтобы тратить время на серию NOP, почему бы не сделать что-нибудь полезное? Поэтому это время используется для обрабо─ тки орнаментов. Обработка орнаментов Орнаменты изменяют высоту звука с тече─ нием времени на некоторое целое число по─ лутонов. Их можно использовать для украше─ ния звука, для ослабления атаки ноты или (возможно,наиболее часто) для создания бы─ стрых арпеджированных аккордов. Все три тональных канала могут исполь─ зовать обработку орнамента. Однако доступ─ ное количество времени позволяет обрабаты─ вать только один канал.Кроме того,основной цикл выполняется более 178 раз в секунду, что слишком быстро для орнаментов и добав─ ляет слышимый низкочастотный шум в мело─ дию. Поэтому три канала обрабатываются по очереди в течение нескольких итераций цик─ ла - каждый канал только около 60 раз в секунду, что ближе к традиционным орнамен─ там Spectrum. Эта циклическая последовате─ льность выполняется с использованием само─ модифицирующегося кода,обновляющего каждый раз адрес перехода к одной из трех подпро─ грамм орнамента. Как выглядит обработка орнамента для одного из каналов. Сначала мы загружаем указатель орнамента для этого канала и считываем из него значение орнамента. Это значение может быть ORNAMENT_END, что ука─ зывает на конец орнамента. Если это так,мы сбрасываем указатель в начало цикла орна─ мента и считываем новое значение, иначе мы увеличиваем указатель на единицу и сохра─ няем его для следующей итерации. Теперь нужно обработать смещение высоты ноты. Для этого необходимо взять смещение (bend) для текущего канала и накопленное значение его смещения, сложить их вместе и сохранить новое значение. Следующее необходимое нам значение - значение базовой ноты,соответствующее пос─ ледней проигранной ноте.Если оно равно ну─ лю, мы ничего не добавляем и останавливаем обработку орнамента. Иначе добавляем это значение к значению орнамента,прочитанному ранее. Оба эти значения предварительно ум─ ножены на два, поэтому достаточно взять старший байт таблицы нот и прочитать пос─ ледовательно два байта, что и будет значе─ нием высоты ноты. Далее складываем с нако─ пленным значением смещения тона и получаем финальное значение для высоты тона. Сохра─ ним его в памяти, позже оно нам понадобит─ ся. Возвращаемся к обработке событий Если есть новое событие,то мы прерываем обработку орнамента и приступаем к обрабо─ тке события. Загружаем указатель шаблона события и читаем из него длину события и его тип. Значение длины используем для установки нового значения таймера события. Далее на─ ходим адрес подпрограммы обработчика собы─ тия по таблице и исполняем её. События в мелодии используются для об─ работки всего, что не является нотой. Они могут указать проигрывателю переключиться между режимами синтезирования на опреде─ лённом канале, изменить орнамент или инст─ румент у выбранного канала,применить изме─ нение высоты звука в канале или изменить скорость воспроизведения мелодии. По сути всё, что нужно сделать процеду─ рам обработки событий - это обновить нес─ колько глобальных переменных, содержащих текущее состояние проигрывателя.Однако это немного усложняется тем фактом,что некото─ рые переменные появляются в памяти неско─ лько раз. Я объясню, почему это так. Регистр гораздо быстрее загружать непо─ средственно числом, чем адресацией памяти, особенно если этот адрес неудобно хранить в регистре. Обратите внимание на разницу в этих парах инструкций: ld a,(nn) ;13 тактов ld a,n ;7 тактов (на 6 тактов быстрее) ld hl,(nn) ;16 тактов ld hl,nn ;10 тактов (на 6 быстрее) ld de,(nn) ;20 тактов ld de,nn ;10 тактов (на 10 быстрее) По возможности переменные сохраняются с использованием второго метода, непосредст─ венно в коде проигрывателя.Недостаток это─ го метода в том, что переменные, доступ к которым необходим в разных точках проигры─ вателя, дублируются в памяти и для консис─ тентности все экземпляры должны быть запо─ лнены и обновлены одинаково. Примером, когда этот вид кэширования особенно полезен, является сброс состояния инструмента и орнамента канала на первую строку, что необходимо делать всякий раз, когда на канале воспроизводится новая но─ та. Чтобы избежать повторного считывания этого состояния для каждой новой ноты, ис─ ходные свойства текущего инструмента и ор─ намента сохраняются в подпрограммах для каждого канала для прямой загрузки. Эти свойства необходимо скопировать в нужные места при смене инструментов и орнаментов. Обработка канала А После завершения обработки орнамента / события начинается обработка канала A. Су─ ществует два разных метода генерации отс─ чётов для канала A (FM или унисон), между которыми можно переключаться,изменяя адрес перехода. Эти два метода аналогичны друг другу в том смысле, что они считывают дан─ ные паттерна для канала и устанавливают необходимые параметры для генерации отсчё─ тов. Сначала таймер мелодии считывается вме─ сте с таймером канала A. Эти таймеры испо─ льзуются для проверки, доступна ли новая нота из текущего паттерна. Если это так,мы загружаем указатель паттерна,получаем дли─ ну и значение ноты и сохраняем указатель паттерна. Затем проверяется значение ноты. Нулевое значение используется для обоз─ начения паузы. В этом случае я использовал установку высоты тона канала в ноль, при этом указатель сэмпла перестаёт продвига─ ться по сэмплу и фактически прекращает ге─ нерацию звука. Однако во время первонача─ льного тестирования я обнаружил,что указа─ тель останавливается на непредсказуемых значениях выборки,и в сочетании с нелиней─ ным выходом AY и звуком из других каналов генерировались странные артефакты. Кроме того, поскольку текущий инструмент всё ещё был активен, звучали щелчки каждый раз, когда устанавливался новый сэмпл. Чтобы избежать этих проблем,указатель сэмпла те─ перь сбрасывается в ноль, и активируется фиктивный немой инструмент - до тех пор, пока не будет обнаружена нота (не пауза). Мелодия, состоящая только из пауз,может быть немного скучной для прослушивания,по─ этому предположим, что мы в конечном итоге встретим ноту с ненулевым значением высоты тона. Во-первых, применяется её орнамент и изменение высоты тона, так как они ещё не были обработаны для этой ноты.(На этот раз кода не так много.Это потому,что нам нужно только значение орнамента из первой строки орнамента, которое можно сохранить напря─ мую, и нет необходимости снова накапливать значение изменения высоты тона.) После этого мы сбрасываем указатели орнамента и инструмента на первые строки и загружаем изначальные указатели сэмпла. Что делать,если ещё не пора читать ноту из текущего паттерна? Тогда мы обрабатыва─ ем инструмент для ноты,которая уже играет. Каждая строка инструмента может воспроиз─ водиться в течение определенного времени до перехода к следующей строке инструмен─ та, что позволяет более компактно хранить идентичные строки. При этом используются ранее упомянутые таймеры для инструментов каждого канала. Первое,что мы делаем,- это берем таймер инструмента канала А, уменьшаем его и ре─ зультат проверяем на ноль. Выполнение декремента даёт нам бесплатную проверку на ноль,но это требует,чтобы значение таймера первой строки каждого инструмента было увеличено на единицу. Хранятся два указателя инструментов: один на текущую строку инструмента и один на следующую строку. Если уменьшенный тай─ мер инструмента не равен нулю, мы остаемся в той же строке, используя указатель теку─ щей строки. В противном случае мы загружа─ ем указатель следующей строки. Указатель следующей строки может указывать за конец определения инструмента, в этом случае мы возвращаемся к текущей строке инструмента. Теперь мы сохраняем таймер для этой строки вместе с указателем инструмента (как новый текущий указатель).Мы загружаем фактическую информацию об инструменте (си─ гналы несущей и модулятора для FM или оди─ ночный сигнал для унисона) в регистры и сохраняем указатель следующей строки.Затем нам просто нужно загрузить высоту ноты из таблицы высоты тона и наконец начать гене─ рировать звук. Генерация звука для канала А Для канала А есть два цикла генерации звука,по одному для каждого метода генера─ ции звука, и они развернуты для скорости. Ниже пример цикла генерации FM отсчёта в канале А. Он приведён первым, так как является самым простым. ;отсчёты 0,1 ;такты ld c,ixh ;8/8 ;генерация фазы ld a,(bc) ;7/15 ;байт волны модулятора ld e,a ;4/19 ld a,(de) ;7/26 ;байт несущей волны ld (hl),a ;7/33 ;сохраняем в буфер inc l ;4/37 ld (hl),a ;7/44 ;сохраняем в буфер inc l ;4/48 add ix,sp ;15/63 ;переход на след. шаг Хотя это только одна итерация цикла,она генерирует два отсчёта в буфере.Чтобы сге─ нерировать 60 значений для полного буфера, цикл должен выполниться 30 раз. В следующей таблице показано значение каждого регистра в приведенном выше фраг─ менте кода. Поскольку нам вообще не нужен стек в этом проигрывателе, мы можем испо─ льзовать указатель стека для разных целей, не беспокоясь о том, потребуется ли он для PUSH / POP или CALL / RET. Регистр Назначение ======= ========== A Общее назначение BC Указатель на волну модулятора DE Указатель на несущую волну HL Указатель на буфер вывода SP Высота ноты IX Счетчик нот / фаза Сначала мы берём старший байт счетчика нот и используем его в качестве индекса волны модулятора.После получения байта во─ лны модулятора мы используем его в качест─ ве индекса в несущей волне. Копируем байт сигнала несущей в выходной буфер, дублируя его, потому что нам нужно два значения для каждого прохода по циклу.Наконец,мы обнов─ ляем счетчик нот,используя высоту нот,хра─ нящуюся в SP. Ниже цикл генерации отсчётов унисона для канала A. Он немного сложнее. Занимает такое же количество тактов, что и для FM генерации, но разбит на две части. ;отсчёты 0,1 ;такты ld e,h ;4/4 ;первая фаза ld a,(de) ;7/11 ;байт формы волны ld e,l ;4/15 ;вторая фаза add hl,sp ;11/26 ;следующий шаг ex de,hl ;4/30 add a,(hl) ;7/37 ;+ байт формы волны rra ;4/41 ;уменьшаем наполовину ld (bc),a ;7/48 ;сохраняем в буфер inc c ;4/52 ld (bc),a ;7/59 ;сохраняем в буфер inc c ;4/63 ;отсчёты 2,3 ;такты ld l,d ;4/4 ;первая фаза ld a,(hl) ;7/11 ;байт формы волны ld l,e ;4/15 ;вторая фаза add a,(hl) ;7/22 ;+ байты формы волны rra ;4/26 ;уменьшаем наполовину ld (bc),a ;7/33 ;сохраняем в буфер inc c ;4/37 ld (bc),a ;7/44 ;сохраняем в буфер inc c ;4/48 ex de,hl ;4/52 add hl,sp ;11/63 ;следующий шаг В следующей таблице показано значение каждого регистра в приведенном выше фраг─ менте кода.Обратите внимание,что использо─ ванная здесь инструкция EX DE,HL меняет местами значения DE и HL, поэтому таблица описывает их значения в начале фрагмента кода. Регистр Назначение ======= ========== A Общее назначение BC Указатель на буфер вывода DE Указатель на форму волны HL Счетчик нот / фаза SP Высота ноты Основная идея метода унисона состоит в том, что старший и младший байты высоты звука одинаковые, и мы используем как ста─ рший, так и младший байты счетчика нот для генерации окончательной формы волны. Пос─ кольку младший байт постепенно переносится в старший по мере обновления счетчика нот, мы фактически играем две ноты с очень не─ большими различиями в высоте тона, созда─ вая эффект унисона или хоруса. Сначала мы берём старший байт счетчика нот и используем его в качестве индекса для формы волны. Затем мы загружаем байт формы сигнала,указанный этим индексом,в A. Младший байт счетчика нот также используе─ тся в качестве индекса для формы сигнала, но мы пока не загружаем байт формы сигна─ ла. Мы добавляем высоту нот в счётчик нот, чтобы обновить его, а затем меняем местами DE и HL. Теперь,когда HL является указате─ лем на желаемый байт сигнала, мы можем ис─ пользовать ADD A,(HL), чтобы сложить два байта сигнала вместе,и RRA, чтобы привести их в правильный диапазон. Результат затем сохраняется (дважды) в буфере отсчётов. Для второй половины цикла мы делаем то же самое, что и для первой половины,но из- за того, что DE и HL поменяны местами, мы вычисляем и сохраняем результат до того, как поменяем их обратно и снова обновим счётчик нот. Обработка канала B Теперь, когда все отсчёты канала A были сгенерированы в выходной буфер, мы сохра─ няем указатель фазы, чтобы он был доступен в следующий раз, и переходим на канал B. Метод генерации отсчётов для канала B так─ же может быть либо FM-синтезом, либо синх─ ронизацией осцилляторов. Код подготовки почти такой же,как и для канала A, поэтому пропустим его и перейдём сразу к генерации отсчётов, начиная с FM- синтеза. Генерация звука для канала B FM-синтез для канала A записывает непо─ средственно в буфер отсчётов (перезаписы─ вая его предыдущее содержимое),но для двух других каналов мы должны смешать наши сге─ нерированные отсчёты с содержимым буфера. Это означает, что цикл генерации отсчётов канала B (естественно, развернутый) весьма отличается от цикла A. Однако каналы B и C используют одну и ту же процедуру для ге─ нерации FM, чтобы уменьшить дублирование кода. Перед входом в цикл мы сбрасываем флаг переноса из альтернативного набора регистров, чтобы сигнализировать, что мы в данный момент обрабатываем канал B. Позже код канала C снова войдет в тот же цикл с сброшенным флагом переноса, и поток выпол─ нения завершится в нужном месте. Это быст─ рее, чем вызов подпрограммы, и не требует использования SP. На этот раз каждая итерация цикла нем─ ного короче: ;такты ld c,ixh ;8/8 ;устанавливаем фазу ld a,(bc) ;7/15 ;байт формы волны ld e,a ;4/19 ld a,(de) ;7/26 ;байт несущей волны add a,(hl) ;7/33 ;смешиваем с ;содержимым буфера ld (hl),a ;7/40 ;сохраняем в буфер inc l ;4/44 add ix,sp ;15/59 ;переход к след. фазе Мы больше не дублируем отсчёты, поэтому цикл должен выполняться в течение полных 60 итераций. Теперь отсчёт смешивается с отсчётом из канала А перед сохранением в буфере. Теперь о синхронизации осцилляторов.Это эффект,полученный с использованием высоко─ частотной несущей волны, которая сбрасыва─ ет свою фазу всякий раз, когда форма волны модулятора завершает цикл, создавая инте─ ресный звук. Две формы волны имеют незави─ симые периоды, и к ним можно применить из─ менение высоты тона. Пример одной итерации синхронизации ос─ цилляторов вместе с таблицей, показывающей использование регистров: ;такты add ix,sp ;15/15 ;след.фаза модулятора sbc a,a ;4/19 ;если carry, то a=#ff, ;иначе a=#00 and e ;4/23 ;AND с фазой несущей add a,b ;4/27 ;следующая фаза несущей ld e,a ;4/31 ;сохраняем несущую фазу ld a,(de) ;7/38 ;байт формы волны add a,(hl) ;7/45 ;смешиваем с ;содержимым буфера ld (hl),a ;7/52 ;сохраняем в буфер inc l ;4/56 Регистр Назначение ======= ========== A Общее назначение B Шаг фазы несущей DE Указатель на форму волны HL Указатель на буфер вывода IX Счетчик модулятора / фаза Первое, что мы делаем,- это увеличиваем фазу модулятора, добавляя к ней значение модуляции. Теперь мы хотели бы проверить, завершен ли цикл формы сигнала модуляции, и если да, сбросить фазу несущей. К счастью, есть простой способ сделать это без дорогостоящих ветвлений. Команда SBC A,A вычитает A из себя с переносом, в результате чего A содержит #ff, если флаг переноса был установлен, или #00 в проти─ вном случае. Если мы затем применим AND результата с фазой несущей, мы должны по─ лучить либо неизменную фазу, либо ноль в зависимости от того,переполнилась инструк─ ция ADD IX,SP или нет. У этого подхода есть одна проблема:если все ноты имеют положительные значения, то─ гда флаг переноса будет противоположным тому, что мы хотим. Он будет установлен только после завершения цикла модуляции сигнала, в противном случае он будет сбро─ шен. Решение этой проблемы простое: инвер─ тировать все значения в таблице поиска вы─ соты тона. Это означает, что сигналы на всех каналах теперь воспроизводятся в об─ ратном порядке, но этот побочный эффект вряд ли будет заметен. Затем фаза несущей увеличивается путём добавления значения шага несущей, и мы по─ лучаем байт формы сигнала,который смешива─ ется с отсчётом из канала A и сохраняется обратно в буфер. Обработка канала C Завершив обработку канала B, мы прошли половину пути. Следующим идёт канал C, и единственный поддерживаемый им метод гене─ рации звука - это FM-синтез. Генерация звука для канала C Код подготовки канала C во многом такой же, как и для других каналов, поэтому я не думаю, что мне нужно показывать его снова. Поскольку канал C использует код генерации совместно с каналом B, нам просто нужно запустить этот код ещё раз. Однако мы дол─ жны быть осторожны, заранее должен быть настроен указатель на воспроизводимый в данный момент сэмпл, чтобы он воспроизво─ дился правильно. Обработка мелодии После обработки канала C мы можем при─ ступить к обработке состояния мелодии.Тай─ мер мелодии уменьшается, и если он равен нулю, мы сбрасываем его обратно на текущую скорость мелодии и уменьшаем таймер патте─ рна. Если он также равен нулю,мы загружаем следующий паттерн мелодии. Так как этот случай встречается относительно редко (один раз в несколько секунд), я решил не подгонять этот кусок кода по тактам,поэто─ му время может быть немного превышено. Мы переходим к следующей пятибайтовой записи ордера сонга и возвращаемся к точке зацикливания,если дошли до конца. Затем мы получаем пять индексов паттерна для теку─ щей позиции (по одному для каждого кана─ ла), ищем каждый из них в таблице паттер─ нов,чтобы получить адрес паттерна,и сохра─ няем указатель паттерна в переменных для этого канала. Мы также копируем таймер, указывающий на количество начальных строк в паттерне до ноты. Длина нового паттерна получается из ка─ нала D. Обработка канала D Мы почти закончили, осталось только об─ работать канал D, самый простой из пяти каналов. Если время играть сэмпл в канале D, берём индекс сэмпла и используем его для получения адреса и длины этого сэмпла из таблицы сэмплов. В противном случае пе─ ремещаем указатель текущего сэмпла на 60 отсчётов за вычетом поправки, сделанной ранее как часть обработки канала C. Если сэмпл закончил воспроизведение,мы проигры─ ваем 60-байтовый пустой сэмпл. На этом об─ работка канала D завершена. Общие задачи Небольшая хитрость, которая появилась из-за того, что у меня закончились такты в определённом месте программы. Расположение пары адресов перехода устанавливается на основе переменной таймера мелодии, чтобы не тратить время на эту обработку где-то ещё. Последнее,что мы делаем в основном цик─ ле - это меняем местами два буфера чтения/ записи для следующей итерации основного цикла и сохраняем фазу канала C на буду─ щее.
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября