Технические аспекты сотворения мира. Часть 4
Воспроизведение звука
Итак, как и было обещано, в этой части будет дано решение такой проблемы, как воспроизведение звука через Sound Blaster. Вообще, кроме решения, представленного здесь, существует, по крайней мере, еще одно. Далее оно будет кратко описано. Это делается для того, чтобы те, кому придется не по душе путь, предложенный в этой статье, не отчаивались, а попробовали решить проблему по-другому.
Ознакомимся с тем, как использовать приведенные ниже подпрограммы.
uses Crt, Dos; var wPort: Word; bPlayFlag: Byte; wRate: Wor d; wCounter, wCurrentCounter: Word; wWritePort: Word; p Interrupt08hVector: Pointer; wPointer, wBuffer: Word; wLimit: Word; pObserver: Pointer; procedure SoundDriver; interrupt; assembler; a sm cmp bPlayFlag,0 je @3 mov dx,wWritePort @1 : in al,dx test al,80h jnz @1 mov al,10h out dx,al @2: in al,dx test al,80h jnz @2 les di,[offset wPointer] mov al,es:[di] ou t dx,al inc di mov wPointer,di cmp di,wLimit jbe @3 mov bPlayFlag,0 pushf call pObserver <tab>@3: dec wCurrentCounter jz @4 mov al,20h out 20h,al jmp @5 @4: mov ax,wCounter mo v wCurrentCounter,ax pushf call pInterrupt08hVector @5: end; function InitializeSoundDriver: Boolean; var wHour, wMinute, wSecond , wSec100: Word; l1, l2: Longint; b1: Byte; begin<r> wPort:=$210; while wPort<>$290 do begin Port[wPort+6]:=1; Delay(10); Port[wPort+6]:=0; Delay(10); GetTime(wHour, wMinut e, wSecond, wSec100); l1:=wHour*60*60*100+wMinute*60*100+wSecond*100+wSec10 0; b1:=0; while b1<2 do begin if b1=0 then if (Port[wPort+$E] and $80)<>0 then b1:=1; if b1=1 then if Port[wPort+$A]=$AA then b1:=2; if b1<>2 then begin GetTime(wHour, wMinute, wSecond, wSec100); l2:=wHour*60*60*100+wMinu-te*60*100+wSecond*100+wSec100; if (l2-l1)>100 then b1:=3 end end; if b1=2 then begin bPlayFlag:=0; wWritePort:=wPort+$C; while Port[wWritePort]>=$80 do; Port[wWritePort]:=$D1; GetIntVec(8, pInterrupt08hVector); <tab> SetIntVec(8, @SoundDriver); SetRate( 22050); InitializeSoundDriver:=True; Exit end; wPort:=wPort+$10 <tab> end; InitializeSoundDriver:=False end; procedure SetRat e(w1: Word); begin wRate:=w1; wCounter:=Round(wRate/18.2); wCurrentCounter:=wCounter; Port[$43]:=54; Port[$40]:=Lo( 1193181 div wRate); Port[$40]:=Hi(1193181 div wRate) end; proced ure Play(pBuffer: Pointer; wLength: Word; pProcedure: Pointer); begin wPoin ter:=Word(pBuffer); wBuffer:=Word(Longint(pBuffer) div $10000); wLimit :=wLength+wPointer; pObserver:=pProcedure; bPlayFlag:=1 end; procedure Observer; interrupt; begin ... end; procedure Beep(bLevel: Byte); assembler; asm mov dx,wWritePort @1: in al,dx test al,80h jnz @1 mov al ,10h out dx,al @2: in al,dx test al,80h jnz @2 mov al,bLevel out dx,al end; pro cedure UninitializeSoundDriver; begin while Port[wWritePort]>=$80 do; Port[wWritePort]:=$D3; Port[$43]:=54; Port[$40]:=0; Port[$40]:=0; SetIntVec(8, pInterrupt08hVector) end;
Процедура SoundDriver является "драйвером", ответственным за воспроизведение звука в фоновом режиме. Поскольку эту процедуру не придется вызывать, не будем вдаваться в подробности и перейдем к следующей подпрограмме — InitializeSoundDriver.
Эта функция должна вызываться один раз в самом начале программы. Она инициализирует звуковой "драйвер" и производит некоторые другие необходимые действия. Кроме этого, InitializeSoundDriver на выходе выдает информацию: можно ли использовать звуковую карту. Если функция вернула значение False, значит либо звуковая карта отсутствует, либо драйвер для нее не загружен. После удачного вызова InitializeSo-undDriver в любой момент можно воспроизводить нужный звуковой сигнал, который должен быть представлен в формате PCM (Pulse Code Modulation — Импульсно-Кодовая Модуляция). Перед этим процедурой SetRate можно задать частоту воспроизведения (w1), которая должна быть не менее 19 и не более 65535 Гц. По умолчанию задается частота 22050 Гц.
Для фонового воспроизведения следует вызывать процедуру Play, которой передаются такие параметры, как указатель на буфер со звуковыми данными (pBuffer), длина буфера (wLength), а также указатель процедуры (pProcedure), которая будет вызвана после того, как все данные из буфера будут переданы на Sound Blaster. Эта процедура должна быть объявлена как подпрограмма, обрабатывающая прерывания (для примера см. подпрограмму Observer) и предназначена для использования следующим способом: поскольку звуковой сигнал, который необходимо проиграть, может иметь очень большой размер, а размер буфера не должен превышать 64 Кб, то тогда фрагменты сигнала сохраняются в нескольких буферах, и когда проиграет один фрагмент, процедура Observer вызовом подпрограммы Play заставит играть следующий и так далее.
Звуки можно воспроизводить не только через Play, но и через процедуру Beep, которой необходимо будет передавать значения амплитуды звукового сигнала. Поскольку подпрограммы, представленные в этой части, работают только с 8-битными монофоническими звуками, закодированными в формате PCM, то следует помнить, что значения от 0 до 127 соответствуют отрицательной амплитуде сигнала, 128 — нулевой, а 129-255 — положительной. Используя процедуру Beep, программисту придется решать такую задачу, как контроль паузы между двумя последовательными вызовами этой подпрограммы. Чем больше будет пауза, тем с меньшей частотой будет идти воспроизведение. Использовать процедуру Delay модуля Crt для решения этой проблемы не рекомендуется, лучше выполнять какую-нибудь арифметическую команду определенное количество раз, определив предварительно, сколько раз эта команда выполняется, скажем, за одну секунду.
После того, как необходимость в использовании Sound Blaster'а исчезнет или же перед завершением программы, следует вызвать процедуру UninitializeSoundDriver, которая выполнит все действия, необходимые для дальнейшего нормального функционирования системы.
Заканчивая обсуждение представленных выше подпрограмм, стоит затронуть несколько небольших деталей. Во-первых, нельзя менять порядок следования переменных wPointer и wBuffer — в памяти они должны идти одна за другой, причем wPointer первая. В противном случае, нарушится работоспособность звукового "драйвера". Во-вторых, нельзя менять частоту вызова IRQ0 (прерывание 08h), поскольку иначе изменится частота фонового воспроизведения (см. документацию по программируемому таймеру 8254, канал 0, режим 3). В-третьих, если в программе потребуется перехватить прерывание 08h, то следует помнить, что вызываться оно будет с частотой, задаваемой процедурой SetRate, поэтому обработчик прерывания придется подкорректировать для того, чтобы все работало так, как было задумано.
С приведенными подпрограммами закончили, теперь кратко рассмотрим другой способ воспроизведения звука через Sound Blaster. Суть его заключается в том, что данные на звуковую карту передаются через DMA (Direct Memory Access — Прямой Доступ к Памяти), причем, если говорить конкретно, данные передаются DSP (Digital Sound Processor — Процессор Цифрового Звука), который обеспечивает запись и воспроизведение 8- и 16-битного моно- и стереозвука, а также множество других вещей. Также используется микшер, который позволяет смешивать сигналы от разных источников, управляя громкостью каждого. Есть еще FM-синтезатор, который имитирует звучание различных музыкальных инструментов. Кроме всего указанного, для воспроизведения звука понадобится также работать с PIC (Programmable Interrupt Controller — Программируемый Контроллер Прерываний). Если идти этим путем, то объем исходных текстов увеличится примерно на порядок, причем владельцы устаревших звуковых карт останутся, скорей всего, за бортом, однако такой путь позволит использовать всю мощь современного звукового оборудования.
Прежде, чем завершить эту часть, ознакомимся еще с форматом WAV-файлов. Это необходимо для того, чтобы достать из них те данные, которые затем придется проигрывать через Sound Blaster.
Формат WAV является подформатом формата RIFF (Resource Interchange File Format — Формат Файла Обмена Ресурсами). Поэтому в самом начале WAV-файла идут четыре символа "RIFF". За ними следует число типа Longint (4 байта), показывающее, какова длина файла без учета первых восьми байт. Далее идет сигнатура WAV-формата — четыре символа "WAVE". После сигнатуры в произвольном порядке следуют различные секции. Нас будут интересовать две из них, которые обязательно присутствуют в каждом WAV-файле, — "Формат" и "Данные".
Секция "Формат" обозначается четырехсимвольной сигнатурой "fmt " (три буквы и пробел), после которой следует число типа Longint, равное оставшейся длине секции. Следующие два числа имеют тип Word. Если первое равно 1, значит, звуковые данные представлены в формате PCM. Второе число показывает число каналов (1 — моно, 2 — стерео). Далее идут два числа типа Longint: первое — частота дискретизации, второе — количество передаваемых в секунду байт. Следующее за ними число типа Word показывает, на какое количество байт выравнивается длина секции "Данные". Последнее число, которое нас интересует, в случае, когда данные представлены в PCM, имеет тип Word и равно количеству бит, отводимых на представление одного сэмпла.
Начало секции "Данные" определяется по сигнатуре "data", после которой следует четырехбайтное число, равное оставшейся длине секции. Далее идут сами данные, описывающие амплитуду звукового сигнала.
После рассмотрения структуры WAV-файла становится понятно, как из него достать ту информацию, которая нужна. При этом данные, сохраненные в файле, должны соответствовать 8-битному монофоническому звуку, представленному в формате PCM. Если это не так, то какой-нибудь программой, работающей с WAV-файлами, данные следует преобразовать в нужное представление.
Вот, в принципе, и все, что необходимо знать для того, чтобы воспроизводить WAV-файлы. Закончив рассмотрение их структуры, мы также завершили последнюю часть статьи "Технические аспекты сотворения мира". Мы рассмотрели решения основных проблем, которые могут возникнуть в процессе разработки компьютерных игр. Конечно же, есть и другие проблемы, но наверняка поиск их решения доставит много удовольствия тем, у кого они возникнут...
Сергей Иванчегло

