Как написать игру для ZX Spectrum на ассемблере

       

МУЗЫКАЛЬНЫЙ СОПРОЦЕССОР



МУЗЫКАЛЬНЫЙ СОПРОЦЕССОР

Несоизмеримо большими возможностями для создания звукового оформления игровых программ обладают компьютеры ZXSpectrum 128 и Scorpion ZS 256, благодаря встроенному в них трехканальному музыкальному сопроцессору.

Мы уже показали, как можно в одном стандартном звуковом канале совместить два голоса, однако из-за наложения частот звук при этом получается «грязным», чересчур насыщенным гармониками, напоминая по тембру широкораспространенный в свое время гитарный эффект FUZZ. Музыкальный же сопроцессор позволяет получить одновременно до трех чистых тонов. Но его достоинства не ограничиваются только этим.

Как известно, музыкальный звук характеризуется тремя основными параметрами: частотой, которая определяет высоту тона, тембром (окраской), зависящим от количества и состава гармоник и амплитудой, определяющей громкость звучания. Используя только стандартный звуковой канал, легко можно управлять высотой звука, несколько сложнее получить различную окраску тона и практически невозможно влиять на громкость, а вот музыкальный сопроцессор позволяет манипулировать и этим третьим параметром, варьируя громкость звука от Forte до полного его исчезновения. Правда, что касается тембра, то и здесь, к сожалению, нет простых решений и приходится прибегать примерно к тем же методам, что и при управлении стандартным выводом. Зато вы можете без труда получить эффект «белого» шума, причем его среднюю высоту также можно регулировать.

Нельзя не упомянуть и еще об одной особенности музыкального сопроцессора. Он работает совершенно независимо, без опеки центрального процессора, поэтому последний может быть занят каким-нибудь полезным делом (например, опрашивать клавиатуру либо выводить на экран текст или графику), в то время как музыкальный сопроцессор самостоятельно извлекает звук. CPU лишь изредка нужно отвлекаться от своей работы, чтобы дать своему «коллеге» указание перейти к следующей ноте, а затем он вновь может вернуться к решению более насущных проблем.


Вы знаете, что для управления работой музыкального сопроцессора из Бейсика-128 имеется дополнительный (по отношению к стандартному Spectrum-Бейсику) оператор PLAY, но он на самом деле не реализует и десятой доли всех немыслимых возможностей, которые могут быть осуществлены только из ассемблера. Об этом можно судить хотя бы по тем играм, которые написаны специально для Spectrum 128.

Звук извлекается программированием собственных регистров сопроцессора, которые так же, как и регистры CPU имеют по 8 разрядов. Всего их насчитывается 16 (обозначаются от R0 до R15), но нас будут интересовать только 14 из них, так как остальные два служат для несколько иных целей, о чем сказано, например, в [2]. Сначала мы рассмотрим функции этих регистров, а затем расскажем о том, как с ними обращаться.

Первые шесть регистров (R0...R5) образуют три пары и задают высоту звука для каждого из трех каналов в отдельности (сами каналы обозначаются буквами A, B и C). То есть регистровая пара R0/R1 определяет частоту тона в канале A, пара R2/R3 делает то же самое для канала B и R4/R5 - для C. Хотя каждая пара состоит из 16 бит, используются только 12 младших разрядов: все 8 бит младшего регистра (R0, R2 и R4) и 4 младших бита старшего регистра (R1, R3 и R5). Таким образом, числа, определяющие высоту звука, находятся в пределах от 0 до 4095 включительно. В табл. 10.1 приводится соответствие звуков из диапазона неполных девяти октав и чисел, определяющих эти ноты.

Таблица 10.1. Значения для регистров R0...R5

 СККБМ12345
До 338916958474242121065326
До диез 319916008004002001005025
Ре 30201510755377189944724
Ре диез 28501425712356178894522
Ми 26901345673336168844221
Фа 25391270635317159794020
Фа диез 23971198599300150753719
Соль 22621131566283141713518
Соль диез 21351068534267133673317
Ля403120151008504252126633116
Си бемоль38041902951476238119593015
Си35911795898449224112562814
<


Следующий регистр - R6 - определяет среднюю частоту выводимого шума. Поскольку получение шумовых эффектов одновременно в разных голосах не имеет практического применения (их все равно невозможно будет различить на слух), то этот регистр является общим для всех трех каналов. Для него можно задавать значения от 0 до 31, то есть значащими являются только пять младших битов.

