В обе стороныС. А. АндриановО программировании звуковых плат Sound Blaster 16 в режиме full duplex.Первые вычислительные машины были совершенно непохожи на нынешние. Обычно они оснащались лишь набором тумблеров и рядами лампочек и не имели ни клавиатуры, ни дисплея, не говоря уж об аудиоустройствах. Однако уже тогда программисты пытались заставить этих монстров издавать различные звуки, причем иногда даже воспроизводить какое-нибудь музыкальное произведение. Например, они добились того, что магнитные сердечники, используемые в качестве запоминающих устройств, исполняли полонез Огинского. Но естественно, ЭВМ постоянно совершенствовались, и потому к моменту появления ПК клавиатура и дисплей воспринимались всеми как вполне стандартные устройства ввода-вывода. Не был забыт и звук. В дисплейный блок или клавиатуру, подключаемую к главной ЭВМ, как правило, встраивался маленький динамик, издававший всякие гудки, щелчки, а порой и что-то более сложное. Терминалом большой ЭВМ и руководствовались при создании ПК. С самого начала IBM PC не повезло со звуком. К сожалению, фирма-разработчик решила, что ее детище будет предназначено исключительно для делового применения. Поэтому при достаточно высокой производительности (16-разрядный процессор) и широких графических возможностях (цветной графический дисплей) этот ПК не только не превосходил, но зачастую и уступал по звучанию своим 8-разрядным собратьям. Его одноразрядный звук использовался в основном лишь для сигнализации о неисправностях аппаратуры или об ошибках оператора. Однако IBM PC имел открытую архитектуру, и как только звук понадобился (сначала для игр), сразу была создана отдельная аудиоплата, вставляемая в разъем расширения. Такие устройства (Game Blaster фирмы Adlib) умели синтезировать несложный музыкальный звук, но с появлением Sound Blaster стало возможным записывать на IBM-совместимом компьютере и воспроизводить монофонический звук, хотя лишь 8-разрядный. Позднее появились платы, обеспечивающие стереофоническое звучание, а затем и использующиеся для оцифровки 16 разрядов. Правда, если первые звуковые платы старались сделать совместимыми сначала с Creative Sound Blaster, а затем с Creative Sound Blaster Pro (8-разрядные моно- и стереоплаты соответственно), то после перехода на 16-разрядный звук каждый разработчик пошел своим путем. Что-то похожее происходило позже и с видеоадаптерами. Пока законодателем мод в области ПК считалась IBM, существовали и стандарты де-факто на видеоадаптеры: сначала был CGA, потом EGA, на смену которому пришел VGA... Однако когда IBM уступила лидерство, на рынке ПК появилась масса видеоплат, объединяемых общим названием SuperVGA (SVGA), но совершенно несовместимых друг с другом. Обратимся снова к аудиоплатам. К тому времени как наметился переход с 8- на 16-разрядный звук, фирма Creative Labs, пионер в области разработки аудиоплат, перестала играть на рынке доминирующую роль. Да и старания производителей защититься от конкурентов путем патентования всех новшеств явно не способствовали формированию нового стандарта де-факто. Но если в области видеоадаптеров благодаря усилиям ассоциации VESA удалось навести хоть какой-то порядок, то с аудиоплатами подобная идея была обречена на провал. Звуковые платы, в отличие от видеоадаптеров, не имеют ПЗУ с драйверами, позволяющими нивелировать особенности аппаратуры и создавать более или менее унифицированный интерфейс с прикладными программами. Да и сама поддержка звука на уровне BIOS не была предусмотрена конструкторами IBM. Все это привело к тому, что до сих пор не появился стандарт на 16-разрядный звук, и послужило, пожалуй, одной из главных причин отказа от DOS в качестве основной платформы для компьютерных игр и перехода на Windows+DirectX. Однако если в офисе и дома Windows практически вытеснила другие ОС, в некоторых областях профессиональной сферы, например там, где нужны работающие в реальном времени программы, DOS еще не сдала своих позиций. Тем более что отдельные возможности, имеющиеся в DOS, попросту нереализуемы в Windows.
Когда фирма Creative Labs разрабатывала свою первую 16-разрядную плату Sound Blaster 16, она, видимо, и не предполагала, что эту аудиоплату можно будет использовать одновременно и для записи, и для воспроизведения звука (работа в так называемом режиме full duplex). Наверное, именно поэтому данный режим не поддерживается и в драйверах для Windows. Однако его можно запрограммировать в DOS. Правда, передача в таком случае получается несколько несимметричной: в одном направлении звук будет 16-, а в другом — 8-разрядным. Впрочем, вряд ли это серьезно ограничивает реальные применения подобной технологии. При использовании ПК для общения «как по телефону» 8-разрядного звука вполне достаточно, а при записи и сведении фонограммы одновременно с 16-разрядным режимом записи можно обойтись контрольным прослушиванием всего в 8 разрядов. Когда же ПК играет роль измерительного прибора, а звуковая плата — дешевых ЦАП/АЦП, разрядности получаемого сигнала обычно бывает достаточно, по крайней мере в одну сторону. В противном случае следует заменить звуковую плату специализированным прибором. Правда, такая несимметричность затрудняет использование ПК в качестве гитарного процессора, ревербератора или эквалайзера, но вообще-то для концертной деятельности компьютер не слишком подходит. * * *Программа, иллюстрирующая, каким образом можно применять звуковую плату Creative Labs Sound Blaster 16 или совместимые с ней, например AWE32, приведена в листинге 1. Она реализует простейшее эхо: записанный через микрофон звук спустя некоторое время воспроизводится через громкоговорители, подключенные к выходу аудиоплаты. Листинг 1. Простейшее цифровое эхоprogram echo; uses dsp_dma,getsbinf; {Ввод звука - 16 бит со знаком, вывод - 8 бит со знаком.} const BufSize = 2*1024; { размер буфера DMA } TimeConst = 156; { 156 - примерно 10 кГц } HalfBufToFill : integer = 0; { которая половина буфера DMA свободна } BothBuf : byte = 0; { индикатор заполнения обоих буферов } type RecBufType = array[0..BufSize-1]of integer; { для буфера DMA записи } PlayBufType = array[0..BufSize-1]of shortint; { для буфера DMA воспроизведения } var RecBuf : ^RecBufType; { буфер DMA для записи} PlayBuf : ^PlayBufType;{буфер DMA для воспроизведения} inpage, outpage : word; {страницы для буферов DMA} inoffset, outoffset : word; {смещения для буферов DMA} {$F+} procedure SBint;interrupt; {обработчик прерывания от звуковой платы} var intstat : integer; i : integer; begin Port[base + $04] := $82; {проверяем, по какому каналу пришло прерывание} intstat := Port[base + $05] and 3; BothBuf := BothBuf or intstat; if (intstat and 2 <> 0) then begin {16-битовый канал} i := Port[base + $0F]; end; if (intstat and 1 <> 0) then begin {8-битовый канал} i := Port[base + $0E]; end; if BothBuf = 3 then begin {если прошли прерывания от обоих каналов} for i := 0 to BufSize div 2 - 1 do PlayBuf^[HalfBufToFill*BufSize div 2 + i] := hi(RecBuf^[HalfBufToFill*BufSize div 2 + i]); write(HalfBufToFill,#8); {выводим на экран номер половинки буфера} HalfBufToFill := HalfBufToFill xor 1; BothBuf := 0; end; if (irq > 8) then {для IRQ 10, посылаем сигнал EOI во второй контроллер} Port[$A0] := $20; Port[$20] := $20; { посылаем EOI в первый контроллер} end; {$F-} var SkipLength : longint; {размер памяти до границы 64-Кбайт страницы} SkipBlock : pointer; begin writeln(‘ Эхо - Sound Blaster 16 в ‘, ‘режиме full duplex’); writeln(‘ для завершения работы ‘, ‘нажмите Enter’); GetBlasterInfo; {определяем характеристики карты} if (cardtype <> 6) then begin {Проверка, что на плате возможен full duplex} writeln(cardtype); writeln( ‘Для работы программы необходим Sound Blaster 16.’); halt; end; if (dma8 = dma16) then begin writeln(‘Ошибка: совпадение 8-битового и ‘, ‘16-битового каналов DMA.’); halt; end; SetMixer; {сброс DMAC и установки микшера} getmem(SkipBlock,16); {проверка, чтобы буферы не пересекали границу 64К} SkipLength := $10000 - (seg(SkipBlock^) shl 4) - ofs(SkipBlock^); freemem(SkipBlock,16); if SkipLength > 3*BufSize then getmem(SkipBlock,SkipLength); getmem(RecBuf,2*BufSize); {выделение памяти для буфера записи} inpage := ((longint(seg(RecBuf^)) * 16) + ofs(RecBuf^)) div $10000; inoffset := ((longint(seg(RecBuf^)) * 16) + ofs(RecBuf^)) and $FFFF; getmem(PlayBuf,BufSize); {выделение памяти для буфера воспроизведения} outpage := ((longint(seg(PlayBuf^)) * 16) + ofs(PlayBuf^)) div $10000; outoffset := ((longint(seg(PlayBuf^)) * 16) + ofs(PlayBuf^)) and $FFFF; fillchar(PlayBuf^,BufSize,0); {очистка буфера воспроизведения} EnableInterrupt( @SBint); SetupDMA(dma16,inpage,inoffset,BufSize, $54); {DMA на ввод} SetupDSP($BE,$10,BufSize div 2,TimeConst); {16 бит со знаком FIFO моно} SetupDMA(dma8,outpage,outoffset,BufSize, $58); {DMA на вывод} SetupDSP($C6,$10,BufSize div 2,TimeConst); {8 бит со знаком FIFO моно} readln; dspout($D5); {приостанавливаем 16-битовый ввод-вывод} dspout($D0); {приостанавливаем 8-битовый ввод-вывод} DisableInterrupt; freemem(PlayBuf,BufSize); freemem(RecBuf,2*BufSize); if SkipLength < 3*BufSize then freemem(SkipBlock,SkipLength); end. Сначала необходимо убедиться, что звуковая плата способна работать в режиме full duplex. Проще (и безопаснее) всего это сделать с помощью переменной окружения ‘BLASTER’. Подобным способом следует определить и базовый адрес порта ввода-вывода, а также номера используемых IRQ и канала DMA. Программа, выполняющая разбор переменной окружения, приведена в листинге 2. Плата должна быть 6-го типа, а номера 8- и 16-разрядного каналов DMA — различаться. Листинг 2. Извлечение данных из переменной окруженияunit GetSBInf; interface var base :integer; { базовый адрес ввода-вывода} irq :integer; { номер IRQ } dma8 :integer; { 8-битный канал DMA } dma16 :integer; { 16-битный канал DMA } midi :integer; { порт MIDI } cardtype :integer; { номер типа платы } procedure GetBlasterInfo; {извлечение информации о плате} implementation uses dos; var s : string; {переменная окружения ‘BLASTER’} e : byte; {позиция в этой строке} function str2hex:word; {преобразует последовательность hex-цифр в число} var val : word; begin val := 0; inc(e); while (s[e] <> ‘ ‘) and (s[e] <> char(0)) and (e <= length(s)) do begin case UpCase(s[e]) of ‘0’..’9’ : val := val * 16 + (byte(s[e]) - byte(‘0’)); ‘A’..’F’ : val := val * 16 + (byte(s[e]) - byte(‘A’) + 10); else begin writeln( ‘Ошибка в цифровых параметрах переменной окружения’); halt; end; end; inc(e); end; str2hex := val; end; procedure GetBlasterInfo; {информация о плате} begin s := getenv(‘BLASTER’); e := 1; if (length(s)>0) then begin while (e < length(s)) do begin case UpCase(s[e]) of ‘A’:base := str2hex; ‘I’:irq := str2hex; ‘D’:dma8 := str2hex; ‘H’:dma16 := str2hex; ‘P’:midi := str2hex; ‘T’:cardtype := str2hex; end; {case} inc(e); end; {while} end else begin writeln( ‘Отсутствует переменная окружения BLASTER’); halt; end; {if} end; end. Затем следует проинициализировать DSP (Digital Signal Processor — цифровой процессор сигналов) и установить режим микшера, для управления которым имеются два адреса портов: базовый+4 (для задания номера регистра) и базовый+5 (для записи/чтения нужной величины). Назначение регистров микшера приведено в табл. 1. Несколько пояснений к табл. 1. Регистры до 2Еh включительно служат для совместимости с предыдущими моделями Sound Blaster, однако, поскольку глубина регулировки уровня в последних моделях возросла, необходимо ввести новые регистры. Старые дублируют старшие биты новых регистров того же назначения. Шаг регулировки громкости у старых регистров — 4 дБ, а у новых — 2 дБ. Появление регистра 3Сh позволяет отключить источники сигнала без изменения положения регуляторов уровня, а добавление регистров 3Dh—3Eh — подключать входные сигналы в любом порядке. Например, можно подсоединить правый канал CD к левому звуковой платы, а правый канал линейного входа смешать с микрофоном и снова послать в правый канал. Кроме того, появились входные и выходные аттенюаторы с шагом 6 дБ и регуляторы тембра с шагом 2 дБ, а также стала возможной автоматическая регулировка уровня микрофонного входа. В случае монофонического сигнала все регулировки осуществляются по левому каналу.
После сброса DSP и установки режима работы микшера следует создать в оперативной памяти два буфера: для записываемого звука и для воспроизводимого. Поскольку и запись и воспроизведение будут осуществляться через DMAC (Direct Memory Access Controller — контроллер прямого доступа к памяти), к расположению буферов предъявляются некоторые дополнительные требования. Во-первых, они должны находиться в нижнем мегабайте адресного пространства. В реальном режиме работы процессора это выполняется всегда, а о том, как сделать такое в защищенном, рассказано в статье «Программирование Sound Blaster в защищенном режиме процессора» (см. «Мир ПК», № 3/98, с. 48). Во-вторых, буфер не должен пересекать границы 64-Кбайт страниц, поэтому при выделении памяти под него сначала следует проверить, хватит ли места для размещения буферов записи и воспроизведения до конца текущей страницы. Если его окажется недостаточно, то нужно запросить всю память до конца данной страницы, чтобы начало свободной памяти (кучи) совпало с началом следующей, где будут размещены буферы. Для каждого из буферов определяются номер 64-Кбайт страницы и смещение в ней, которые надо затем сообщить контроллеру прямого доступа к памяти (DMAC). Процедуры работы с DMAC и цифровым сигнальным процессором (DSP) приведены в листинге 3. При инициализации режим работы контроллера необходимо записать в регистр 0Bh для 8-разрядного режима или в регистр D6h — для 16-разрядного. Значения отдельных битов этих регистров приведены в табл. 2. Запись и воспроизведение звука — процессы непрерывные и требующие одновременной работы как пары DMAC—звуковая плата, так и процессора для подготовки данных или их использования. Поэтому возникает вопрос, каким образом организовать работу, чтобы процессор и DMAC не мешали друг другу, используя одну и ту же область памяти. Выход был найден. Звуковой буфер стали делить на две части, причем в DMAC передается полная длина буфера, а в DSP звуковой платы — только половина ее. Тогда аппаратные прерывания будут генерироваться в начале и в середине периода воспроизведения всего буфера. А в случае, когда DMAC работает с первой половиной буфера, процессор может обрабатывать вторую, и наоборот. Листинг 3. Работа с DSP и DMAunit DSP_DMA; interface procedure DspOut(val:byte); {выводит байт на DSP} procedure SetupDMA(dmach,page,ofs,DMAcount,dmacmd:word); {установка режима DMA} procedure SetupDSP(dspcmd, mode, DSPcount, tc:word); {установка режима DSP} procedure SetMixer; {сброс платы и выбор источника сигнала} procedure EnableInterrupt(newvect:pointer); {установка векторов прерываний} procedure DisableInterrupt; {восстановление векторов прерываний} implementation uses getsbinf,crt,dos; var intvecsave :pointer; { старый вектор прерывания} intrnum :integer; { номер прерывания } intrmask :integer; { маска прерывания } { структура, содержащая данные контроллера DMA } type DmaPortRec = record addr,count,page : byte; end; const DmaPorts : array[0..7]of DmaPortRec = ( (addr:$00; count:$01; page:$87), {0} (addr:$02; count:$03; page:$83), {1} (addr:$04; count:$05; page:$81), {2 не используется} (addr:$06; count:$07; page:$82), {3} (addr:$00; count:$00; page:$00), {4 не используется} (addr:$C4; count:$C6; page:$8B), {5} (addr:$C8; count:$CA; page:$89), {6} (addr:$CC; count:$CE; page:$8A)); {7} procedure DspOut(val:byte);{выводит байт в DSP} begin while (Port[base + $0C] and $80) <> 0 do; Port[base + $0C] := val; end; function DspIn:byte;{читает байт из DSP} begin while (Port[base + $0E] and $80) = 0 do; dspin := Port[base + $0A]; end; procedure SetupDMA(dmach,page,ofs,DMAcount,dmacmd:word); { Программирует контроллер DMA} { для 8- или 16-разрядного канала} var mask,mode,ff : byte; begin if (dmach < 4) then begin mask := $0A; mode := $0B; ff := $0C; end else begin mask := $D4; mode := $D6; ff := $D8; ofs := (ofs shr 1) + ((page and 1) shl 15); end; Port[mask] := 4 or dmach; { маскируем DMA} Port[FF] := 0; { сбрасываем триггер-защелку} Port[mode] := dmacmd or (dmach and 3); { уст.режима DMA} Port[dmaports[dmach].addr] := lo(ofs); { младший байт адреса} Port[dmaports[dmach].addr] := hi(ofs);{ старший байт} Port[dmaports[dmach].page] := page; { номер страницы} Port[dmaports[dmach].count] := lo(DMAcount-1); { младший байт счетчика} Port[dmaports[dmach].count] := hi(DMAcount-1); { старший байт} Port[mask] := (dmach and 3); { сброс бита маски} end; procedure SetupDSP(dspcmd, mode, DSPcount, tc:word); { Программирует DSP звуковой платы} begin DspOut($40); {установка константы времени} DspOut(tc); DspOut(dspcmd); {команда Bx/Cx} DspOut(mode); DspOut(lo(DSPcount-1)); DspOut(hi(DSPcount-1)); end; procedure SetMixer;{сброс платы и выбор источника сигнала} var val:byte; begin Port[base + $06] := 1; {сброс DSP} delay(1); Port[base + $06] := 0; if (dspin <> $AA) then {проверка готовности} writeln(‘Sound Blaster не готов.’); Port[base + $04] := $3D; Port[base + $05] := 1; { левый канал:источник сигнала — микрофон} { Port[base + $04] := $3E; {для моно — не обязательно} { Port[base + $05] := 1; } { правый канал:источник сигнала — микрофон} Port[base + $04] := $3C; Port[base + $05] := 0; { на выходе отключаем все, что можно} end; procedure EnableInterrupt(newvect:pointer); {установка векторов прерываний} var intrmask1:word; begin if (irq < 8) then {вычисляем номера прерывания} intrnum := irq + 8 { для IRQ 0-7 прерывания 8-15.} else intrnum := irq - 8 + $70; { для IRQ 8-15 прерывания 70H-78H.} intrmask := 1 shl irq; {маска} GetIntVec(intrnum,intvecsave); { сохраняем старый вектор} SetIntVec(intrnum, newvect); { устанавливаем новый вектор} intrmask1 := intrmask; {разрешаем прерывания} Port[$21] := Port[$21] and not intrmask1; intrmask1 := intrmask1 shr 8; Port[$A1] := Port[$A1] and not intrmask1; end; procedure DisableInterrupt; {восстановление векторов прерываний} var intrmask1:word; begin intrmask1 := intrmask; {запрещаем прерывания} Port[$21] := Port[$21] or intrmask1; intrmask1 := intrmask1 shr 8; Port[$A1] := Port[$A1] or intrmask1; SetIntVec(intrnum,intvecsave);{восстанавливаем вектор} end; end. После программирования DMAC то же самое проделывается и с DSP звуковой платы. Сначала надо установить частоту дискретизации, сообщив ему константу времени t = 256 — 1 000 000 / f, Затем следует задать команду на запись/воспроизведение звука. Для Sound Blaster 16 проще всего выбрать команды Bx/Cx, состоящие из четырех байтов: Command, Mode, LenLo, LenHi. Формат первого байта Command приведен в табл. 3, а второго байта Mode — в табл. 4. Байты LenLo и LenHi — младший и старший в соответствии с длиной передаваемого блока, уменьшенной на единицу. Команды Bx/Cx позволяют задавать как знаковый, так и беззнаковый вид представления отсчетов. При знаковом отсчет представляет собой целое число со знаком, принимающее значение 0 при отсутствии входного сигнала, при беззнаковом — целое число без знака, равное 80h для 8-разрядного режима и 8000h для 16-разрядного при отсутствии входного сигнала. Стандартом де-факто является представление 8-разрядных отсчетов в беззнаковой форме, а 16-разрядных — в знаковой, однако для упрощения процедуры преобразования в приводимой программе обе величины выбраны знаковыми.
После программирования микшера следует установить свои процедуры обработки прерываний от звуковой платы и только потом можно будет задавать режимы DMA и DSP. Затем процессор свободен для выполнения любой другой работы, например с экраном, как это практикуется в компьютерных играх. В данной же программе просто происходит ожидание ввода с клавиатуры. Однако время от времени работу процессора будут приостанавливать прерывания, поступающие со звуковой платы по окончании пересылки очередной порции данных. В задачу обработчика прерываний входит определение номера канала, по которому пришло прерывание. Дело в том, что и 8-разрядный, и 16-разрядный ввод-вывод, и даже внешний MIDI-интерфейс (MPU-401) генерируют одно и то же аппаратное прерывание. Для того чтобы различать их между собой, в адресном пространстве регистров микшера имеется порт номер 82h (регистр статуса прерываний), определяющий источник прерывания (табл. 5). Обработчик прерывания должен сообщить звуковой плате, что ее прерывание принято и обработано, для чего необходимо осуществить чтение из порта 0Eh или 0Fh для 8- либо 16-разрядного режимов соответственно. После прихода прерываний от канала записи и от канала воспроизведения можно считать, что соответствующие половины буферов записи и воспроизведения уже обработаны звуковой платой и пора копировать данные из одного буфера в другой. Так как в обоих случаях была выбрана одинаковая (знаковая) форма представления данных, то их преобразование сводится лишь к переписыванию старших байтов значений двухбайтовых звуковых отсчетов из входного буфера в выходной. По завершении отработки прерывания следует осведомить об этом контроллер прерываний (с учетом каскадирования). После нажатия на Выше приведен выборочный список команд DSP, которые применяются при записи и воспроизведении звука (табл. 6). Здесь не рассматриваются непосредственный ввод-вывод, не использующий DMAC, ввод-вывод с компрессией и MIDI-команды. ОБ АВТОРЕАндрианов Сергей Андреевич — канд. техн. наук; e-mail: pcworld@pcworld.ru или fidonet: 2:5017/11.40. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||