int21h

Realizace: Pascal a Sound Blaster

Prošli jste si teorií, naučili se příkazy a procedury pro základní obsluhu zařízení. Nyní se tedy definitivně dostáváme k tomu, na co jste asi dlouho čekali. Rozezvučíme naši zvukovou kartu. Naučíme se nejen zvuky přehrávat, ale i nahrávat a částečně se také podíváme na MIDI hudbu. Vřele doporučuji si však sehnat o zvukových kartách co nejvíce informací z dokumentace na Internetu. A ještě bych se chtěl omluvit za jednu chybu v minulém díle (ti pozornější si jí možná už všimli): pokud chcete získat z 16bitového DMA z rosahu 4-7 údaj 0-3, musíte samozřejmě provést DMA16 AND 3 a nikoliv OR 3 (jedná se o MOD 4) !!!.

Aktivace zvukové karty

Budeme využívat funkce a proměnné, které jsme si vytvořili v předchozím díle. Také nebudu moc popisovat příklady, protože předpokládám, že už víte, která konstanta co znamená a jaká data za ní následují. Obsluha zvukové karty se defakto skládá ze 3 částí: její spuštění, obsluha (přes přerušení) a ukončení (stačí obyčejná pauza). Pokud implementujte přehrávání delších zvuků než je velikost mixovacího bufferu, bude přídavné mixování (první část do 32 kB namixuje přímo nějaká Vaše procedura PlayWave) provádět obsluha rutiny přerušení, která vždy po každém přerušení projede tabulku aktuálně hraných zvuků a domixuje podle ukazatelů dalších 8 nebo 16 kB (podle toho, jak máte buffer rozdělený). Předpokládám, že již máte vytvořený buffer pro mix a zjištěné parametry karty.

Obsluha přehrávání

Na naší kartě zkusíme nejprve tu nejhorší kvalitu, 11025 Hz (11 kHz) 8 bitů a mono. To proto, že dané soubory WAV jsou malé, obsluha tohoto režimu vyžaduje menší výkon procesoru a měl by fungovat na všech zvukových kartách. Než přinutíme zvukovou kartu hrát, musíme si vytvořit rutinu, která ji pak bude obsluhovat.

Pamatujte, že pokud použijeme DMA, což je náš cíl, zvuková karta vždy v době, kdy přehraje určitý počet dat, vyvolá přerušení o tom, že už nemá co hrát (podle ní) a že s tím tedy musíme něco udělat. Ona prakticky mít co hrát bude (pokud jí sdělíte počet dat poloviční až čtvrtinový než je skutečný, a pokud je navíc v módu AutoInit, tak bude pokračovat zase v dalším bloku - a DMA zajistí opakování od začátku na konci celého bufferu), ale Vám se to určitě na poslech líbit nebude (efekt přeskakující gramofonové desky). My si zvolíme, že buffer rozdělíme na 4 části po 8 kB. Zvuková karta začne po inicializaci hrát automaticky od začátku (protože do počitadla DMA pošleme 0, viz. dále). Náš úkol bude jednoduchý: vždy, když karta přehraje nějakých 8 kB, nahradíme je jinými. V našem příkladu pro jednoduchost vždy pouze přehraná data vymažeme tichem (pro 8bitové vzorky). Důležité jsou ovšem dvě věci, které nutně musíme provést: musíme sdělit řadiči přerušení, že jsme přerušení obsloužili. A totéž musíme sdělit zvukové kartě, aby mohla později vyvolat další. Obsluhu si uděláme pro zjednodušení v Pascalu (proměnná AKTCAST se musí vynulovat při každém startu zvukové karty se změnou DMA počitadla - myslím tím nový start, pokud jen obnovíte provoz po pauze, tak tam to nenulujte - leda byste měnili obsah bufferu):

var AktCast : byte;	(* na začátku programu nastavit na 0 !!! *)


procedure NovaObsluha; interrupt;
var Prac : byte;
begin
{informujeme o zpracování 8bitového přerušení}
 Prac := Port[Adr+$e];
{informujeme řadič přerušení o zpracování 8bitového přerušení}
 Port[$20] := [$20];
{informujeme taktéž o případném 16bitovém přerušení}
 if IRQ in [2]+[8..15] then Port[$a0] := $20;
{vymažeme právě přehraný zvuk tichem a zvýšíme ukazatel na další}
 FillChar(Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13],8192);
 AktCast := (AktCast+1) and 3;		{pro 2 části by stačilo jen NOT nebo XOR 1}
end;  

A to je všechno. Co se od nás žádá. Pokud přehráváte více zvuků současně a jsou delší než 32 kB, je možné upravit řádek s FillChar na tento kód, který zjistí, zda hrají ještě nějaké zvuky a pokud ano (nebyly namixovány celé), domixuje jejich další část. Každý zvuk bude mít určitý popisovač, který bude udávat, zda zvuk hraje a zároveň dokdy (offset>0), a kde má uložena data (a jejich velikost - pro TP7 by sice stačil WORD, ale data můžete mít také v XMS - jejich přenos si jistě dokážete už zařídit - pro každý zvuk si rezervujte např. 8 kB a přenášejte tam data z XMS na pozadí):

const	Zvuku = 8;
var	Zvuk : array[0..Zvuku-1] of record
					Data : pointer;
					Offset : longint;
					Velikost : longint;
				    end;
	Prvni : boolean;


