DirectX 8 - DirectSound
Tämä artikkeli kertoo lukijalle, mikä on DirectSound, mihin sitä käytetään, mihin sitä ei voi käyttää, ja miksi sitä käytetään. Artikkelissa kerron asioita ja esittelen ne lyhyiden ja yksinkertaisten koodinpätkien avustamana, joten asioiden ymmärtäminen ei käy ylitsepääsemättömän vaikeaksi.
23.1.2004 julkaistun artikkelin on kirjoittanut Anzuhan.
Lukijalta oletetaan C/C++-kielen perustaitoja sekä vähän tietämystä COM:in ja DirectX:n toiminnasta yleensä. Artikkelissa ei käydä läpi 3D-ääntä sekä äänen tallentamista, ne jätetään lukijan omaksi huoleksi. Koodi on kirjoitettu Microsoftin Visual Studio .NET 2003 -ohjelmointiympäristössä, ja käytössä on ollut DirectX 8.1 SDK. Koodin kääntymiseksi sinun tulee lisätä projektiisi kirjastot dsound.lib ja dxguid.lib.
Huomaa: tämä artikkeli EI käsittele MP3-tiedoston toistamista ja purkamista, sillä ne eivät kuulu enää DirectSoundiin, ne ovat nykyisin DirectShowin heiniä. Huomaa myös, että tässä artikkelissa käydään pelkät perusteet, sillä jos käytäisiin kaikki asiat läpi, tulisi artikkelista huomattavan iso ja pitkä.
Esimerkin saat tästä: http://www.suomipelit.com/files/artikkel...
Menkäämme siis asiaan!
DirectSound - rätinää ja FM-taajuuksia? #
DirectSound on olennainen osa DirectX:ää ja se käsittää tavallisen digitaaliäänen toiston, kolmiulotteisen äänen toiston ja äänen tallentamisen. Se ei paljoa eroa muista DirectX-rajapinnoista, ja sen alustaminen ja käyttö on aika lailla suoraviivaista. Vain äänipuskureiden käyttö saattaa tuottaa pientä päänvaivaa (eikä mikään ihme). Edellä mainituista tarkoituksista ensimmäinen, eli digitaaliäänen toisto, on se, mitä tulemme tässä artikkelissa käymään läpi. Esimerkki, jonka luon, kykenee toistaamaan allekirjoittaneen kuorsausta. :)
DirectSound-rajapinnan luonti #
Jos olet ohjelmoinut ennen DirectX:llä, voit varmaankin arvata miten DirectSound alustetaan ja otetaan käyttöön. Mutta jos et, kerron sen sinulle. DirectSound alustetaan -- kuten muutkin DirectX-rajapinnat -- käyttämällä DirectSoundCreate8-funktiota, jonka jälkeen asetetaan yhteistyötaso ja niin pois päin. DirectSound-olioita tarvitaan yksi jokaiselle äänikortille, ja jos sinulla on useampia, joudut luomaan muitakin. Yleensä tietokoneissa
on vain yksi äänikortti, joten annamme em. funktiolle ensimmäiseksi parametriksi 0 (oletusäänikortti). Jos et tajunnut, ei se mitään, tässä seuraa koodi joka hoitaa homman puolestasi:
#include <dsound.h>
// Globaalit DirectSound-oliot
LPDIRECTSOUND8 g_pDS = NULL; // DirectSound-olio
HRESULT LuoDS(HWND hwnd)
{
HRESULT hr = S_OK;
// Ensin tehdään itse DirectSound-olio.
if (FAILED(hr = DirectSoundCreate8(0, &g_pDS, NULL)))
{
return hr;
g_pDS->Release(); // Vapauta olion sidokset
}
// Sitten asetetaan olion yhteistyötaso DDSCL_PRIORITY:ksi
// Tällöin saamme itse asettaa ensisijaisen datapuskurin.
g_pDS->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
// Se oli siinä. Sitten luomaan puskuria, joka onkin
// astetta monimutkaisempi vaihe.
return hr;
}
Sehän olikin helppoa, eikö? Tutkitaanpa kyseistä koodia. Ensin, sisällytetään otsikkotiedosto dsound.h, jonka jälkeen esitellään globaali IDirectSound8-olio. Sen jälkeen, luodaan DirectSound-olio käyttämällä DirectSoundCreate8-funktiota. Jos se epäonnistui, vapautetaan se sidoksistaan äänikorttiin. Sen jälkeen asetetaan DirectSoundin yhteistyötaso Windowsin kanssa DSSCL_NORMALiksi, jolloinka DirectSound huolehtii itse ensisijaisen äänipuskurin ääniformaatin ominaisuuksista, esimerkiksi taajuudesta.
Tässä taulukko erilaisista lipuista yhteistyötasolle:
<table style="border: 1px solid #505050; padding: 3px;">
<tr><td align="center">DSSCL_NORMAL</td><td>Normaali yhteistyötaso, DirectSound huolehtii itse ensisijaisen puskurin ääniformaatista.</td></tr>
<tr><td align="center">DSSCL_PRIORITY</td><td>Prioriteetti-yhteistyötaso, jolloin voimme itse asettaa ensisijaisen äänipuskurin ääniformaatin.</td></tr>
<tr><td align="center">DSSCL_EXCLUSIVE</td><td>Prioriteettiyhteistyö, jolloin ohjelmalla on poissulkeva hallinta kun sovellus on etualalla..</td></tr>
<tr><td align="center">DSSCL_WRITEPRIMARY</td><td>Täysi hallinta ensisijaiselle äänipuskurille. Jee!</td></tr>
</table>
Kuten jokainen DirectX:n rajapinta, tämäkin rajapinta pitää vapauttaa laitesidoksistaan.
void VapautaDS()
{
// Vapauta DirectSound
if (NULL != g_pDS)
{
g_pDS->Release();
g_pDS = NULL;
}
}
Äänipuskurit - mitä ne ovat? #
DirectSound-olio on olio, joka edustaa tietokoneen äänikorttia ja sisältää äänipuskureita ja mahdollisesti soittaa niitä. DirectSound-olio sisältää ensisijaisen äänipuskurin, joka huolehtii äänen miksaamisesta ja käsittelee ääntä. Miksaaminen manuaalisesti ei ole niinkään yksinkertaista, ja sitä sinun ei tarvitse tehdä
(miksi meillä muuten olisi DirectSound? :). DirectSound huolehtii itse ensisijaisesta puskurista, jos et aseta yhteistyötasoa prioriteettiyhteistyölle. Silloin meidän pitää luoda itse puskuri ja se on astetta monimutkaisempi juttu, sillä jos haluat asettaa ensisijaisen puskurin sinun tulee käyttää DSBUFFERDESC-struktuuria puskurin luomiseen. Onneksi näin ei ole pakko tehdä, sillä jos asetetaan DSSCL_NORMAL, DirectSound huolehtii itse puskurin alustamisesta.
Kumminkin, DirectSound asettaa joitain tiettyjä rajoituksia puskurille: puskuri asetetaan käyttämään 22 kilohertsin taajuista ja 8-bittistä stereoääntä. Jos halutaan 16-bittistä ääntä tai korkeempia taajuuksia, pitää käyttää DSSCL_PRIORITY-lippua ja asettaa itse kaikki muu. Mutta, perusasetukset riittävät aloittamiseen ihan hyvin.
Toissijaiset puskurit
Toissijaiset puskurit ovat varsinaisia ääniä, joita soitamme. Niiden koko on rajoitamaton, kunhan niille on varattu tarpeeksi muistia. Data voidaan varata äänikortin muistiin, jonka koko on rajattu ja jonka kanssa pitää ja saa olla varovainen, sillä se voi loppua. Tosin, siinä on yksi etu: sen käsittely on nopeampaa.
Toissijaisia puskureita on kahdenlaisia: virtapuskureita, joiden data vaihtelee, ja staattisia puskureita joiden data pysy aina samana, ja joita soitetaan silloin tällöin. Esimerkissä käytän staattista äänipuskuria.
Virtapuskurit taas edustavat vaihtelevia puskureita, joissa data on vaihtelevaa. Nämä ovat edukseen silloin, kun kyseessä on iso kappale dataa ja sitä ei haluta ladata muistiin, esimerkiksi iso 200 megatavun kappale. Näistä sen enempää rupea selittelemään, sillä niiden toiminta on aika monimutkaista.
Ensimmäinen askel -- äänen möläyttäminen kajareist #
Noniin, nyt ollaankin päästy varmaankin artikkelin mielenkiintoisimpaan osaan: äänen toistamiseen. Periaatteessa äänen toistaminen on äänipuskurin luominen ja datan kopioimista, ja sen soittamista. Me haluamme pitää ison taulukon, jossa säilytetään kaikkia ääniä, ja joita soitetaan silloin kun tarve vaatii.
Valitettavasti, DirectSound ei sisällä minkäänlaista laturia tai dekooderia yksinkertaista WAV-laturia. Siis ei mitään! Tämä on suhteellisen huono juttu, ja jonka takia koodaajan pitää kirjoittaa sellainen itse. Minä en sellaiseen hommaan ryhtynyt, vaan latasin Google-nimisen aseen ja asentin kohteeksi WAV-laturin DirectSoundille. Koodi siis ei ole omaani, ja sen on (lähteiden mukaan) kirjoittanut André Lamothe -niminen henkilö. Kiitokset siis käy tästä seuraavasta koodista hänelle. Kumminkin, suomensin kommentit (jotta luku olisi helpompaa)
int LueWAV(LPSTR szTiedosto)
{
HMMIO hwav; // .WAV-tiedoston kahva
MMCKINFO parent, // isäntälohko
child; // lapsilohko
WAVEFORMATEX wfmtx; // aaltotyyppistruktuuri
int sound_id = -1, // äänen id l. tunnus
index; // silmukkamuuttuja
UCHAR *snd_buffer, // väliaikainen puskuri VOC-datalle
*audio_ptr_1=NULL, // osoitin ensimmäiseen kirjoituspuskuriin
*audio_ptr_2=NULL; // osoitin toiseen kirjoituspuskuriin
DWORD audio_length_1=0, // ensimmäisen kirjoituspuskurin pituus
audio_length_2=0; // toisen kirjoituspuskurin pituus
// onko vapaita tunnuksia?
for (index = 0; index < MAX_SOUNDS; index++)
{
// varmista, että tätä ääntä ei käytetä
if (sound_fx[index].state == SOUND_NULL)
{
sound_id = index;
break;
}
}
// saatiinko vapaa tunnus?
if (sound_id==-1)
return(-1);
// alusta isäntälohkon struktuuri
parent.ckid = (FOURCC)0;
parent.cksize = 0;
parent.fccType = (FOURCC)0;
parent.dwDataOffset = 0;
parent.dwFlags = 0;
// kopioi data
child = parent;
// avaa .WAV-filusi
if ((hwav = mmioOpen(szTiedosto, NULL, MMIO_READ | MMIO_ALLOCBUF))==NULL)
return(-1);
// laskeudu RIFF-tasolle
parent.fccType = mmioFOURCC('W', 'A', 'V', 'E');
if (mmioDescend(hwav, &parent, NULL, MMIO_FINDRIFF))
{
// suljetaan tiedosto
mmioClose(hwav, 0);
// palauta virhe, ei ole luettavaa dataa
return -1;
}
// laskeudu aaltoformaattiin
child.ckid = mmioFOURCC('f', 'm', 't', ' ');
if (mmioDescend(hwav, &child, &parent, 0))
{
// sulje
mmioClose(hwav, 0);
// ei ole dataa
return -1;
}
// lue aaltotyyppi tiedostosta
if (mmioRead(hwav, (char *)&wfmtx, sizeof(wfmtx)) != sizeof(wfmtx))
{
// sulje
mmioClose(hwav, 0);
// palauta virhe, ei dataa (deja vu... hmm?)
return -1;
}
// varmista, että se data on WAVE_FORMAT_PCM
if (wfmtx.wFormatTag != WAVE_FORMAT_PCM)
{
// sulje
mmioClose(hwav, 0);
// palauta virhe, ei dataa
return -1;
}
// noustaan taas ylös yksi datataso, jotta voimme lukea dataa
if (mmioAscend(hwav, &child, 0))
{
// sulje
mmioClose(hwav, 0);
// palauta virhe, ei dataa
return -1;
}
// laskeudu datalohkoon
child.ckid = mmioFOURCC('d', 'a', 't', 'a');
if (mmioDescend(hwav, &child, &parent, MMIO_FINDCHUNK))
{
// sulje
mmioClose(hwav, 0);
// palauta virhe, ei dataa
return -1;
} // end if
// noniin! nyt meidän enää tarvitsee lukea data
// ja alustaa DirectSound-puskuri.
// varaa luettavalle datalle muistia
snd_buffer = (UCHAR *)malloc(child.cksize);
// lue data
mmioRead(hwav, (char *)snd_buffer, child.cksize);
// sulje tiedosto
mmioClose(hwav, 0);
sound_fx[sound_id].rate = wfmtx.nSamplesPerSec;
sound_fx[sound_id].size = child.cksize;
sound_fx[sound_id].state = SOUND_LOADED;
// alusta tyypin datastruktuuri
memset(&pcmwf, 0, sizeof(WAVEFORMATEX));
pcmwf.wFormatTag = WAVE_FORMAT_PCM;
pcmwf.nChannels = 1; // yksi kanava, joten ääni monoa
pcmwf.nSamplesPerSec = 11025;
pcmwf.nBlockAlign = 1;
pcmwf.nAvgBytesPerSec = pcmwf.nSamplesPerSec * pcmwf.nBlockAlign;
pcmwf.wBitsPerSample = 8;
pcmwf.cbSize = 0;
// valmistaudu tekemään puskuri
g_DSBufDesc.dwSize = sizeof(DSBUFFERDESC);
g_DSBufDesc.dwFlags = DSBCAPS_STATIC | DSBCAPS_LOCSOFTWARE;
g_DSBufDesc.dwBufferBytes = child.cksize;
g_DSBufDesc.lpwfxFormat = &pcmwf;
// luo äänipuskuri
if (FAILED(g_pDS->CreateSoundBuffer(&g_DSBufDesc,
&sound_fx[sound_id].dsbuffer, NULL)))
{
// vapauta muisti
free(snd_buffer);
// palauta virhe
return(-1);
}
// kopioi data läänipuskuriin, lukitse puskuri
if (FAILED(sound_fx[sound_id].dsbuffer->Lock(0,
child.cksize,
(void **) &audio_ptr_1, &audio_length_1,
(void **)&audio_ptr_2,
&audio_length_2,
DSBLOCK_FROMWRITECURSOR)))
return 0;
// kopioi ensimmäinen osa
memcpy(audio_ptr_1, snd_buffer, audio_length_1);
// ja toinen osa
memcpy(audio_ptr_2, (snd_buffer+audio_length_1), audio_length_2);
// avaa puskuri
if (FAILED(sound_fx[sound_id].dsbuffer->Unlock(audio_ptr_1,
audio_length_1,
audio_ptr_2, audio_length_2)))
return 0;
// vapauta väliaikainen puskuri
free(snd_buffer);
// palauta id
return sound_id;
}
Koodissa käytetään Microsoftin Multimedia I/O-järjestelmää, joka tukee äänen käsittelyä, lataamista, äänen maninpulointia ja n+1 muuta ominaisuutta. Siitä ei sen enempää. Funktion lopussa luodaan toissijainen äänipuskuri ja siihen sitten luetaan data audio_ptr_X-osoittimista. DSBLOCK_FROMWRITECURSOR lukitsee puskurin silloisesta kirjoituskursorista lähtien. Äänen toisto tapahtuu DirectSoundBuffer::Play()-funktion avulla ja pysäyttäminen Stop()-funktion avulla. Ylläoleva koodi ei toimi vielä (eikä edes käänny), sillä siitä puuttuu äänitaulukko johon äänet ladataan. Kaiken löydät esimerkistä.
Esimerkki #
Esimerkin käyttö on helppoa, ohjelma käynnistyy ja mikäli kaikki sujuu kunnolla, paina enter-näppäintä ja sieltä pitäisi kuulua kuorsausta. :) Esimerkki on vielä alkutekijöissään, ja siinä on pieniä puutteita: kun painat enteriä toisen kerran, se alkaa toistamaan toista ääntä. Sinun tehtäväsi on korjata tämä bugi, ja tehdä siitä sellainen, että se lopettaa äänen toiston kun uusi ääni toistetaan. Voit kokeilla DirectInputin yhdistämistä tähän ja äänittää jonkun tietyn äänen (mikäli omistat mikrofonin) ja toistaa jokaisesta eri näppäimesta erikorkeuksisen äänen (vinkki: tämä tapahtuu taajuutta vaihtamalla).
Tästä eteenpäin #
Vaikka asiat saattavat näyttää monimutkaisilta, tässä ei todellakaan, ehei, ei ole edes puoliakaan siitä mitä DirectSound käsittää. DirectSoundiin näet kuuluu vielä tämän lisäksi 3D-ääni ja äänen tallentaminen, joista jokainen on ihan oma lukunsa. Jätän näiden opiskelun sinulle haasteeksi, sillä niillä räpeltäminen se vasta hauskaa onkin! Eli avaapa DirectX:n SDK:n dokumentaatiot eteesi ja anna palaa. Ei se niin vaikeaa ole, eihän? ;)
Loppusanat #
Nyt ollaan siis päästy artikkelin loppuun. Nyt siis tiedät, miten alustaa ja vapauttaa DirectSound-rajapinta, luoda ensi- ja toissijainen äänipuskuri ja kenties möläyttää ääntä kaiuttimista. Toivon, että tästä artikkelista on hyötyä teille kaikille DirectX-ohjelmoinnista kiinnostuneille. Kiitokset Jarkko Parviaiselle artikkelin läpilukaisusta.
