AnMaBaGiMa's Home

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 <windows.h> #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:
  1. Anzahl Kanäle (1 mono, 2 Stereo)
  2. Samples pro Sekunde (22kHz / 44kHz)
  3. 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.

Soundpufferring

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 [...] }