(* úprava rutiny pro obsluhu přerušení *)
var i,Vel : longint;
...
 Prvni := True;
 for Prac := 0 to Zvuku-1 do
  if Zvuk[Prac].Offset > 0 then
  with Zvuk[Prac] do
  begin					{zvuk nebyl namixován celý}
   if Velikost-Offset > 8192 then	{kolik bytů se bude mixovat}
    Vel := 8192 else Vel := Velikost-Offset;
   Inc(Offset,Vel);			{příště budeme hrát další část}
   if Prvni then			{jedná se o první zvuk}
   begin
    Prvni := False;			{zvuk rychle přeneseme do bufferu}
    Move(Mem[Seg(Data^):Ofs(Data^)+Offset],
         Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13],Vel);
   end else
    for i := 0 to Vel-1 do		{zvuk musíme namixovat ke stávajícím datům}
     Inc(Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13+i],
         Mem[Seg(Data^):Ofs(Data^)+Offset+i]);
					{pokud je už zvuk celý, příště ho hrát nebudeme}
   if Offset = Velikost then Offset := 0;
  end;
...  

Teď budeme potřebovat proceduru, která přehraje nějaký zvuk. V ní použijeme počitadlo DMA. Je totiž vhodné mixovat zvuk těsně za ukazatel dat, která se právě přehrávájí. Důvod je jasný: zvuk se přehraje okamžitě (v opačném případě hrozí až 2.97 vteřinové spoždění v naší 11 kHz kvalitě). Jistě Vás možná napadlo, že pokud přečteme aktuální pozici a pak na ní začneme mixovat, že nám bude část zvuku chybět, protože se mezitím ukazatel přesunul jinam. Ano, to platí, zvláště u kvality CD, kde je rychlost přenosu dat opravdu velká. U 11 kHz to není tak strašné, ale pokud chcete, můžete si k výpočtu ukazetele přidat např. 128 bytů. Tím se zvuk přehraje o něco později (0.01s), ale zaručeně celý (a nemělo by hrozit, aby se stihlo přimixovat i posledních 128 bytů dříve, než se přehraje ta rezerva, tedy aby se konec přehrál dříve než začátek). Zároveň musíme zajistit, aby mixovaný zvuk skončil na hranici 8 kB té části bufferu, která se zrovna přehrává (prakticky na konci té minulé), protože při dalším přerušení (pokud je zvuk delší než 32kB-zbytek) se totiž bude mixovat od jejího začátku. To provedeme tak, že pozici, na které se zrovna hraje, vydělíme MOD 8192, tedy např. pokud jsme na pozici 10.000 bytů, což je druhý bank, dostaneme 1808, tedy jsme na bytu 1808 v banku 2. Musíme tedy namixovat 32768-1808 bytů, abychom skončili na konci první banky (jakmile se totiž vyvolá přerušení, začne se přepisovat od začátku druhé banky).

function PrehrajZvuk(Cislo : byte) : boolean;
var i, Ukazatel : word;
    Vel : longint;
begin
{nebudeme hrát, pokud je špatné číslo nebo už daný zvuk hraje}
{druhá chyba se dá vyřešit duplikováním odkazu na zvuk}
 PrehrajZvuk := False;
 if Cislo > Zvuku-1 then Exit;
 if Zvuk[Cislo].Offset > 0 then Exit;
{namixujeme zvuk do bufferu až do velikosti 32 kB}
 with Zvuk[Cislo] do
 begin
{přečteme pozici počitadla 8bitového DMA}
  Ukazatel := Port[OfsDMA[DMA8]];
  Ukazatel := Ukazatel + Port[OfsDMA[DMA8]] shl 8;	{je to 16bitový údaj}
{nyní jsme získali počet dat, která zbývají přehrát!!!}
  Ukazatel := 32768-Ukazatel;				{pozice v 32 kB bufferu}
{zjistíme, kolik bytů zvuku musíme namixovat a zda je delší než 32 kB}
  Vel := Velikost-Offset;
  if Vel > 32768-(Ukazatel and 8191) then
  begin
   Vel := 32768-(Ukazatel and 8191);
   Offset := Vel;
  end;
{nyní vybraný úsek dat přimixujeme}
  for i := 0 to Vel-1 do
  begin
   Inc(Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13+Ukazatel],
       Mem[Seg(Data^):Ofs(Data^)+i]);
{musíme držet ukazatel do mixovacího bufferu v rozmezí 0-32768 bytů}
   Inc(Ukazatel);
   Ukazatel := Ukazatel and $7fff;
  end;
 end;
 PrehrajZvuk := True;
end;  

Tak, teď už umíme přehrávat, ale naše karta zarytě mlčí. Budeme jí tedy muset nastartovat. Budeme se zatím držet té nejmenší možné kvality, pro kterou jsme si právě napsali procedury.

Spuštění přehrávání

Nejprve budeme potřebovat znát lineární adresu našeho bufferu pro DMA řadič. Z ní odvodíme stránku (64kB) a offset v ní. Výpočet nám stačí provést vždy jen jednou, a klidně ho můžete umístit i do procedury, která alokuje buffer, neboť tyto údaje počítá také:

var	LinearAdr : longint;
	Stranka,OfsStranky : word;


begin
{tento výpočet SEG*16+OFS by Vám měl být známý}
 LinearAdr := longint(Seg(SBBuffer^)) shl 4 + Ofs(SBBuffer^);
 Stranka := LinearAdr shr 16;
 OfsStranky := LinearAdr and $ffff;
end;  

Nyní můžeme přejít k programování řadičů a samotné karty (předpokládám, že obsluha přerušení je již na svém místě a daný INT s IRQ je povolený; kdyby to nebylo jasné, tak kromě toho, že obsadíte vektor, musíte ještě dané IRQ na jeho řadiči povolit, tzv. odmaskovat (každý bit znamená jeden kanál, "0" = zapnuto) - proceduru předkládám; pokud používáte nějaká nestandadní přerušení v úseku 8-15, tak si je prosím doplňte, ale myslím, že to je velmi nepravděpodobné):

procedure OdmaskujINT;
begin
 {povolí dané přerušení, "0" = off}
 case IRQ of
   2 : Port[$a1] := Port[$a1] and not 2;
  10 : Port[$a1] := Port[$a1] and not 4;
  11 : Port[$a1] := Port[$a1] and not 8;
  else Port[$21] := Port[$21] and not (1 shl IRQ);
 end;
 if IRQ in [2, 10, 11] then Port[$21] := Port[$21] and not 4;
