Немного истории
Использование DMA для записи и воспроизведения звука
Формат WAV-файла
Пример программы

Немного истории

Когда разработчики компании Intel создавали первый 16-разрядный процессор I8086, они, скорее всего, рассматривали его как переходную модель от 8- к 16-разрядным процессорам и не могли даже предположить, что он станет образцом, совместимость с которым будут вынуждены поддерживать как сама компания Intel в дальнейших разработках, так и большинство ее конкурентов. В отличие от своего 8-разрядного предшественника I8080, у которого была 16-разрядная шина адреса, новый процессор имел два режима адресации: 16-разрядный - для обеспечения совместимости с программами, написанными для I8080, и 32-разрядный. Однако последний был не линейным, а сегментированным, в результате чего полный физический адрес состоял только из 20 разрядов.

Переход от 64-Кбайт к 1-Мбайт адресному пространству казался тогда огромным шагом вперед. Возможно даже, этот процессор несколько опередил свое время, по крайней мере вскоре Intel вынуждена была сделать маленький шаг назад и выпустить модификацию этого процессора, продолжающую оставаться 16-разрядной внутри, но имеющую 8-разрядную внешнюю шину. Новинка, получившая название I8088, была выбрана фирмой IBM, до того момента выпускавшей только большие ЭВМ, для ее первого ПК. Немаловажную роль в выборе именно этого процессора сыграло наличие 8-разрядной шины, позволявшей использовать в конструкции компьютера дешевые 8-разрядные устройства, разработанные для I8080. Применение контроллера прямого доступа к памяти (DMAC - Direct Memory Access Controller), относящегося к этой категории устройств, сразу ограничило максимальный размер передаваемого им единого блока информации 64 Кбайт. Кроме того, фирма IBM решила сэкономить на специальном дешифраторе адреса для DMAC и, отказавшись от применения сегментированной модели памяти для этого устройства, подключила его адресные выходы к младшим разрядам 20-разрядной адресной шины напрямую, а для задания старшей части адреса использовала 4-разрядный внешний порт, в результате чего оказалась невозможной пересылка данных через границу сегмента размером 64Кбайт. Компромиссные решения, принятые Intel и IBM, неожиданно стали промышленным стандартом, а со всеми недостатками когда-то выбранной архитектуры сегодня приходится бороться программистам.

Использование DMA для записи и воспроизведения звука

Первоначально при разработке DMAC предполагалось, что одним из основных его назначений будет более быстрая, чем у процессора, скорость пересылки данных из одной области памяти в другую. В случае с процессором I8080 так и было, но у семейства I8086/88 появились новые команды работы со строками, в результате чего такое использование DMAC оказалось нецелесообразным. С этого времени контроллер прямого доступа к памяти обычно применяется только для передачи данных между памятью и некоторыми периферийными устройствами. К стандартным устройствам такого типа можно отнести контроллер гибких дисков, а к нестандартным (не поддерживаемым на уровне BIOS) - звуковые платы, сканеры, стримеры etc. Причем если программы для сканеров и стримеров пишут в основном разработчики этих устройств, то программировать звуковые платы сегодня вынуждены многие программисты.

Стандартом de facto среди звуковых плат для ПК стала наиболее распространенная - Sound Blaster фирмы Creative Labs. На ее примере мы и рассмотрим способы программирования звуковых плат. Считывать звуковые данные с платы или воспроизводить их можно и без применения DMAC, работая только с eе портами, но при этом процессор будет полностью загружен и не сможет выполнять другую работу. Кроме того, объем записываемых или воспроизводимых данных ограничен объемом доступной процессору оперативной памяти, поэтому такой способ не получил широкого распространения. Обычно компьютер одновременно с воспроизведением звука должен выполнять и другую работу, зачастую более ресурсоемкую. Тогда без DMAC не обойтись.