Регистр R7 служит для управления звуковыми каналами. Он подобен флаговому регистру центрального процессора и значение имеет каждый отдельный бит. Младшие три бита используются для управления выводом чистого тона в каждый из трех каналов. Если бит установлен, вывод запрещен, а при сброшенном бите вывод звука разрешается. Бит 0 связан с каналом A, 1-й бит относится к каналу B и 2-й - к C. Биты 3, 4 и 5 заведуют выводом в каналы A, B и C соответственно частоты «белого» шума. При установке бита вывод также запрещается, а при сбросе его в 0 - разрешается. 6-й и 7-й биты для извлечения звука значения не имеют.

Регистры R8, R9 и R10 определяют громкость звука, выводимого соответственно в каналы A, B и C. С их помощью можно получить 16 уровней громкости, посылая в них значения от 0 до 15. То есть значение для получаемой амплитуды в этих регистрах имеют 4 младших бита. Однако следует знать еще об одной интересной особенности этих трех регистров. Если в каком-нибудь из них установить 4-й бит (например, послав в него число 16), то получится звук не с постоянной, а с изменяющейся во времени громкостью. В этом случае необходимо указать дополнительную информацию в регистрах R11, R12 и R13.

Спаренные регистры R11 и R12 задают скорость изменения громкости звука: чем больше число, тем более плавной будет огибающая. В них можно записывать значения от 0 до 65535. Надо сказать, что изменение младшего регистра почти не ощущается, поэтому чаще достаточно определять лишь регистр R12.

Регистр R13 формирует огибающую выходного сигнала. Установкой одного или нескольких битов из младшей четверки можно получить несколько разнообразных эффектов. При установке нулевого бита звук получается затухающим, если установить 2-й бит, громкость будет наоборот увеличиваться, а установив одновременно 1-й и 3-й биты, вы получите звук, постоянно изменяющийся по громкости.



Последними тремя регистрами действительно иногда бывает удобно пользоваться, хотя гораздо чаще огибающая формируется программным путем, что позволяет получить значительно большее разнообразие оттенков звучания.