end; {volejte hned po SetIntVec}


procedure PovolDMA;
begin
{zakážeme dané DMA během programování}
 Port[DMA8reg[0]] := 4 or DMA8;
{nastavíme pozici dat na 0 - offset}
 Port[DMA8reg[2]] := 0;
{nastavíme režim pro DMA - viz. skládání konstanty AA BB CC DD}
{autoinit čtení z paměti do DSP v blokovém módu 10}
 Port[DMA8reg[1]] := $58 or DMA8;
{zapíšeme stránku, ve které se nachází buffer}
 Port[DMAstr[DMA8]] := byte(Stranka);
{zapíšeme offset v této stránce}
 Port[DMAofs[DMA8]] := Lo(OfsStranky);
 Port[DMAofs[DMA8]] := Hi(OfsStranky);
{zapíšeme velikost dat pro přenos, tj. skutečnou velikost bufferu - 32 kB}
 Port[DMAvel[DMA8]] := $ff;	{lo}
 Port[DMAvel[DMA8]] := $7f;	{hi}
{povolíme daný kanál DMA}
 Port[DMA8reg[0]] := DMA8;
end;

procedure StartSoundBlaster; assembler;
asm
{zapneme reproduktor}
	push	$d1
	call	DSPwrite
{nastavíme frekvenci 11025 přes konstantu, mono je zapnuté standardně}
	push	$40
	call	DSPwrite
	push	165
	call	DSPwrite
{povolíme DMA pro daný kanál; řadič bude čekat na SB, až si od něj data vyžádá}
	call	povolDMA
{sdělíme kartě, po kolika bytech má vyvolávat přerušení, tj. u nás po 8 kB}
	push	$48
	call	DSPwrite
	push	$ff
	call	DSPwrite
	push	$1f
	call	DSPwrite
{zahájíme 8bitové DMA hraní s Autoinitem}
	push	$1c
	call	DSPwrite
end;  

Nyní by Vám měla zvuková karta běžet. Zda správně, to poznáte podle prvního příznaku, tj. že Vám do 10 vteřin nespadne program :-). Pokud ne, můžete zkusit namixovat nějaký WAVe a zaposlouchat se, zda bude hrát či nikoliv. Zvuky musí být ve stejné kvalitě, v jaké zvuková karta hraje. Pokud ne, musíte je převést, nebo tím vytvoříte zajímavé efekty (nejlepší je přehrávat stereo zvuk v mono režimu bez převodu).

Ukončení přehrávání

Ukončení činnosti karty je velice jednoduché. Pošlete kartě příkazy, aby zastavila přehrávání (pauza). Poté ji můžete např. znovu zase spustit, nebo, pokud již končíte s programem, musíte odinstalovat obsluhu přerušení (budete potřebovat SetIntVec a příslušné INT pro daný IRQ, viz. minulý díl). A tím to končí.

procedure ZamaskujINT;
begin
 {procedura zamaskuje dané přerušení, takže je řadič už nebude vytvářet: "1" = off}
 case IRQ of
   2 : Port[$a1] := Port[$a1] or 2;
  10 : Port[$a1] := Port[$a1] or 4;
  11 : Port[$a1] := Port[$a1] or 8;
  else Port[$21] := Port[$21] or (1 shl IRQ);
 end;
 if IRQ in [2,10,11] then Port[$21] := Port[$21] or 4;
end;


procedure ExitSB; assembler;
asm
	push	$d9		{překvapivě to bere i $da, takže pokud Vám to bude zlobit}
	call	DSPwrite	{dejte sem nejprve $d0 a pak $da}
	push	$d0
	call	DSPwrite
	call	odinstINT	(* Nějaká Vaše procedura s SetIntVec na původní rutinu *)
{Nyní musíme zakázat i vyvolávání tohoto přerušení na řadiči}
	call	ZamaskujINT
{ještě můžeme vypnout reproduktor}
	push	$d3
	call	DSPwrite
end;  

Nyní je už zvuková karta opravdu vypnutá. Pokud ji chcete jen pozastavit, použijte jeden z daných DSP příkazů pro pauzu ($d0 nebo $d9). Nyní si můžete opravdu vyzkoušet, zda Vám to hraje. Teprve, pokud to bude fungovat, čtěte dále tento text.

Využití lepší kvality

Dobrá, hraje? To doufám... Nyní se naučíme něco lepšího. Pokud Vaše karta umí alespoň 44 kHz, budou se dít věci. A uznejte sami, že stereo zní lépe než mono. Na tomto místě bych rád podotknul, že 44 kHz 8 bitů stereo zní lépe než 22 kHz 16 bitů stereo, takže pokud Vaše karta neumí 16bitový přenos (pouze SB16 a vyšší), nemusí Vám vadit, že nebudete mít 16bitovou kvalitu (pokud ovšem pustíte CD mechaniku a povolíte CDin výstup, bude hrát v kvalitě CD nezávisle na tom, co do ní ládujete Vy). A asi Vás to překvapí, ale dokud nepůjdete na 16bitové zvuky, tak nemusíte upravovat kromě initu zvukové karty nic! Opravdu, obslužná procedura přerušení i mixování zvuku zůstane naprosto stejné. No není to skvělé?

