Nemrég eljutottam arra a szintre, hogy érdemes lenne hangkezelést is belevinni az enginebe (ld. címoldal) és nekiálltam keresgélni ilyen API-kat/librarykat. Először ezek közül emlitenék meg néhányat:
A fejezetben egy olyan osztályt fogok írni, ami egy külön szálon képes streamelni az audio adatot, miközben a főszál rajzol, illetve az XAudio2 eszköz lejátssza a hangot.
Nézd anyu, több szálon futok!
Azt gondolom mindenki tudja, hogy a memória mérete véges, sőt ez különösen igaz pl. a videókártyára vagy hangkártyára (főleg ha alaplapi).
Ezért akármekkora zenéket nem kéne se a kártyán, se a fő memóriában tárolni. Másrészt amíg egy zenerészletet játszik a kártya, addig a következő
részletet már nyugodtan töltögethetné a háttérben, így mondjuk egy 10 MB-os zenefájl helyett összesen csak 3x64 KB -ot kell eltárolni, ami igencsak
takarékos! Ezt a töltögetést szokták hívni streaming-nek (adatfolyam).
A bar() függvény semmiféle feldolgozást nem végez, hanem berakja az üzenetet egy sorba és vissza is tér, így a főszál megy tovább a saját dolgára. Egy másik szál azonban időnként megnézi ezt a (közös) sort, és ha van benne üzenet azt feldolgozza. Egy nagyon fontos dolog azonban kimaradt az ábráról: könnyen lehet, hogy a push végrehajtásának felénél az operációs rendszer elveszi a processzort a fő száltól és a dolgozó kapja meg. Tegyük fel, hogy a méretet már megnövelte a sor, de az elem még nem került be: elszáll a program. Ez azért van, mert a C++ STL egyáltalán nem thread safe. Tehát a programozó dolga biztosítani, hogy az ilyen közös változókat mindig csak az egyik thread használhassa. Jobb helyeken ezt mutual exclusion -nak (kölcsönös kizárás, mutex) hívják. Az ide vonatkozó implementációkat nem fogom most leírni, inkább majd visszamenőleg egy külön cikkben. A lényeg annyi, hogy a fő osztályban van egy Guard objektum, ami nem enged be más szálat amíg az egyik szál használja a közös változót.
XAudio 2 incializálás
Első nekifutásra csak hozzuk létre a lényeges objektumokat. Hasonlóan a Direct3D-hez itt is két központi objektum van. Az egyik az IXAudio2 interfész,
ez hozza létre a hang objektumokat és ez felel a hardver kezeléséért is. A másik interfész az IXAudio2MasteringVoice, ami a konkrét output eszközre írja
az audio adatokat, de nem nagyon kell foglalkozni vele (hacsak nem a master volume-t akarod állitgatni).
CODE
#include <xaudio2.h>
IXAudio2* xaudio2 = NULL;
IXAudio2MasteringVoice* masteringvoice = NULL;
int main()
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
InitXAudio2();
// TODO:
if( masteringvoice )
masteringvoice->DestroyVoice();
if( xaudio2 )
xaudio2->Release();
CoUninitialize();
system("pause");
return 0;
}
HRESULT InitXAudio2()
{
HRESULT hr;
if( FAILED(hr = XAudio2Create(&xaudio2, 0)) )
{
MYERROR("Could not create XAudio2 object");
return E_FAIL;
}
if( FAILED(hr = xaudio2->CreateMasteringVoice(&masteringvoice)) )
{
MYERROR("Could not create mastering voice");
return E_FAIL;
}
return S_OK;
}
Az XAudio2Create függvénynek meg lehet adni második paraméterként az XAUDIO2_DEBUG_ENGINE flaget, ekkor a D3DX-hez hasonlóan a debug libraryt fogja használni. Harmadik paraméterként még meg lehet adni, hogy melyik processzort használja az objektum, de a dokumentáció azt javasolja, hogy bízzuk rá az XAudio-ra. Az IXAudio2::CreateMasteringVoice metódusnak szintén több paramétere van, ha az alapértelmezetteket hagyjuk, akkor a detektált hardver alapján választ, így nem különösebben érdekesek. A main függvényben nagyjából ennyi történik hang szempontból, a többi ehhez kapcsolódó dolgot az AudioStreamer nevű osztályban fogom megvalósítani.
Ogg Vorbis hangfájlok betöltése
Azért ezt választottam mert egyszerű betölteni (van hozzá lib), és a VLC player tud ilyenbe konvertálni. Két libraryra van szükség, az egyik a libogg, a másik a libvorbis
(ki nem találtátok volna). Egy kis fejtágítás: a vorbis egy audio formátum (és codec), az ogg pedig a fájl (mint konténer) formátuma ami ezt tárolja
(tehát ha valami meg tudja nyitni az ogg fájlokat, az nem biztos hogy dekódolni is tudja ami benne van). Amit meg kell említeni, hogy a vorbis egy veszteséges tömörítő.
CODE
class Sound : public IXAudio2VoiceCallback
{
friend class AudioStreamer;
private:
WAVEFORMATEX format;
IXAudio2SourceVoice* voice;
char* data[MAX_BUFFER_COUNT];
unsigned int totalsize;
// ...
};
Nem adtam meg most minden membert, azok majd a streamelésnél fognak szerepet játszani, mint ahogy a data tömb többi eleme is. A voice változó reprezentálja a hangot az XAudio2 felé (mint pl. Direct3D-ben a textúra). Erről azt kell tudni, hogy a háttérben egy buffer sort tárol és ha az egyiknek a lejátszását befejezte, akkor veszi a következőt. Hogy a user felé legyen valami visszajelzés erről, meg lehet adni létrehozáskor egy callback objektumot, ezért származtattam az osztályt az IXAudio2VoiceCallback interfészből. Hét metódust kell felüldefiniálni, ezekről majd később (csak egyet fogok ténylegesen használni). A betöltéshez Crusader kolléga tutorialját használtam fel. A lényeg, hogy a vorbis nem garantálja, hogy annyi adatot fog beolvasni amennyit kértél tőle, ezért mindenképpen egy ciklusban kell elvégezni az olvasást. A betöltést az AudioStreamer osztály végzi el, a létrehozott Sound objektumot pedig egy listában tárolja (és a program végén felszabadítja). CODE
Sound* AudioStreamer::LoadSound(IXAudio2* xaudio2, const std::string& file)
{
FILE* infile = NULL;
fopen_s(&infile, file.c_str(), "rb");
// ...
OggVorbis_File oggfile;
int stream;
long readbytes = 0;
long totalread = 0;
ov_open(infile, &oggfile, NULL, 0);
vorbis_info* info = ov_info(&oggfile, -1);
Sound* s = new Sound();
// ez a struktúra irja le a hangot az XAudio2 felé
s->format.nChannels = info->channels;
s->format.cbSize = sizeof(WAVEFORMATEX);
s->format.wBitsPerSample = 16;
s->format.nSamplesPerSec = info->rate;
s->format.nAvgBytesPerSec = info->rate * 2 * info->channels;
s->format.nBlockAlign = 2 * info->channels;
s->format.wFormatTag = 1;
s->totalsize = (unsigned int)ov_pcm_total(&oggfile, -1) * 2 * info->channels;
s->data[0] = new char[s->totalsize];
// addig tölt amíg van mit olvasni
do
{
readbytes = ov_read(
&oggfile, s->data[0] + totalread, s->totalsize - totalread,
0, 2, 1, &stream);
totalread += readbytes;
}
while( readbytes > 0 && totalread < s->totalsize );
ov_clear(&oggfile);
// folyt.
Ezen a ponton az adat már bent van a tömbben, már csak az XAudio2 felé kéne ezt valahogy delegálni. Először a voice változót hozom létre az alábbi módon: CODE
if( FAILED(xaudio2->CreateSourceVoice(&s->voice, &s->format, 0, 2.0f, s)) )
{
SND_ERROR("AudioStreamer::LoadSound(): Could not create source voice");
delete s;
return NULL;
}
// folyt.
Az első két változó nyilvánvaló, a második kettőt csak azért adtam meg, hogy utolsó paraméterként megadhassam az s objektumot, mint callback, így le tudja majd kezelni a különféle eseményeket. Innen már csak magát az adatot kell beadni az objektumnak, még pedig egy buffer leíróval: CODE
XAUDIO2_BUFFER audbuff;
memset(&audbuff, 0, sizeof(XAUDIO2_BUFFER));
audbuff.pAudioData = (const BYTE*)s->data[0];
audbuff.Flags = XAUDIO2_END_OF_STREAM;
audbuff.AudioBytes = s->totalsize;
audbuff.LoopCount = XAUDIO2_LOOP_INFINITE;
if( FAILED(s->voice->SubmitSourceBuffer(&audbuff)) )
{
SND_ERROR("AudioStreamer::LoadSound(): Could not submit buffer");
delete s;
return NULL;
}
// ...
soundguard.Lock();
sound.push_back(s);
soundguard.Unlock();
}
Ezen a bufferen keresztül lehet megadni az ismétlések számát, helyét meg egy csomó egyéb dolgot, amire most abszolút nincs szükség. Mivel ez után nem fog több adat jönni, ezért megadtam az XAUDIO2_END_OF_STREAM flaget, ezzel jelezve az objektum felé, hogy ha befejezte ezt a buffert, akkor több adat nem jön már. A lejátszás a voice->Start(0); hívással történik, a felszabadítás pedig a fentihez hasonlóan voice->DestroyVoice();. A hangerőt lehet állitani a mastering voice-on is, de ezen is a voice->SetVolume(mennyi); metóduson keresztül (alapértelmezésben 1).
Streamelés külön szálon
Ez eddig még nem is volt olyan nehéz, most viszont ismertetni fogom, hogy a streamelést hogyan oldottam meg. A Vorbis és az XAudio2 oldaláról nem lesz nehéz (használatilag teljesen ugyanaz), programozásilag viszont jól meg kell gondolni, hogy mit csinálsz. Első lépésként a Sound osztályt kiegészítem az alábbi változókkal:
A másik eset az lesz, amikor az XAudio2 befejezte egy buffer lejátszását, és meghívja a felüldefiniált OnBufferEnd metódust. Ez paraméterként meg tudja kapni a bufferhez tartozó user adatot (XAUDIO2_BUFFER::pContext member); legyen ez az adat a buffer indexe. Hogyan tudjuk eldönteni, hogy melyik buffert kell behúzni? Tudjuk, hogy ha van még hely a sorban, akkor a buffer bekerült már, tehát csak akkor kell itt behúzni, ha nincs szabad buffer. Sőt azt is tudjuk, hogy a buffereken sorban megyünk végig, tehát az éppen aktuális tölthető buffer előttit töltöttük be utoljára: CODE
COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE Sound::OnBufferEnd(void* context)
{
if( context )
{
int id = *reinterpret_cast<int*>(context);
if( freebuffers == 0 )
{
int buffind = (MAX_BUFFER_COUNT + (currentbuffer - 1)) % MAX_BUFFER_COUNT;
PullBuffer(buffind);
}
++freebuffers;
}
// ...
}
A PullBuffer metódus teljesen hasonlóan rakja be a buffert, mint a szimpla hangnál, de csak akkor lesz a flag member XAUDIO2_END_OF_STREAM, ha ez az utolsó buffer (azaz már az egész fájlt beolvastuk).
Shared változók pokla
A program nehéz része az, hogy hova tegyünk lockokat. Egyáltalán mik a shared változók? Először is azt kéne megnézni, hogy hány thread van:
Ezen kívül még a Sound osztályba is raktam egy guardot, mégpedig a Play metódusba. Ha a hang szimpla, akkor rögtön le lehet játszani amint meghivja a main thread ezt a metódust. Ha viszont streamelendő, akkor meg kell jegyezni, hogy már játszani kéne, és majd a worker thread fogja ténylegesen elindítani. Másrészt az itt felhasznált változókat a worker thread egyébként is bármikor írhatja: CODE
void Sound::Play()
{
playguard.Lock();
{
playrequest = true;
if( voice && canplay && !playing )
{
playing = true;
voice->Start(0);
}
}
playguard.Unlock();
}
Az AudioStreamer osztályban a fentiek értelmében a guard mindenhol lockol, ahol a hanglistát babrálja. Különösen figyelni kell a Destroy() metódusra, hiszen a worker thread még futhat ekkor! Az appverif.exe nevű toolt nagyon ajánlom használni, mert segít megmondani, hogy hol rontottad el.
Summarum
A fejezet végére még néhány gondolat:
A kód és a bináris letöhető a szokott helyen. A 11_Particles nevű progiba raktam bele. A teszt zene Judas Priest: Painkiller nevű heavy metal dal. 320 kb/s-be tömörítettem, ez kb. MP3 minőség. Ha kell valakinek FLAC-ban szóljon. METAL FOREVAH!!!!! Höfö:
|