Перейдем теперь к вопросу, как программируются регистры музыкального сопроцессора. Связь с ними осуществляется через порты с адресами 49149 (#BFFD) и 65533 (#FFFD). Чтобы записать какое-либо значение в любой из регистров, его необходимо прежде всего выбрать (или назначить), выполнив команду OUT в порт 65533. Например, регистр R8 выбирается следующими командами:

LD BC,65533 ;в паре BC - адрес порта ; для выбора регистра LD A,8 ;в аккумуляторе - номер регистра OUT (C),A ;выбор

После этого в выбранный регистр можно записывать данные либо читать его содержимое. Для записи используется порт 49149, а для чтения - опять же 65533. Приведем фрагмент программы, в котором читается значение установленного ранее регистра и если оно не равно 0, содержимое регистра уменьшается на единицу:

LD BC,65533 ;адрес порта для чтения IN A,(C) ;читаем значение текущего регистра JR Z,ZERO ;если 0, обходим DEC A ;уменьшаем на 1 ; Выбираем порт 49149 для записи ; (значение регистра C остается прежним - #FD) LD B,#BF OUT (C),A ;записываем значение в выбранный регистр ZERO ......... ;продолжение программы

Добавим к сказанному, что выбор регистров музыкального сопроцессора и все манипуляции с ними лучше производить при запрещенных прерываниях, хотя в приведенных примерах это и не отражено.

Можно написать универсальную подпрограмму, которая считывает из блока данных значения всех регистров и тем самым задает параметры звуков одновременно для всех трех каналов:

OUTREG DI ;запрещаем прерывания ; Данные будем считывать из массива DATREG ; в обратном порядке, начиная с последнего элемента LD HL,DATREG+13 LD D,13 ;начальный номер загружаемого регистра LD C,#FD ;младший байт адреса порта сопроцессора OUTR1 LD B,#FF ;адрес для выбора регистра OUT (C),D ;выбираем регистр LD B,#BF ;адрес для записи в регистр ; Записываем в порт байт из ячейки, адресуемой парой HL, ; и уменьшаем HL на 1 OUTD DEC D ;переходим к следующему регистру ; (с меньшим номером) JP P,OUTR1 ;повторяем, если записаны еще не все ; регистры (D >= 0) EI ;разрешаем прерывания RET DATREG DEFS 14 ;массив данных для регистров сопроцессора



Эта процедура вполне может быть использована как для получения отдельных звуковых эффектов, так и для создания музыкальных произведений. Основная сложность заключается в написании программы, которая бы изменяла соответствующим образом элементы массива DATREG и тем самым управляла работой сопроцессора. Чтобы получить действительно первоклассное звучание, потребуется достаточно серьезная программа, которую объем книги, к сожалению, не позволяет здесь привести (надеемся, ее все же удастся включить в одну из последующих книг серии «Как написать игру»). Поэтому мы предлагаем более простую процедуру, извлекающую отдельные звуки, характер которых, тем не менее, вы сможете изменять практически в неограниченных пределах. (Упомянутая музыкальная программа строится, в общем, по такому же принципу, что и приводимая здесь процедура. Поэтому после ее досконального изучения вы можете попытаться самостоятельно написать программу, пригодную для исполнения музыкальных произведений.)

Эту программу также лучше составить в виде прерывания, чтобы можно было получать звуки любой продолжительности и не отвлекаться на их формирование в основной программе. Начнем с выяснения, какие переменные нам здесь потребуются. Помимо уже известных по предыдущему примеру значений базового и текущего адресов в блоке данных, описывающем характер звучания и флага разрешения вывода звука понадобятся переменные, задающие частоту тона и шума, длительность звучания, количество повторений эффекта, флаг разрешения вывода в канал тона или шума. Кроме этого, необходимо каким-то образом задавать огибающую звука и характер изменения высоты тона или шума. Эти две последние характеристики должны изменяться независимо друг от друга, поэтому порядок их изменения лучше всего определять в двух дополнительных блоках данных. То есть к перечисленным переменным добавятся еще четыре: базовые и текущие адреса для каждого из этих двух блоков.

В связи с обилием переменных, а также и с тем, что все они должны иметься в трех экземплярах - для каждого канала по полному набору - лучше всего свести их в таблицу, начало которой передавать в регистре IX. Построим такую таблицу с указанием смещений от начала и значений каждого смещения:



0/1 - базовый адрес блока данных эффекта 2/3 - текущий адрес в блоке данных 4/5 - частота тона 6 - частота шума 7 - флаг разрешения вывода в канал 8 - длительность звучания эффекта 9 - количество повторений эффекта 10 - вывод тона (1), шума (8) или их комбинации (9) 11/12 - базовый адрес данных для формирования частоты 13/14 - текущий адрес данных для формирования частоты 15/16 - базовый адрес данных для формирования огибающей 17/18 - текущий адрес данных для формирования огибающей

Поскольку задавать все значения переменных в основной программе немыслимо, они должны быть определены в блоках данных, а чтобы иметь возможность устанавливать их выборочно (ведь, например, при выводе чистого тона частота шума не имеет значения), используем принцип управляющих кодов. В основном блоке данных будем определять адреса двух дополнительных блоков и заказывать выводимый звук - чистый тон, шум или их комбинацию, а также длительность звучания эффекта:

0 - конец данных (возврат на повторение эффекта) 1 - адрес данных для изменения тона (+2 байта адреса) 2 - адрес данных для формирования огибающей (+2 байта адреса) 3 - управление выводом тона/шума (+2 байта: 1-й определяет вывод тона, шума или комбинированный вывод, 2-й - длительность звучания)

Когда программа встретит код 0, вывод звука либо прекратится, либо весь эффект повторится еще раз - в зависимости от заданного изначально количества повторений. Следующие два байта после кодов 1 или 2 интерпретируются как адреса дополнительных блоков данных, определяющих характер изменения частоты звука и огибающей соответственно. С этих двух кодов должен начинаться любой блок данных, иначе программа не будет «знать», каким образом изменять звук. Последний код (3) служит собственно для извлечения звука. После него необходимо указать еще два однобайтовых параметра: первый задает вывод тона (1), шума (8) либо одновременно и тона и шума (9), второй определяет продолжительность заданного звука в 50-х долях секунды.



В дополнительных блоках данных также используем некоторые управляющие коды. После байта со значением 128 будет задаваться двухбайтовая величина частоты тона (см. ), а за кодом 129 последует байт средней частоты шума, который может иметь значения от 0 (самый высокий звук) до 31 (самый низкий). Определив частоты, можно поставить метку начала их изменения, поставив код 130. Далее должны следовать значения приращения частоты, которые лежат в диапазоне от -124 до +127. За один такт прерываний (1/50 секунды) будет выполнен один из этих кодов. Завершаться этот блок данных должен кодом 131, после которого все составляющие его числа будут проинтерпретированы с начала или от кода 130, если таковой был использован. Соберем все коды данных для изменения частоты воедино:

128 - задание частоты тона (+2 байта частоты тона) 129 - задание частоты шума (+1 байт частоты шума) 130 - метка повторения эффекта 131 - возврат на начало или метку -124...+127 - приращение частоты

В блоке данных для формирования огибающей будет использован только один управляющий код 128, отмечающий конец интерпретации записанных значений и переход на повторение эффекта. Остальные коды, задающие громкость звука, могут передаваться числами от 0 до 15. Каждое из этих чисел также обрабатывается за одно прерывание.

128 - возврат на начало данных огибающей 0...15 - значение громкости

Прежде чем продолжить, хочется обратить ваше внимание на то, что при построении собственных блоков данных необходимо следить за порядком следования управляющих кодов, иначе результат будет очень далек от ожидаемого. Кроме того, ни один из блоков не может состоять только из управляющих кодов, поскольку в этом случае компьютер просто «зависнет».

Выяснив, что мы хотим в итоге получить, напишем процедуру для интерпретации описанных блоков данных. Она имеет довольно внушительные размеры, но все же попытайтесь с ней разобраться. На входе перед обращением к ней в аккумуляторе задается номер канала (0 для A, 1 для B и 2 для C), а индексный регистр IX, как мы уже говорили, адресует таблицу переменных соответствующего канала:



GETSND LD (N_CHAN),A ;запоминаем номер канала LD A,(IX+7) AND A RET Z ;выход, если вывод в канал не разрешен DEC (IX+8) ;уменьшаем счетчик длительности звука JR NZ,GETS5 LD L,(IX+2) ;текущий адрес основного блока данных LD H,(IX+3) GETS1 LD A,(HL) INC HL AND 3 JR NZ,GETS2 ; Код 0 - возврат на повторение эффекта LD A,(IX+11) ;установка текущего адреса LD (IX+13),A ; данных изменения частоты LD A,(IX+12) ; на начало LD (IX+14),A ; блока LD L,(IX) ;начальный адрес основного LD H,(IX+1) ; блока данных DEC (IX+9) ;уменьшение количества повторений JR NZ,GETS1 XOR A ;завершение звучания в канале LD (IX+7),A ;запрет вывода звука в канал LD (IX+10),A LD A,(N_CHAN) ADD A,8 LD E,A XOR A JP SETREG ;выключение громкости GETS2 DEC A JR NZ,GETS3 ; Код 1 - адрес данных для изменения тона LD A,(HL) ;младший байт адреса LD (IX+11),A INC HL LD A,(HL) ;старший байт адреса LD (IX+12),A INC HL JR GETS1 GETS3 DEC A JR NZ,GETS4 ; Код 2 - адрес данных для формирования огибающей LD A,(HL) ;младший байт адреса LD (IX+15),A LD (IX+17),A INC HL LD A,(HL) ;старший байт адреса LD (IX+16),A LD (IX+18),A INC HL JR GETS1 ; Код 3 - управление выводом тона/шума GETS4 LD A,(HL) ;1 - тон, 8 - шум, 0 - пауза, ; 9 - тон и шум одновременно INC HL AND 9 LD (IX+10),A LD A,(HL) ;продолжительность вывода INC HL LD (IX+8),A LD (IX+2),L LD (IX+3),H ; Восстановление текущего адреса данных для изменения тона LD A,(IX+11) LD (IX+13),A LD A,(IX+12) LD (IX+14),A GETS5 LD L,(IX+13) LD H,(IX+14) GETS6 LD A,(HL) INC HL CP 128 ;задание частоты тона JR NZ,GETS7 LD A,(HL) LD (IX+4),A INC HL LD A,(HL) AND 15 LD (IX+5),A INC HL JR GETS6 GETS7 CP 129 ;задание частоты шума JR NZ,GETS8 LD A,(HL) AND 31 LD (IX+6),A JR GETS6 GETS8 CP 130 ;метка нового начала JR NZ,GETS9 LD (IX+11),L LD (IX+12),H JR GETS6 GETS9 CP 131 ;возврат к началу JR NZ,GETS10 LD L,(IX+11) LD H,(IX+12) JR GETS6 ; Изменение частоты звука или шума GETS10 LD (IX+13),L LD (IX+14),H LD D,0 BIT 7,A JR Z,GETS11 LD D,255 GETS11 LD E,A LD L,(IX+4) LD H,(IX+5) ADD HL,DE LD (IX+4),L LD (IX+5),H ADD A,(IX+6) LD (IX+6),A ; Определение элементов массива DATREG, задающих частоту LD (DATREG+6),A ;частота шума LD A,(N_CHAN) ADD A,A LD E,A LD A,L PUSH HL CALL SETREG ;младший байт частоты тона POP HL INC E LD A,H CALL SETREG ;старший байт частоты тона ; Формирование огибающей LD L,(IX+17) LD H,(IX+18) GETS12 LD A,(HL) INC HL CP 128 ;повторение с начала JR NZ,GETS13 LD L,(IX+15) LD H,(IX+14) JR GETS12 GETS13 LD (IX+17),L LD (IX+18),H AND 15 PUSH AF LD A,(N_CHAN) ADD A,8 LD E,A POP AF JP SETREG ;задание громкости звука



В процедуре обработки прерываний приведенная подпрограмма будет вызываться трижды для определения характера звучания в каждом из трех каналов независимо друг от друга. Это дает возможность использовать в программе одновременно три самостоятельных источника звука, закрепив за каждым игровым объектом свой звуковой канал. Правда, здесь есть одно небольшое ограничение: поскольку средняя частота «белого» шума общая для всех трех каналов, то его лучше выводить только в какой-то один, а другие два использовать для вывода изменяющегося тона.

Вот описываемая процедура обработки прерываний:

SND128 PUSH AF PUSH BC PUSH DE PUSH HL PUSH IX CALL NXTSND POP IX POP HL POP DE POP BC POP AF JP 56 NXTSND LD IX,CHAN_A XOR A CALL GETSND ;задание переменных для канала A LD IX,CHAN_B LD A,1 CALL GETSND ;задание переменных для канала B LD IX,CHAN_C LD A,2 CALL GETSND ;задание переменных для канала C ; Вычисление значения регистра R7, ; управляющего выводом в каналы тона и шума LD A,(CHAN_C+10) AND 9 ;выделяем биты 0 и 3 RLCA ;сдвигаем влево LD B,A ;результат сохраняем в регистре B LD A,(CHAN_B+10) AND 9 ;то же самое для других двух каналов OR B RLCA LD B,A LD A,(CHAN_A+10) AND 9 OR B CPL ;инвертируем биты LD E,7 ;устанавливаем данные регистра R7 в DATREG CALL SETREG ; Извлечение звука OUTREG LD HL,DATREG+13 LD D,13 LD C,#FD OUTR1 LD B,#FF OUT (C),D LD B,#BF OUTD DEC D RET M ;выход, если D < 0 JR OUTR1 DATREG DEFS 14 ; Задание элемента E массива DATREG значением из аккумулятора SETREG LD HL,DATREG LD D,0 ADD HL,DE LD (HL),A RET N_CHAN DEFB 0 ;номер текущего канала ; Таблицы переменных для каждого канала CHAN_A DEFS 19 CHAN_B DEFS 19 CHAN_C DEFS 19

Наше прерывание остается дополнить процедурами включения и выключения 2-го режима. Попутно желательно выполнить и некоторые другие действия, а именно, при установке 2-го режима нужно очистить все три таблицы переменных, инициализировав их нулевым байтом, а при возврате первого режима прерываний необходимо еще убедиться, что звук выключен:



INITI LD HL,CHAN_A ;инициализация таблиц переменных LD DE,CHAN_A+1 LD BC,19*3-1 LD (HL),0 LDIR LD HL,SND128 ;установка прерывания

STOPI CALL IMOFF ;возврат к 1-му режиму LD A,#FF ;выключение звука LD E,7 CALL SETREG ;запись в регистр сопроцессора R7 ; значения #FF JP OUTREG

Порядок действий при использовании описанной программы должен быть следующим. В начале работы нужно включить 2-й режим прерываний, вызвав процедуру INITI. Извлечение очередного звука необходимо начинать с определения некоторых переменных в таблицах CHAN_A, CHAN_B или CHAN_C. Для этого нужно записать в первые два байта таблицы, соответствующей выбранному каналу, адрес начала основного блока данных и то же значение продублировать в следующих двух байтах таблицы. Затем указать количество повторений звука по смещению +9, а элементы таблицы +8 и +7 инициализировать байтом 1 (переменную по смещению +7 нужно задавать обязательно в последнюю очередь, так как именно она «запускает» звук). По окончании работы (или если в программе предусмотрены обращения к дисководу) следует восстановить стандартный режим обработки прерываний и выключить звук, обратившись к подпрограмме STOPI.

Продемонстрируем применение описанной процедуры извлечения звуков на примере небольшой игры, которую назовем БИТВА С НЛО. По земле катается грузовик с зенитной лазерной установкой, который методично расстреливает маячащую в небе «летающую тарелку» (Рисунок  10.1). НЛО также не остается внакладе и отвечает хоть и малоприцельным, но зато плотным веерным огнем.


Содержание раздела