Dobrá, ale jak na tu lepší kvalitu. Možná Vás napadne, že to bude souviset s časovací konstantou. A máte pravdu. Jednoduše, pokud chcete 22 kHz, šoupněte to do vzorečku a místo 165 dostanete 211, pro 44 kHz dostanete 233. Klidně si to zkuste. Pouhou změnou konstanty se změní i rychlost dat, a tedy je tak budete muset dodávat. Dobře, ale stále jsme pouze v režimu mono. Jak docílit stereo? Musíte kartě sdělit, že budete využívat stereo bit v mixeru a ten pak přepnout do režimu stereo. Také musíte mít na paměti, že objem přenášených dat pak stoupne 2x, tedy musíte upravit i příslušné konstanty: pro 22 kHz ve stereu to bude 233 (protože to má stejný objem jako 44 kHz v mono), pro 11 kHz to bude 211, a pro 44 kHz už 245. Dobře, frekvence tedy známe, teď ještě jak naprogramovat to stereo.

Po vložení konstanty pošlete kartě příkaz $a8. A pak se podíváte do mixeru, a nastavíte stereo bit:

asm
{aktivace možnosti sterea - u některých karet to stačí}
	push	$a8
	call	DSPwrite
{přepnutí mixeru do módu stereo - u nových karet}
	mov	dx,adr
	add	dx,4
	mov	al,$e
	out	dx,al
	inc	dx
	in	al,dx
{	and	al,253}
	or	al,2		{bit 1}
	out	dx,al
{můžete využít již naprogramované funkce pro práci s mixerem}
end;  

Tak, zvučí? A krásně? To jsem rád. Pokud jste šťastnými majiteli originál karty Sound Blaster 16 (nebo něčeho, co ji dokáže alespoň emulovat - Sound Blaster 128, DOSbox, atd.), můžete si svůj program přepsat (či připsat) pro podporu 16bitových zvuků (ale rovnou Vám rovnou řeknu, že to Vaše karta na 75% až 90% podporovat nebude). Vezměte si před sebe všechny dosud napsané procedury a funkce v tomto dílu, a můžeme začít upravovat. Podotýkám, že všechny zvuky mají nyní velikost vzorku 2 byty a navíc ve formátu Integer se znaménkem. DMA řadič tedy bude očekávat počet wordů, nikoliv bytů.

Nejprve obsluha přerušení. Tu nyní musíme napsat tak, aby oznamovala 16bitové přerušení pro kartu a mixovala nikoliv byty, ale integery. Řádek s Port[Adr+$e] bude zřejmě potřeba změnit na tento (i když, nic se nestane, pokud tam budou oba - nejsem si totiž jistý, zda 16bitové přerušení na kartě vůbec souvisí s 16bitovým DMA, ale u všech příkladů na 8bitové hraní co mám, je pouze $e, ačkoli se počítá i s IRQ > 8, takže asi ano):

 Prac := Port[Adr+$f];

Dále mixovací část, ta s tím cyklem, bude vypadat takto (nic jiného se měnit nebude, pozor na to!):

   for i := 0 to Vel shr 1-1 do
     Inc(integer(MemW[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13+i shl 1]),
         integer(MemW[Seg(Data^):Ofs(Data^)+Offset+i shl 1]));  

Podobně změníme cykl i ve funkci PrehrajZvuk:

  if Ukazatel and 1 = 1 then Dec(Ukazatel);
  for i := 0 to Vel shr 1-1 do
  begin
   Inc(integer(MemW[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13+Ukazatel]),
       integer(MemW[Seg(Data^):Ofs(Data^)+i shl 1]));
   Inc(Ukazatel,2);
(* upravte případně také nastavení VEL a OFFSET,
   aby to bylo číslo MOD 2 = 0, raději pomocí DEC *)  

Tady může být zrada u čtení počitadla DMA. Ukazuje byty nebo wordy? Pokud totiž wordy, musíte UKAZATEL vynásobit *2 (ale nemusíte pak upravovat VEL a OFFSET, protože budou po x2 zaručeně sudé). Aktivace stereo módu zůstane stejná, jen se použije příkaz $a4 a nikoliv $a8. Výpočet adresy bufferu také (jen je nutné mít na paměti, že se do řadiče musí všechny offsety a velikosti posílat ve Wordech, tj. 2x menší). To v případě, že bychom chtěli jet nadále přes konstantu. Protože ale neznáme příkaz ke spuštění 16bitového DMA, musíme použít už příkazy, které nám nabízí karta Sound Blaster 16. Ta nenastavuje konstantu, ale nýbrž frekvenci. Proceduru StartSoundBlaster tedy přepíšeme celou (můžete ji samozřejmě použít i pro 8bitové hraní, stačí když konstantu $bx nahradíte na $cx):

procedure StartSoundBlaster; assembler;
asm
{povolení repro je pravděpodobně u SB16 zbytečné}
	push	$d1
	call	DSPwrite
{nastavíme frekvenci, zde 22 kHz, protože 22050 = $5622}
	push	$41
	call	DSPwrite
	push	$22
	call	DSPwrite
	push	$56
	call	DSPwrite
{pokud bude potřeba, nastavíme stereo pomocí nějaké procedury - pro starší karty}
	call	NastavStereo
	call	povolDMA
{a teď nastavíme a spustíme 16bitový režim}
	push	$bc	{Lo:1100b = autoinit hraní s FIFO}
	call	DSPwrite
	push	$30	{110000b = stereo a signed}
	call	DSPwrite
	push	$ff
	call	DSPwrite{8 kB ve Wordech, pokud to nebude fungovat}
	push	$0f	{dejte místo $0f údaj $1f}
	call	DSPwrite
end;  

Procedura pro nastavení DMA řadiče bude vypadat stejně, jen se změní odkazy do tabulek, resp. samotné tabulky (není nad předdefinované konstanty) a samozřejmě všechno, co bylo v bytech, bude nyní ve wordech:

procedure PovolDMA;
begin
 Port[DMA16reg[0]] := 4 or (DMA16 and 3);
 Port[DMA16reg[2]] := 0;
 Port[DMA16reg[1]] := $58 or (DMA16 and 3);
 Port[DMAstr[DMA16]] := byte(Stranka);	{offset je nutné převést na Wordy, tj. DIV 2}
 Port[DMAofs[DMA16]] := Lo(OfsStranky shr 1);
 Port[DMAofs[DMA16]] := Hi(OfsStranky shr 1);
 Port[DMAvel[DMA16]] := $ff;		{zde je počet WORDů - 1 !!!}
 Port[DMAvel[DMA16]] := $3f;
 Port[DMA16reg[0]] := DMA16 and 3;
