DirectX-Anwendungen mit C++ erstellen - Einführung in DirectSound
Wir werden Ihnen nun zeigen wie Sie mit DirectX Töne und Musik durch Ihre Lautsprecherboxen
jagen.
Wir empfehlen Ihnen zum besseren Verständnis dieses Teiles das 1. Kapitel der
DirectDraw - Einführung zu lesen.
In diesem Kapitel soll unser Ziel lediglich die
Initialisierung von DirectSound und das
Abspielen einer Wave-Datei sein.
Leider ist es hier nicht möglich die erwarteten Resultate mittels Screenshot vorzustellen, aber Sie werden bestimmt sehr schnell
herraus höhren ob da was nicht stimmt.
Als erstes erzeugen wir ein neues Projekt mit dem Namen
TestDSound und der Quelldatei
TestDSound.c. Als Header-Datei nutzen wir
die
dsound.h und der Linker muß mit der
dsound.lib-Datei gefüttert werden.
Der Aufbau von DirectSound ist dem von DirectDraw sehr ähnlich. Wir benötigen als erstes ein zentrales
DirectSound - Object von dem
wir dann Arbeitsflächen - hier
Soundpuffer genannt - ableiten.
Demnach sieht der Definitionsteil des Programmes so aus:
/*
* TestDSound.c
*/
#include
#define INITGUID // <- nur für lcc-win32 Compiler
#include "dsound.h"
LPDIRECTSOUND lpDSound; //das Zentrale DirectSound-Objekt
LPDIRECTSOUNDBUFFER lpDSPrim; //der Primäre Sounpuffer
LPDIRECTSOUNDBUFFER lpDSWork; //Arbeits-Soundpuffer
BOOL IsDSound = FALSE; //ist TRUE wenn DSound erfolgreich initialisiert wurde
LRESULT WINAPI WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
Der Vollständigkeit halber bilden wir hier die WinMain-Funktion ab. Sie erzeugt ein Fenster,
zeigt dieses an, ruft die Funktion
InitDirectSound auf und startet die Messagebehandlung.
Nichts Neues, also :
BOOL APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
WNDCLASS wc;
HWND hwnd;
MSG msg;
wc.lpszClassName = "TestDSound";
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) (COLOR_BACKGROUND + 1);
wc.lpszMenuName = NULL;
RegisterClass(&wc);
hwnd = CreateWindow ("TestDSound", "Test DirectSound Fenster", WS_OVERLAPPED,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow (hwnd, SW_SHOW);
IsDSound = InitDirectSound(hwnd); //DirectSound initailisieren
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
ExitDirectSound(); //DirectSound wieder abbauen
return msg.wParam;
}
Wenden wir uns also nun der Initialisierung von DirectSound zu. Eingebettet wird sie in die Funktion
InitDirectSound.
Sie werden feststellen, daß bei der Programmierung von DirectSound sehr starke Synergien zu DirectDraw deutlich werden.
Der Allgorythmus der DSound-Initialisierung sieht etwa so aus:
- Zentrales DirectSound-Objekt erstellen
- Kooperationsebene setzen
- Soundpuffer beschreiben
- Soundpuffer anhand der Beschreibung erstellen
Auch hier benutzen wir 2 Soundpuffer, einen Primären und einen normalen. Allerdings hat das hier andere Gründe als in DirectDraw.
Was jedoch gleich ist: In DirectDraw stellte die primäre Zeichenfläche die Bildschirmausgabe dar. Ebenso stellt der primäre Soundpuffer die
Soundausgabe dar. Mit dem primären Sounpuffer bereiten wir die Soundkarte für die Ausgabe vor und stellen das Ausgabeformat (Samplingrate,
Bits/Sample ...) ein. Anschließend wird der primäre Soundpuffer nicht mehr gebraucht.
In Codierter Form stellt sich dieser Allgorythmus wie folgt dar:
BOOL InitDirectSound(HWND hwnd)
{
DSBUFFERDESC soundBuffDesc; //Beschreibung des Soundpuffers
//Erstellen des zentralen DirectSound-Objektes
if (DirectSoundCreate(NULL, &lpDSound, NULL) != DD_OK)
{
MessageBox(hwnd, "Fehler bei Create DirectSound-Objekt", "ERROR", MB_OK);
return FALSE;
}
//Kooperationsebene setzen
if (lpDSound->lpVtbl->SetCooperativeLevel(lpDSound, hwnd, DSSCL_PRIORITY) != DS_OK)
{
MessageBox(hwnd, "Fehler bei SetCooperativeLevel", "ERROR", MB_OK);
return FALSE;
}
//Beschreibung des Soundpuffers füllen
ZeroMemory(&soundBuffDesc, sizeof(soundBuffDesc));
soundBuffDesc.dwSize = sizeof(soundBuffDesc); //DX-Standard
soundBuffDesc.dwFlags = DSBCAPS_PRIMARYBUFFER;//primärer SoundPuffer
soundBuffDesc.dwBufferBytes = 0; //Puffergrösse muss 0 Byte sein für primären Puffer
soundBuffDesc.lpwfxFormat = NULL; //Pufferformat muss NULL sein für primären Puffer
//Soundpuffer erzeugen
if (lpDSound->lpVtbl->CreateSoundBuffer(lpDSound, &soundBuffDesc, &lpDSPrim, NULL)!=DS_OK)
{
MessageBox(hwnd, "Fehler bei Create DirectSoundPuffer", "ERROR", MB_OK);
return FALSE;
}
[...]
//hier folgt später die Einstellung des Ausgabeformats des Soundpuffers
[...]
lpDSPrim->lpVtbl->Release(lpDSPrim);
return TRUE;
}
Wie Sie vielleicht bemerkt haben, haben wir das Pufferformat nicht gesetzt.
Aber was bedeutet das ? Ganz einfach : Bei der Initialisierung von DirectDraw erhielt unsere primäre Zeichenfläche immer das
Pixelformat unseres Desktops. Dies geschah föllig automatisch. Meist verändert man diese Tatsache, indem man eine Anwendung im
Vollbildmodus startet. Dabei wird das Format der Zeichenfläche durch die Auflösung (BreitexHöhe) und die Farbtiefe (16Bit /32Bit)
bestimmt.
Diese Festlegung des Formates müssen wir bei DirectSound immer selbst vornehmen. Ein Soundpuffer-Format besteht im Wesentlichen aus 3
Angaben:
- Anzahl Kanäle (1 mono, 2 Stereo)
- Samples pro Sekunde (22kHz / 44kHz)
- Bits pro Sample (8 / 16)
Dieses Ausgabeformat bestimmt wie die Sound-Daten von der Soundkarte verarbeitet und ausgegeben werden.
Gespeichert wird das Format in einer Struktur. Diese nennt sich
WAVEFORMATEX. Definiert ist sie in der Header-Datei
waveread.h.
Dem Soundpuffer teilen wir diese Daten dann per
SetFormat mit in dem wir einen Zeiger auf diese Struktur übergeben.
Aber lassen Sie uns als erstes das Ausgabeformat beschreiben:
BOOL InitDirectSound(HWND hwnd)
{
WAVEFORMATEX wFormat;//wFormat wird die Daten über das Ausgabeformat halten
[...]
//hier folgt die Einstellung des Ausgabeformats des Soundpuffers
ZeroMemory( &wFormat, sizeof(WAVEFORMATEX));//ersteinmal die Struktur leeren
wFormat.wFormatTag = WAVE_FORMAT_PCM; //welches Format ?
wFormat.nChannels = 2; // 2 Kanäle
wFormat.nSamplesPerSec = 22050; //22Khz Samplingrate
wFormat.wBitsPerSample = 16; //16 Bits / Sample
wFormat.nBlockAlign = wFormat.wBitsPerSample / 8 * wFormat.nChannels;
wFormat.nAvgBytesPerSec = wFormat.nSamplesPerSec * wFormat.nBlockAlign;
if (lpDSPrim->lpVtbl->SetFormat(lpDSPrim, &wFormat) != DS_OK)
{
MessageBox(hwnd, "Konnte Format nicht setzen!", "ERROR", MB_OK);
lpDSPrim->lpVtbl->Release(lpDSPrim);//primären Puffer frei geben
return FALSE;
}
[...]
}
Bevor wir uns die ersten Piepser im Lautsprecher anhöhren werden wir als nächstes
die Aufräumarbeiten in der Funktion
ExitDirectSound vollziehen.
void ExitDirectSound()
{
if (lpDSWork != NULL) lpDSWork->lpVtbl->Release(lpDSWork);
if (lpDSPrim != NULL) lpDSPrim->lpVtbl->Release(lpDSPrim);
if (lpDSound != NULL) lpDSound->lpVtbl->Release(lpDSound);
}
Im ersten DirectDraw-Kapitel haben wir Ihnen an dieser Stelle gezeigt, wie sie ein paar Punkte in ihre Arbeitszeichenfläche
"malen". Sie werden sicher verstehen, dass wir das mit dem Soundpuffer nicht machen können, da wir die Soundkarte nicht
zerstören möchten. Also - das wird Sie sicher freuen - werden wir Ihnen gleich zeigen wie sie eine Wave-Datei in einen Soundpuffer
laden und diesen dann abspielen.
Für die Arbeit mit Sound-Dateien im WAV-Format bietet uns das DirectX7-SDK die Dateien
wavread.cpp und
wavread.h an. Für C++ - Entwickler mit
DirectX gut geeignet ist das darin definierte Objekt
CWaveSoundRead.
Da unsere Beispielprogramme aber in C gehalten sind, ist es notwendig einige Änderungen an den Funktionen durchzuführen. Damit Sie sich nicht
damit abmühen müssen werden sie die geänderte Fassung zusammen mit dem Quelltext dieses Kapitels runterladen können.
Fügen Sie diese Dateien zum Projekt hinzu. Zusätzlich muss der Linker noch die Datei
winmm.lib vorgesetzt bekommen.
Mit Hilfe dieser Funktionen werden wir eine Wave-Datei öffnen, das Format und die eigentlichen Sounddaten auslesen und in einen Soundpuffer
übertragen.
Die Funktion
ReadWaveToBuff soll diese Aufgaben für uns erlededigen. Wir übergeben Ihr einen Zeiger auf einen IDirectSoundPuffer, der innerhald
der Funktion erzeugt wird und den Dateinamen der Sounddatei.
Doch vor der Praxis noch ein paar theoretische Worte zum Soundpuffer:
Vom Konzept ist ein Soundpuffer ein Ring, er hat also weder Anfang noch Ende. Wenn wir z.Bsp. einen Sounpuffer mit 100 Bytes anlegen, dann
befindet sich das 100-ste Byte an Position 0. Das folgende Bild soll dies etwas verdeutlichen.