В реальном режиме работы процессора для вывода звука через контроллер прямого доступа к памяти на звуковую плату необходимо выделить память для звуковых данных, запрограммировать эту плату и DMAC, а также переопределить прерывание. Процессор после этого может заниматься другой работой до наступления аппаратного прерывания, генерируемого звуковой платой по окончании звуковой последовательности. Затем процессор должен снабдить пару DMAC-звуковая плата очередной порцией данных. При этом программисту необходимо позаботиться о том, чтобы выделенный буфер, содержащий звуковую последовательность, не пересекал границу сегмента размером 64 Кбайт, а также требуется преобразовать 32-разрядный сегментированный адрес в 20-разрядный несегментированный.

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

Формат WAV-файла

В нашем журнале (Д. В. Солдатенков. Программируем Sound Blaster. "Мир ПК", #9/94) уже были опубликованы сведения об основах программирования Sound Blaster и формате VOC-файла (стандарт Creative), однако сегодня более распространен формат WAV (стандарт Microsoft Windows). Каждый из форматов допускает различные варианты представления заголовка. Упрощенная структура двух наиболее часто встречающихся вариантов для формата WAV приведена в таблице.

Структура WAV-файла
Смещение от начала файла Длина Описание
0h 4h Идентификатор формата ('RIFF')
4h 4h Длина блока данных (длина файла - 8h)
8h 4h Идентификатор блока звуковых данных ('WAVE')
0ch 4h Идентификатор подблока заголовка ('fmt" - с пробелом в конце)
10h 4h 000ch/0010h - длина подблока заголовка
14h 2h 01h - тип формата представления данных
16h 2h Число каналов (1 - моно, 2 - стерео)
18h 2h/4h Частота дискретизации, Гц
1ah/1ch 2h/4h Скорость передачи данных, байт/с (произведение числа каналов, частоты дискретизации и разрядности в байтах)
1ch/20h 2h Числов байт для представления одного отсчета (1 - 8 бит моно, 1 - 16 бит стерео)
1eh/22h 2h Разрядность, бит (8, 16)
20h/24h 4h Идентификатор подблока данных ('data')
24h/28h 4h Длина звуковых данных
28h/2ch   Звуковые данные

Пример программы

Простейшая программа, демонстрирующая работу со звуковой платой в защищенном режиме процессора, приведена в листинге 1. В примере решено было ограничиться лишь воспроизведением 8-битного звука, что обусловлено двумя причинами. Первая из них заключается в том, что все необходимые регистры и команды DSP (Digital Signal Processor - цифровой сигнальный процессор) звуковой платы были описаны ранее в статье Д.В. Солдатенкова, и это позволяет сократить объем журнальной публикации. Вторая причина связана с тем, что подавляющее большинство выпускаемых сегодня звуковых плат являются 16-разрядными и совместимы с Sound Blaster лишь на уровне 8-битного звука, т.е. подавляющее большинство звуковых плат, выпускаемых не Creative Labs, не совместимы с Sound Blaster 16, и соответственно 16-битный звук в них реализуется по-другому. Следовательно, стандарта, даже de facto, для воспроизведения 16-битного звука не существует.

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

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

Процедуры выделения и освобождения нижней памяти называются GetSBMem и FreeSBMem соответственно. Для облегчения переноса программы на другой компилятор или в другой язык программирования в листинге 2 приведены ассемблерные варианты этих процедур.

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

Листинг 1. Воспроизведение звука из WAV-файла 
через звуковую плату Sound Blaster

program playwavp;	{воспроизведение 8-битного звука}
					{в защищенном режиме процессора}
{$D+,L+,I+,R+,S+,V+}
uses dos,winapi;
const
   BlasterPort:word = $220;  {номер порта Sound Blaster}
   BlasterIRQ:word  =    5;  {номер прерывания Sound Blaster}
   BlasterDMAl:word =    1; {номер канала DMA Sound Blaster}
   maxlen = 35000;           {длина звукового буфера}
{   Seg0040 = $40;}
   SouBlas : Boolean = False; {было ли прерывание от SB }

function clock:longint;       {функция определения времени}
begin
   clock := MemL[Seg0040:$6c];
end;

var
    ExitSave:Pointer;  {адрес старой программы выхода в DOS}
    SBold:pointer;{адрес обработчика прерывания Sound Blaster'a}
    SBIRQ:byte;      { номер прерывания Sound Blaster'a }

{$F+}
procedure SBint ; interrupt;   { обработчик прерывания SB }
begin
   SouBlas:=True;
   Port[$20]:=$20;
end;

procedure MyExit; {дополнительная процедура при выходе в DOS}
begin
    ExitProc:=ExitSave;
    SetIntVec(SBIRQ+8,SBold);
end;
{$F-}

procedure IntInit(sbi:byte);  {установка нового вектора} 
						  {прерывания и т.д.}
begin          { sbi - номер аппаратного прерывания SB }
        if sbi < 8 then  SBIRQ:=sbi  else  SBIRQ:=2;
        GetIntVec(SBIRQ+8,SBold);
        SetIntVec(SBIRQ+8,@SBint);  {переопределение}
							{прерывания SB}
        ExitSave:=ExitProc;  {переопределение}
						  {процедуры выхода}
        ExitProc:=@MyExit;
end;

procedure WriteCommand(Comm:byte);  { процедура записи}
						{команды в регистр SB }
begin
   while (Port[BlasterPort+$0C] and $80) <> 0 do;
   Port[BlasterPort+$0C]:=Comm;
end;

procedure NotSupport;  { вывод на экран сообщения }
begin
   writeln('Format not supported');
   halt;
end;

var
   longi:longint;    {рабочая ячейка}
   arr1:pointer;     {для выравнивающего массива}
   pusto:word;       {длина выравнивающего массива}
   sndp:pointer;     {для массива звуковых отсчетов}
   DMAPage:byte;     {номер 64K сегмента для записи}
					   {в регистр DMA}
   DMAOfs :word;     {смещение звуковой последовательности}
					   {в нижней памяти}

procedure GetSBMem; {выделение буфера }
					 {нижней памяти для SB/DMA}
begin
   longi := GlobalDosAlloc(16);
   arr1 := ptr(longi and $0FFFF,0); {селектор нижней памяти}
   longi := $FFFF-(((longi shr 16) shl 4) and $ffff)+1;
   pusto := longi;   {столько осталось до начала}
					   {следующего 64k сегмента}
   GlobalDosFree(seg(arr1^));
   longi := GlobalDosAlloc(pusto);
   arr1 := ptr(longi and $0FFFF,0);  {селектор нижней}
							{памяти (выравн.масс.)}
   longi := GlobalDosAlloc(maxlen);
   sndp := ptr(longi and $0FFFF,0);  {селектор нижней}
							{памяти (для звука)}
   DMAPage := longi shr 28;
   DMAofs := 0;
end;

procedure FreeSBMem; {возвращение нижней памяти в систему}
begin
    GlobalDosFree(seg(sndp^));
    GlobalDosFree(seg(arr1^));
end;

{*****************************************}

const
   dmap : array[0..3]of byte = ($87,$83,$81,$82); {номера}
							      {регистров DMA}
var
   i : word;      {рабочая ячейка}
   t,friq:word;   {временной параметр/частота дискретизации}
   lenfil:word;   {длина считываемой части звуковых данных}
   snd:file;      {звуковой файл}
   riff:array[0..15]of char; {массив для чтения}
						 {неиспольз. частей заголовка}

Begin    {the main program}
    if paramcount <> 1 then begin
       writeln;
       writeln("  Usage: playwavp filename.wav');
       writeln;
       halt;
    end;
    IntInit(BlasterIRQ);    { переустанавливаем прерывания }
    Assign (snd,paramstr(1));
    Reset (snd,1);
    BlockRead(snd,riff,16);     { $00}
    BlockRead(snd,longi,4);   { $10 длина заголовка}
    BlockRead(snd,i,2);          { $14}
    BlockRead(snd,i,2);          { $16 число каналов}
    if i <> 1 then notsupport;
    BlockRead(snd,friq,2);      { $18 частота дискретизации}
    Seek(snd,longi+$12);       { $1A пропускаем заголовок}
						     {до предпосл.слова}
    BlockRead(snd,i,2);          {     разрядность}
    if i <> 8 then notsupport;
    GetSBMem;         {запрашиваем нижнюю память для звука}
    BlockRead(snd,longi,4);     {  'data'}
    BlockRead(snd,longi,4);     { длина данных}
    if longi > maxlen then lenfil := maxlen else lenfil := longi;
    BlockRead(snd,sndp^,lenfil); {звуковые данные}
    close (snd);
    WriteCommand($D3);          {включаем звук}
    t:=256 - 1000000 div friq;
    WriteCommand($40);      {задаем частоту дискретизации}
    WriteCommand(t);
    Port[$21] := Port[$21] and not (1 shl BlasterIRQ); 
							{разрешаем прерывание}
    Port[$A]:=BlasterDMAl + 4;     {маскируем DMA }
    Port[$C]:=0;                   {сбрасываем триггер}
    Port[$B]:=BlasterDMAl + $48;   {задаем режим передачи }
    Port[$2] := lo(DMAOfs);        {задаем адрес буфера}
    Port[$2] := hi(DMAOfs);
    Port[dmap[BlasterDMAl]] := DMAPage; {задаем 64k страницу}
    Port[BlasterDMAl*2 + 1]:=lo(lenfil-1); {длина звуковой}
							{последовательности}
    Port[BlasterDMAl*2 + 1]:=hi(lenfil-1);
    Port[$A]:=BlasterDMAl;         {размаскируем DMA }
    WriteCommand($14);      {начинаем воспроизведение звука}
    WriteCommand(lo(lenfil-1));
    WriteCommand(hi(lenfil-1));
    longi:=clock;    {на всякий случай ограничим по времени}
    while (SouBlas = false) and (longi+50 > clock) do ;
    if not SouBlas then Port[$20]:=$20;   {сбрасываем DMA }
    i:=Port[BlasterPort+$0E];   {сбрасываем Sound Blaster}
    WriteCommand($D1);     {выключаем звук}
    Port[$21] := Port[$21] or (1 shl BlasterIRQ); 
							{запрещаем прерывание}
    FreeSBMem;         {возвращаем память в систему}
End.

Листинг 2. Процедуры выделения и освобождения 
нижней памяти для звукового буфера

var m1,m2,m3 : word; { флаги процедур выделения памяти: }
{ 4 - успешно, 5 - ошибка, 2 - не выделялась }

procedure GetSBMem; {выделение буфера нижней памяти}
					 {для SB/DMA}
var j:word;
begin
   j := (longint(maxlen) + 15) div 16; {длина блока}
							  {в параграфах}
   m1 := 2;   m2 := 2;   m3 := 2;
   asm
      mov ax,$0100
      mov bx,j
      int $31      {запрашиваем память}
      rcl m1,1     {запоминаем CF}

      mov bx,$1000
      mov cx,ax
      and cx,$fff
      sub bx,bc      {вычисляем размер до конца сегмента}
      cmp j,bx
      jb @l1       {если достаточно места, уходим}

      push bx
      mov ax,$0101
      int $31      {возвращаем память}

      pop bx
      mov ax,$0100
      int $31      {забираем память до конца 64k-сегмента}
      rcl m2,1     {запоминаем CF}
      mov word ptr [arr1+2],dx  {сохраняем селектор}

      mov ax,$0100
      mov bx,j
      int $31    {запрашиваем память для звукового буфера}
      rcl m3,1   {запоминаем CF}
      mov bx,ax
      mov ax,0
      adc ax,ax
      mov m3,ax   {запоминаем CF}
 @l1:
      mov word ptr [sndp+2],dx    {сохраняем селектор}
      xor dx,dx
      mov word ptr [sndp],dx      {сохраняем смещение}
      mov ax,bx
      shl ax,4
      mov DMAOfs,ax
      shr bx,12
      mov DMAPage,bl
   end;
   if((m1 and 1) or (m2 and 1) or (m3 and 1)) <> 0 then halt;
end;

procedure FreeSBMem; {возвращение нижней памяти в систему}
begin
   asm
      mov ax,$0101
      mov dx,word ptr[sndp+2]
      int $31
   end;
   if m2 = 4 then
   asm
      mov ax,$0101
      mov dx,word ptr[arr1+2]
      int $31
   end;
end;


Андрианов Сергей Андреевич - к.т.н., e-mail: andriano@divo.ru, Fido: 2:50/435.40