end;  

Podobně jako spouštěcí proceduru bude potřeba upravit i ukončovací proceduru, neboť ta teď pracovala také s 8bitovými vzorky:

procedure ExitSB; assembler;
asm
{	push	$da	
	call	DSPwrite}
	push	$d5
	call	DSPwrite
	call	odinstINT
	call	ZamaskujINT
	push	$d3
	call	DSPwrite
end;  

A to by mělo být z přehrávání zvukové WAV části všechno. Pokud Vám hraje 16bitový zvuk, máte štěstí (ale myslete i na ty méně šťastné. Pokud ne, ani 8bitový zvuk není špatný a navíc zabírá na disku 2x méně místa. Podíváme se ještě na opačnou cestu, tj. nahrávání do souboru WAV, či RAW.

Záznam ze zvukové karty

Tuto funkci asi využije jen málo z Vás a ještě menšímu počtu bude fungovat. Pokud pomineme vliv mimozemských civilizací, může se stát cokoliv: máte zlumené vstupy nahrávání v mixeru, emulační ovladač pro Vaši kartu neumí nahrávání, atd. Přeci ale jen máte jakousi šanci, takže by byla škoda jí promarnit.

V nahrávacím módu karta a DMA fungují obráceně, tj. neposílají se data do karty, ale z karty. Opět se využívá buffer, do kterého karta cyklicky zapisuje. Dá se říci, že to je i jednodušší než přehrávání. Váš první úkol bude založit si na disku nějaký soubor WAV s hlavičkou, nebo RAW bez hlavičky, do kterého budeme data ukládat (pokud si je nechcete uchovávat celé v paměti). Pak bude následovat úprava rutiny pro obsluhu přerušení:

var ZvukSoubor : file;		(* Proveďte REWRITE(ZvukSoubor,1) *)
    Nahrano : longint;		(* před každým nahráváním vynulovat !!! *)


procedure NovaObsluha; interrupt;
var Prac : byte;
    SkutRW : integer;
begin		(* funguje pro 11-44 kHz, mono/stereo a 8/16 bitů *)
 Prac := Port[Adr+$e];
 Prac := Port[Adr+$f];
 Port[$20] := [$20];
 if IRQ in [2]+[8..15] then Port[$a0] := $20;
 BlockWrite(ZvukSoubor,Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+CBuffer shl 13],8192,SkutRW);
 AktCast := (AktCast+1) and 3;
 Inc(Nahrano,8192);
end;  

Vidíte, že se skoro vůbec nezměnila. Co se nezmění vůbec, je nastavení INTů, ať už na začátku, nebo na konci. Změní se startovací procedury pro DMA a SB, protože musíme rozběhnout přenos druhým směrem:

procedure PovolDMA8;
begin
 Port[DMA8reg[0]] := 4 or DMA8;
 Port[DMA8reg[2]] := 0;
 Port[DMA8reg[1]] := $54 or DMA8;
 Port[DMAstr[DMA8]] := byte(Stranka);
 Port[DMAofs[DMA8]] := Lo(OfsStranky);
 Port[DMAofs[DMA8]] := Hi(OfsStranky);
 Port[DMAvel[DMA8]] := $ff;
 Port[DMAvel[DMA8]] := $7f;
 Port[DMA8reg[0]] := DMA8;
end;


procedure PovolDMA16;
begin
 Port[DMA16reg[0]] := 4 or (DMA16 and 3);
 Port[DMA16reg[2]] := 0;
 Port[DMA16reg[1]] := $54 or (DMA16 and 3);
 Port[DMAstr[DMA16]] := byte(Stranka);
 Port[DMAofs[DMA16]] := Lo(OfsStranky shr 1);
 Port[DMAofs[DMA16]] := Hi(OfsStranky shr 1);
 Port[DMAvel[DMA16]] := $ff;
 Port[DMAvel[DMA16]] := $3f;
 Port[DMA16reg[0]] := DMA16 and 3;
end;  

Jak vidíte, změnila se vlastně jen jedna konstanta. A takto to bude podobné i u procedur pro rozběhnutí samotné karty (zde ukazuji zase 11 kHz 8 bitů mono (u SB16 to bude 16 bitů), ale změna kvality zase spočívá pouze ve změně konstanty, nastavení mixeru, popř. změny frekvence u SB16):

procedure StartSoundBlaster8; assembler;
asm
	push	$d3		{staré karty neuměly současně nahrávat a hrát}
	call	DSPwrite	{proto reproduktor zakážeme, dneska zbytečné}
	push	$40
	call	DSPwrite
	push	165
	call	DSPwrite
	call	povolDMA8
	call	PovolVstup
	push	$48
	call	DSPwrite
	push	$ff
	call	DSPwrite
	push	$1f
	call	DSPwrite
	push	$2c		{příkaz pro autoinit DMA 8bitový záznam}
	call	DSPwrite
end;


procedure StartSoundBlaster16; assembler;
asm
	push	$d3
	call	DSPwrite
	push	$41
	call	DSPwrite
	push	$11
	call	DSPwrite
	push	$2b
	call	DSPwrite
	call	povolDMA16
	call	PovolVstup
	push	$be
	call	DSPwrite
	push	$10
	call	DSPwrite
	push	$ff
	call	DSPwrite
	push	$0f
	call	DSPwrite
end;  

Něco, co se změní o trochu více, jsou ukončovací procedury:

procedure ExitSB8; assembler;
asm
	push	$d0
	call	DSPwrite
	call	odinstINT
	call	ZamaskujINT
	call	UlozZbytek