Dieses Wissen ist ganz wichtig, denn wenn wir Daten in den Puffer schreiben möchten, müssen wir diesen wie schon in DirectDraw
sperren (
Lock). Dabei müssen wir angeben ab welcher Position wieviel Bytes gesperrt werden sollen. So ist es möglich einen Soundpuffer
der 100 Bytes groß ist, ab der Position 0 mit 100 Bytes zu sperren und zu befüllen als auch ab Position 50 mit 100 Bytes zu sperren und befüllen.
Im zweiten Falle wird der Puffer bis 100 Bytes beschrieben und dann bei 0 wieder angefangen.
Das hat zur Folge, daß wir beim Sperren eines Soundpuffers nicht nur einen sondern 2 Zeiger auf die Sounddaten erhalten. Wobei der zweite
Zeiger nur von Bedeutung ist (und auch nur dann gesetzt ist) wenn der Lock auf den Puffer über sein Ende hinaus geht. Man sollte daher auch strickt vermeiden
einen größeren Bereich zu sperren als der Buffer lang ist. Wenn wir von einem 100 Byte langem Puffer 150 Byte sperren so werden die letzten 50 Bytes die ersten
50 bytes des Puffers wieder überschreiben.
Aber genug der Theorie - kommen wir nun wieder zur Praxis:
BOOL ReadWaveToBuff(LPDIRECTSOUNDBUFFER *lplpDSBuff, LPSTR lpFile)
{
CWAVSTRUCT cWave;//die Struktur für die Arbeit mit WAV-Dateien
DSBUFFERDESC dsbd; //Beschreibung des Soundpuffers (für Anlage benötigt)
BYTE *lpZBuff; //Zwischenpuffer für die Sounddaten der Wavedatei
UINT realSize; //enthält wirklich gelesene Bytes der WAVE-Daten
void *lpvBuff1; //erster Zeiger auf Sounddaten
UINT iSize1; //wieviel Bytes enthält der erste SoundPufferbereich
void *lpvBuff2; //zweiter Zeiger auf Sounddaten
UINT iSize2; //wieviel Bytes enthält der zweite SoundPufferbereich
//Wav-datei öffnen
if (FAILED(WaveOpenFile(lpFile,&cWave.m_hmmioIn,&cWave.m_pwfx,&cWave.m_ckInRiff )))
{
MessageBox(NULL, "Konnte Wave-Datei nicht öffnen!", "FEHLER", MB_OK);
return FALSE;
}
//und die Header-Informationen lesen
if (FAILED(WaveStartDataRead(&cWave.m_hmmioIn,&cWave.m_ckIn,&cWave.m_ckInRiff )))
{
MessageBox(NULL, "Konnte Wave-Datei nicht lesen!", "FEHLER", MB_OK);
return FALSE;
}
//Anhand des Formates der Wave-Daten nun einen Soundpuffer erzeugen
ZeroMemory(&dsbd, sizeof(DSBUFFERDESC));
dsbd.dwSize = sizeof(DSBUFFERDESC);
dsbd.dwFlags = DSBCAPS_STATIC | DSBCAPS_LOCSOFTWARE;
dsbd.dwBufferBytes = cWave.m_ckIn.cksize;
dsbd.lpwfxFormat = cWave.m_pwfx;
//nun den Soundpuffer mit dieser Beschreibung erzeugen
if (lpDSound->lpVtbl->CreateSoundBuffer(lpDSound,&dsbd,lplpDSBuff,NULL) != DS_OK)
{
MessageBox(NULL, "Konnte Soundpuffer nicht erzeugen", "FEHLER", MB_OK);
return FALSE;
}
//Der Soundpuffer existiert,Sounddaten der Wavedatei in einen seperaten Puffer lesen
if ((lpZBuff = (BYTE*)malloc(cWave.m_ckIn.cksize)) == NULL)
{
MessageBox(NULL,"Kein Speicher für Sounddaten","FEHLER",MB_OK);
return FALSE;
}
//die eigentlichen Sounddaten der Wav-Datei lesen
if (FAILED(WaveReadFile(cWave.m_hmmioIn,cWave.m_ckIn.cksize,lpZBuff,&cWave.m_ckIn,&realSize )))
{
MessageBox(NULL,"Fehler beim übertragen der WAVE-Daten", "FEHLER",MB_OK);
free(lpZBuff);
return FALSE;
}
//Wave-Datei schließen
mmioClose( cWave.m_hmmioIn, 0 );
//nun die Wave-Daten aus dem Bytepuffer in den Soundpuffer übertragen
//wie in DirectDraw müssen wir den Puffer sperren (LOCK) und erhalten einen
//Zeiger auf die Sounddaten - da kopieren wir die Wavedaten hin.
if ((*lplpDSBuff)->lpVtbl->Lock((*lplpDSBuff), 0, dsbd.dwBufferBytes, &lpvBuff1, &iSize1,
&lpvBuff2, &iSize2, 0) != DS_OK)
{
MessageBox(NULL, "Fehler bei Soundbuffer Lock", "FEHLER", MB_OK);
free(lpZBuff);
return FALSE;
}
//Bytes übertragen
memcpy(lpvBuff1,lpZBuff, iSize1);
//wenn iSize1 kleiner als die gesammte Pufferlänge dann zeigt lpvBuff2 auf den restlichen Teil
if (iSize1 < dsbd.dwBufferBytes)
{
//die restlichen Sounddaten übertragen
lpZBuff += iSize1; //Zeiger im Zwischenpuffer um die bereits kopierten Bytes nach vorn
memcpy(lpvBuff2, lpZBuff, iSize2);
}
//Soundpuffer wieder entsperren
(*lplpDSBuff)->lpVtbl->Unlock((*lplpDSBuff),lpvBuff1, iSize1, lpvBuff2, iSize2);
//zwischenspeicher wird nun nicht mehr gebraucht
free(lpZBuff);
return TRUE;
}
Nun sind wir so weit, dass wir unser kleines Programm dazu bringen können eine Wave-Datei abzuspielen. Da es nicht möglich ist das Ergebnis
hier darzustellen gibts nur einen Auschnitt der Änderungen in der
WinMain-Funktion und wie immer den fertigen Quelltext + einer kleinen
Wave-Datei als
Download(1.8 MB)
ohne WAV-Datei(7 KB)
BOOL APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
[...]
IsDSound = InitDirectSound(hwnd); //DirectSound initailisieren
if ((IsDSound && ReadWaveToBuff(&lpDSWork, "testdsbuff.wav") == TRUE)
{
//den Soundpuffer abspielen, wieder und wieder
lpDSWork->lpVtbl->Play(lpDSWork,0,0,DSBPLAY_LOOPING);
}
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//nach Beendigung des Programms, das Abspielen anhalten
if (lpDSWork != NULL) lpDSWork->lpVtbl->Stop(lpDSWork);
ExitDirectSound(); //DirectSound wieder abbauen
[...]
}