end;


procedure ExitSB16; assembler;
asm
	push	$d5
	call	DSPwrite
	call	odinstINT
	call	ZamaskujINT
	call	UlozZbytek
end;  

Asi se ptáte, k čemu že slouží procedura ULOZZBYTEK. No, prakticky by tady ani být nemusela. Pokud byste nechali zvukovou kartu pomocí příkazu $d9, resp. $da dojít až na konec bloku, počkali si na to (popř. by bylo vyvoláno přerušení, jenže to nemusí přijít), tak stačí uložit jen aktuální blok dat, ovšem pouze tehdy, pokud každý blok po jeho uložení na disk vyplníte poté v paměti i tichem. Na konci souboru pak bude pár bytů ticha. Pokud toto nechcete a nechcete ho i neustále vytvářet, musíte kartu zastavit hned. Poté ale nějak potřebujeme zjistit, kolik bytů už karta přenesla. Uděláme to podobně, jako když mixujeme poprvé zvuk (tady může být opět u 16bitových zvuků zrada v tom, že DMA řadič může vracet pozici ve wordech - to je nutné si vyzkoušet):

procedure UlozZbytek;
var i, Ukazatel : word;
begin
 Ukazatel := Port[OfsDMA[DMA8]];
 Ukazatel := Ukazatel + Port[OfsDMA[DMA8]] shl 8;
 Ukazatel := 32768-Ukazatel;
 BlockWrite(ZvukSoubor,Mem[Seg(SBBuffer^):Ofs(SBBuffer^)+Ukazatel and 8191,Ukazatel and 8191);
 Inc(Nahrano,Ukazatel and 8191);		{MOD 8192}
 {zde případně zapište hlavičku WAV s celkovým počtem dat}
 {do rezervovaného prostoru na začátku souboru}
 Close(ZvukSoubor);
end;  

Pokud Vás zajímá, proč tam provádíme ten AND 8191, tak je to proto, jelikož zapisujeme bloky po každých 8 kB, avšak DMA počitadlo se zvyšuje až do 32 kB, tedy vždy, když je větší nebo rovno 8192, tak obsahuje už i zapsaná data a ty musíme elimovat (proto vlastně od údaje UKAZATEL odečítáme 8192 tak dlouho, dokud není číslo menší než 8192).

Ještě jsme na něco zapomněli. Ve startovací funkci máme proceduru PovolVstup. Ta slouží k nastavení mixeru, aby daný vstup, ze kterého chceme nahrávat, nebyl ztlumený na 0 (jinak nahrajeme ticho). Procedura může vypadat nějak takto:

const	Mikrofon = 1;
	CD = 2;
	Line = 4;


procedure PovolVstup;
var	Vstup : byte;
begin
{zde si vybereme, že chceme nahrávat z mikrofonu}
 Vstup := Mikrofon;
{pro starší karty zapíšeme, který vstup jsme vybrali}
 WriteMixer($c,Vstup);
{zbytek je pro novější karty:}
{zde povolíme oba kanály pro stereo, pokud chcete nahrávat mono,}
{uložte do $3d registru 0 - což je levý kanál; někde se uvádí}
{naopak pravý kanál - asi záleží na zvukové kartě}
 case Vstup of	{L}
  Mikrofon : WriteMixer($3d,1);
        CD : WriteMixer($3d,6);	{oba kanály}
      Line : WriteMixer($3d,24);	{16+8}
 end;
 case Vstup of	{R}
  Mikrofon : WriteMixer($3e,1);
        CD : WriteMixer($3e,6);
      Line : WriteMixer($3e,24);
 end;
{nastavíme hlasitosti pro oba kanály na maximum}
 case Vstup of
  Mikrofon : WriteMixer($a0,7);
        CD : WriteMixer($28,255);
      Line : WriteMixer($2e,255);
 end;
{povolíme odposlouchávání, tj. zdroj kromě mikrofonu}
{uslyšíme i z reproduktorů}
 case Vstup of
  Mikrofon : WriteMixer($3c,0);
        CD : WriteMixer($3c,6);
      Line : WriteMixer($3c,24);
 end;
end;  

Pokud Vám to nahrává alespoň ticho, jste na dobré cestě. Stačí jen povolit někde příslušný vstup (např. na ovladači zvukové karty). Pak by Vám to mělo chodit. Tímto se rozloučím s DSP čipem a přesuneme se na druhou část zvukové karty, v dnešní době (právem) opomíjenou...

Využívání MIDI

Zvuková karta umí nejen přehrávat zvuky, ale také i noty. V dnešní době se však tato možnost už nepoužívá, neboť se jedná o příliš malou kvalitu např. oproti MODům. Avšak, pokud Vás zajímá, jak se kdysi vyráběla hudba, která měla jen pár kB, můžete se spolu se mnou podívat na dvě následující kapitoly, které se věnují horší a lepší kvalitě zpracování notových souborů. Existují sice zvukové karty, které jsou schopny zpracovávat přímo celé soubory MIDI a mají na to funkce, ale nám bude stačit, když se podíváme jen na hraní jednotlivých not.

FM syntéza

Tuto část proberu trochu hopem, protože se asi dneska nenajde nikdo, kdo by přehrával hudbu na OPL čipu. Když už někdo bude chtít použít MIDI, jistě sáhne pro mnohem lepší a jednodušší GM syntéze (pokud nevyužije rovnou XG). FM čip je programovatelný, takže se do něj dají nahrát tzv. banky, které určují, jak se ten či onen nástroj přehraje. FM čip můžete najít na adrese $388, $220 či $240. Stejně jako u GM (viz. dále), má každý kanál (track) určen nástroj, který na něm bude hrát (bicí mají zvláštní význam). Na tomto kanále pak hraje nota s určitým číslem a hlasitostí. Pokud Vás zajímá programování FM, najděte si např. jednotku Stevena H Dona. My se podíváme jen na základní procedury a funkce. Nejprve otestujeme, zda je čip FM aktivní a jakého je typu (OPL2 nebo 3):

var FMadr : word;


function TestOPL(test : word) : byte;
var	A,B : byte;
begin
 TestOPL := 0;
{Zkusíme najít čip FM}
 Port[Test] := 0; Delay(1); Port[Test+1] := 0; Delay(1);
 Port[Test] := 4; Delay(1); Port[Test+1] := $60; Delay(1);
 Port[Test] := 4; Delay(1); Port[Test+1] := $60; Delay(1);
 A := Port [Test];
 Port[Test] := 2; Delay(1); Port[Test+1] := $FF; Delay(1);
 Port[Test] := 4; Delay(1); Port[Test+1] := $21; Delay(1);
 B := Port[Test];
 Port[Test] := 4; Delay(1); Port[Test+1] := $60; Delay (1);
 Port[Test] := 4; Delay(1); Port[Test+1] := $60; Delay (1);
{otestujeme přítomnost OPL2}
 if ((A and $e0) = 0) and ((B and $e0) = $c0) then TestOPL := 2
  else Exit;	{čip na dané adrese není}
{test na OPL3}
 if Port[Test] and $6 = 0 then TestOPL := 3;
{čip OPL2 má jen 5 hlasou syntézu, čip OPL3 už 14 hlasou}
 FMadr := Test;
end;  

Nyní budeme potřebovat funkci, která bude do FM čipu zapisovat nějaká ta data a příkazy:

procedure WriteFM(Cip,Registr,Hodnota : byte);
var	poc,Temp : Byte;
	Adresa : Word;
begin
 case Chip of
  0 : Adresa := FMadr;
  1 : Adresa := FMadr+2;
 end;
 Port[adresa] := Registr;
 for poc := 1 to 25 do temp := Port[Adresa];
 Port [adresa+1] := Hodnota;
 for poc := 1 to 100 do temp := Port[Adresa];
end;  

Pokud Vás to zaujalo, nahlédněte do již zmíněné jednotky MIDIPLAY (Background MIDI unit) s přiloženým souborem FM.DAT (obsahuje definice nástrojů) na adrese http://shd.cjb.net, a hledejte tyto funkce: SetInstr, SetDrum, EnableNote, DisableNote, CutNote, NoteOff, NoteOn, EnableDrums a DrumOn. Díky nim budete moci na FM čipu hrát. V jednotce jsou i funkce pro práci s MIDI soubory (pokud jste si vymysleli vlastní formát nebo chcete lepší kvalitu, přejděte na další kapitolu věnující se GM) jako DoEvent, Playing, TimerHandler, LoadMidi, UnLoadMidi, PlayMIDI a StopMIDI (+/- nějaké další podpůrné funkce a procedury). Opravdu nemám náladu se zabývat něčím, co má jednak špatnou kvalitu a jednak se to špatně programuje (až uvidíte ty procedury plné příkazů na 20 řádků, pochopíte - a nemá smysl, abych to sem kopíroval přes Copy&Paste). GM je opravdu dobré, snadné (pravda, funguje zase jen pod Windows, nebo s dobrými ovladači (nebo HW GM kartou) i pod DOSem, ale kdo by dneska přehrával hudbu v MIDI, když máme MODy). Pokud Vám jde hlavně o formát MIDI pro Vaši hru, doporučení: udělejte si vlastní. Většinou mám zkušenost, že si rychleji udělám vlastní formát než se naučím ten stávající (a to platí i pro MODy).

General MIDI

Ač se to zdá neuvěřitelné, tak přesto, že GM poskytuje mnohem lepší zvuk (pokud je samozřejmě podporován), tak se s ním pracuje mnohem snadněji, než s obyčejnou FM syntézou. Naší prioritou je naučit se nechat zaznít jednotlivé noty. Ty poté můžete vypnout nebo je v daném kanálu nahradit jinou. V opačném případě Vám nota prostě postupně dozní. General MIDI může sídlit na jedné z těchto adres: $221,$231,$241,$251,$301,$321,$331,$333,$335,$337,$341,$361. Bohužel nevím, jak se to dá zdektovat jinak než přes proměnnou BLASTER (hodnota za P). Většinou bývá $330, takže použijte port $331. Další jediné řešení je zkoušet všechny tyto adresy postupně a provádět reset GM čipu, zda se ozve. Pokud ano, tak jste tu adresu našli. Avšak pozor, $330 a výše je oblíbená adresa SCSII řadiče (pokud není nastaven na $130)!

Pro každý kanál si nastavíte program, což je vlastně volba nástroje, a pak vyvážení (nebo-li balance). Každá nota poslaná na tento kanál pak přejme tyto parametry. U ní se pouze nastaví její číslo a hlasitost. Pamatujte, že všechna čísla jsou 0-127, tedy např. střed balance je 63. Kanálů je obvykle 16 (?). No, jdeme se podívat na základní funkce. Nejprve budeme potřebovat něco, co nám detekuje, zda je GM čip na zvukové kartě:

function TestGM(Adr : word) : boolean; assembler;
asm
	mov	bl,0
	push	10
	call	delay		{spoždění 10 ms}
	mov	dx,adr
	inc	dx
	in	al,dx
	and	al,$40
	or	al,al
	jnz	@neni		{je Port[Adr+1] and $40 = 0?}
	dec	dx
	mov	al,$f8	{provedeme reset}
	out	dx,al
	push	10
	call	delay
	inc	dx
	in	al,dx
	and	al,$40
	or	al,al
	jnz	@neni		{je na dané adrese GM čip?}
	inc	bl
@neni:
	mov	al,bl
end;  

Teď se podíváme na dvě funkce, které čtou z, resp. zapisují do GM čipu. Jednu z nich posléze využívá i funkce, která resetuje GM čip (měl by se provést alespoň na začátku programu):

var	GMadr : word;


procedure WriteGM; assembler;
asm
	mov	dx,gmadr
	push	ax
	xor	ax,ax
@cekej:
	dec	ah
	jz	@vyprsel
	in	al,dx
	and	al,$40	{je možné vysílat?}
	jnz	@cekej
@vyprsel:
	dec	dx
	pop	ax
	out	dx,al
end;


function ReadGM : byte; assembler;
asm
	mov	dx,gmadr
	xor	ax,ax
@cekej:
	dec	ah
	jz	@vyprsel
	in	al,dx
	and	al,$80	{je možné číst?}
	jnz	@cekej
@vyprsel:
	dec	dx
	in	al,dx
end;


procedure ResetGM; assembler;
asm
	mov	dx,gmadr
	mov	al,$ff	{xor	ax,ax; dec	ax}
	out	dx,al
	call	readgm
	xor	ax,ax
@cekej:
	dec	ah
	jz	@vyprsel
	in	al,dx
	and	al,$40
	jnz	@cekej
@vyprsel:
	mov	al,$3f
	out	dx,al		{tady je výjimka; není tu DEC DX}
end;  

Tak, a teď vytaste nějakou příručku o programování MIDI a zjistěte si, který nástroj je pod jakým číslem programu. Teď Vám uvedu dvě funkce, které pro daný kanál nastaví nástroj a vyvážení L-R:

procedure Nastroj(Kanal,Cislo : byte); assembler;
asm
	mov	al,kanal
	add	al,$c0
	call	writegm
	mov	al,cislo
	call	writegm
end;


procedure Balance(Kanal,Poloha : byte); assembler;
asm
	mov	al,kanal
	add	al,$b0
	call	writegm
	mov	al,10
	call	writegm
	mov	al,poloha
	call	writegm
end;  

Dobrá, kanál máme nakonfigurovaný, teď by to chtělo něco zahrát. Následující funkce řídí noty: první notu spustí a nechá doznít. Může být překryta (ale sama ještě chvíli běží) notou novou na stejném kanálu nebo zastavena:

procedure NotaOn(Kanal,Nota,Hlasitost : byte); assembler;
begin
	mov	al,kanal
	add	al,$90
	call	writegm
	mov	al,nota
	call	writegm
	mov	al,hlasitost
	call	writegm
end;


procedure NotaOff(Kanal,Nota,Hlasitost : byte); assembler;
asm
	mov	al,kanal
	add	al,$80
	call	writegm
	mov	al,nota
	call	writegm
	mov	al,hlasitost
	call	writegm
end;

A to je všechno. Tak jednoduchá záležitost to je. Prakticky je to jednodušší než programovat DSP čip a protože některé nástroje umí i hlas, střelbu, šum moře, ptáčky, atd., můžete ozvučit svoji hru čistě jen pomocí GM. Pokud Vám ovšem nebude vadit, že to bude na téměř každém počítači znít asi trošku jinak.

Zvuky ve Free Pascalu

Protože FP má zřejmě nějaké problémy (ale snad budou brzy opraveny, jako např. chyba v saturaci MMX) v implementaci DMPI, resp. DMA, pravděpodobně Vám nebude fungovat přehrávání s jeho využíváním. A naopak, bez něj budou Vaše programy pomalejší než by měly být. Doporučuji proto využívat jednotky třetích stran. Pokud Vám nevyhovuje jednoduchý RAIN postavený na platformě MIDAS (např. kvůli tomu, že všechny soubory přehrává z disku, kvůli čemuž je pomalejší než by měl být), můžete využít buď už přímo MIDAS, nebo jednotky Jedi SDL, Bass20, FMOD nebo lx5suite. S menšími úpravami (GetDPMIIntVec, OutPortB, atd.) by však měly chodit již napsané kódy pro Turbo Pascal (snad kromě konvenční paměti). Zde bude nutné si jen vyzkoušet, zda DMA funguje a pokud ne, napsat program bez něj. Existuje pár "zaručených" jednotek, které údajně mají přehrávat zvuky pod FP a jsou i více méně jednoduché, ale zda budou fungovat přímo u Vás, nelze standardně zaručit. Pokud se věnujete vývoji aplikací pro Windows, využijte raději Direct Sound jednotky.

Tímto článkem se s Vámi zatím loučím. Už mi totiž došly nápady. Jen doufám, že moje "nepřítomnost" (tedy kromě toho, že mi admin svěřil web, takže se na pár měsíců (přes prázdniny) změní rychlost objevování článků) nepohřbí náš časopis. Možná ještě někdy napíši něco o rychlém čtení z disku popř. ovládání CD-ROM, ale zatím už fakt nevím, o čem bych dál psal. Navíc se musím věnovat svým dalším projektům (tj. převod firmy na Linux (včetně web serveru, sdílení internetu, kamer, alarmu, evidence příchodů a odchodů, +naprogramovat pro to všechno software), úprava jedné písničky, napsání českých titulků k zahraničním filmům, napsat program na kalkulace pro firmu, nebo bych se také měl věnovat svému hlavnímu projektu (editor) atd.). Nyní byste měli mít všechno, co je potřeba na naprogramování kompletní hry (a nejen hry). Jinak Vám ještě poradím, abyste si zálohovali svá data co nejvíce často, protože se mi teď např. stalo, že jsem při machinacích s TXT soubory přišel o tento článek (naštěstí jen ve stádiu, kdy v něm byly 3 odstavce, ale i tak to naštve, když to musí člověk psát podruhé). A věřte nebo ne, já jsem se o zvukových kartách naučil docela dost až při psaní tohoto článku (do té doby mi to přišlo celkem složité, hlavně záznam a 16bitový přenos)...:-D Hodně štěstí a úspěchů při programování...

2006-12-06 | Martin Lux
Reklamy: