Autor Beitrag
tommie-lie
ontopic starontopic starontopic starontopic starontopic starofftopic starofftopic starofftopic star
Beiträge: 4373

Ubuntu 7.10 "Gutsy Gibbon"

BeitragVerfasst: Mo 10.02.03 17:31 
Das ist mein erstes Tutorial in der Richtung, also spiiieeeeeßt mich nicht auf! [Sid – IceAge]

Die Multimedia-API ist recht mächtig, wenn man sie versteht, richtig einzusetzen. Viele kennen nur den PlaySound-Befehl. Der ist auch gar nicht schlecht, aber ein bisschen unflexibel. Außerdem muss die Datei vollständig in den Speicher passen, was auch nicht gerade toll ist, wenn man versucht, einen Mediaplayer zu schreiben...
Mal ganz davon abgesehen, daß das dekomprimieren von exotischen Formaten, wie zum Beispiel Ogg Vorbis, nicht möglich ist, weil es keinen ACM-Codec für Ogg gibt. Auch eigene Equalizer-Funktionen sind nicht ohne weiteres Möglich, wenn man diese High-Level-API benutzt.


Dieses Tutorial soll die Funktionsweise der Multimedia-API und deren Benutzung zeigen. Ich will mich dabei auf die Low-Level-Waveform-Ebene konzentrieren, wer also was über MCI wissen will, ist hier schlecht bedient.

Sämtliche Codebeispiele, die im Laufe des Textes auftauchen werden, benötigen die VCL nicht. Es muss nur die Unit „mmsystem“ eingebunden sein, mehr nicht. Dieses Tutorial eignet sich also sowohl für VCL-Anwendungen, als auch für nonVCL-Anwendungen, ohne Einschränkungen. Nur Windows wird vorrausgesetzt, ab Win95.

Ich habe folgenden Aufbau geplant:

  • Aufbau einer Wavedatei
  • Benutzung der Multimediastreams
  • Funktionsweise der Waveformausgabe



Schauen wir uns zuerst einmal das RIFF-Waveform-Format an.
Eine RIFF-Datei besteht aus dem RIFF-Header und den verschiedenen Subchunks.
Der Header ist 8 Bytes lang und besteht aus dem RIFF-Identifier („RIFF“) und der verbleibenden Länge der Datei nach dem Header:
ausblenden Quelltext
1:
2:
3:
4:
struct {
   char  id[4];    // identifier string = "RIFF"
   DWORD len;      // remaining length after this header
} riff_hdr;


Dem RIFF-Header folgt ein 4-byte-langer File-Identifier. Bei Waveform-Dateien ist dieser „WAVE“.

Der Rest der Datei besteht aus so gennanten RIFF-Chunks.
Am Anfang jedes Chunks steht ein Chunk-Header, der insgesamt 8 Bytes lang ist:
ausblenden Quelltext
1:
2:
3:
4:
struct {    // CHUNK 8-byte header
   char  id[4];  // identifier, e.g. "fmt " or "data"
   DWORD len;   // remaining chunk length after header
} chunk_hdr;

Richtig, die 4 id-Bytes sind entweder „fmt „ oder „data“. Der Unterschied zwischen diesen beiden Typen ist, daß ein fmt-Chunk die Formatangaben (Samplerate, Kanäle, Bits per Sample, ...) enthält, während ein data-Chunk die eigentlichen Daten enthält.

In einem fmt-Chunk sind die Waveform-Formatdaten wie folgt angeordnet:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
struct{
   WORD  wFormatTag;         // Format category
   WORD  wChannels;          // Number of channels
   DWORD dwSamplesPerSec;    // Sampling rate
   DWORD dwAvgBytesPerSec;   // For buffer estimation
   WORD  wBlockAlign;        // Data block size
}

Man hat also, wenn alles gut geht, 14 Bytes an Formatdaten, die man verarbeiten muss. Doch dazu später...

wChannels beschreibt die Anzahl der Kanäle im Datenchunk. 1 steht also für Mono, 2 für Stereo.
dwSamplesPerSec ist die Anzahl an Samples, pro Sekunde. Standard ist 44.1kHz, alles andere ist aber auch Möglich. Je mehr Samples man in einer Sekunde abspielt, desto mehr Details werden gezeichnet. Kurze Knackser gehen bei einer zu geringen Samplerate also unter.
dwAvgBytesPerSec ist die Antahl an Bytes pro Sekunde. Mediaplayer können diese Zahl benutzen, um die größe des Pre-Read-Buffers zu bestimmen (beispielsweise um eine Sekunde „vorzulesen“, bevor die Daten abgespielt werden.
wBlockAlign ist die Größe eines Blockes. Ein Block ist ein Sample, in dem alle Kanäle enthalten sind. Bei 8bit Mono ist ein Block also ein Byte groß, bei 8bit Stereo ist er 2 Byte lang, usw.



wFormatTag beschreibt die Waveform-Ketegorie und kann folgende Werte annehmen:
ausblenden Quelltext
1:
2:
3:
4:
WAVE_FORMAT_PCM
FORMAT_MULAW
IBM_FORMAT_ALAW
IBM_FORMAT_ADPCM

Ersteres ist das Standardformat für Microsoft-Waveform-Dateien. Die letzten drei sind proprietäre Formate, alle von IBM.

Dannach ist das fmt-Chunk zuende und das data-Chunk beginnt, mit seinem 8-byte-Header, gefolgt von den Daten.
Die eigentlichen Daten beginnen also erst nach 34 Bytes. Zwischen den Chunks darf alles stehen. Einige Programme (wie zum Beispiel GoldWave) fügen daher am Ende jeder Datei, einen Suffix an, der eine Art Copyright darstellen soll. Das ist legitim, da am Ende der Datei der data-Chunk bereits zu Ende ist, die Daten also nicht mehr zu den Daten gehören. Daher ist in jedem Chunk auch die verbleibende Chunk-Länge angegeben, damit ein Programm nicht blind bis zum Ende liest und allem vertraut, was der Spezifikation entspricht.

Das war's eigentlich schon in Sachen RIFF-Waveform-Dateien. Wer sich in einem binären Editor mal eine Wavedatei anschaut, kann das eben besprochene nachvollziehen.





Kommen wir zum FileIO.
Dazu stellt Windows bereits den so genannten Multimedia Stream bereit, der sowohl gepufferten, als auch ungepufferten Lese- und Schreibzugriff auf Dateien erlaubt. Ich beschreibe ihr nur den ungepufferten Lesezugriff. Beim gepufferten Schreibzugriff hat man die Wahl, Windows den Puffer verwalten zu lassen, oder dies selber zu tun. Ersteres lässt sich durch ein simples Flag in der Create-Prozedur ändern, letzteres ist ein bisschen heikler. Mehr Infos findet man im Win32 SDK „Multimedia Programmer's Reference“ im Unterkapitel „File Input and Output“.


Microsoft hat den MMFIO speziell an die RIFF-Dateien angepasst, was uns sehr zu Gute kommt.

Ein Filestream ist vom Typ HMMIO und wird durch mmioOpen geöffnet:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
HMMIO mmioOpen(

    LPSTR szFilename,  
    LPMMIOINFO lpmmioinfo,  
    DWORD dwOpenFlags  
   );


Ein einfaches Delphi-Beispiel um eine Datei unbuffered lesend zu öffnen würde also wie folgt aussehen:
ausblenden Quelltext
1:
2:
3:
4:
5:
var
  Soundfile: HMMIO;


Soundfile := mmioOpen(PCHar('C:\mywave.wav'), nil, MMIO_READ);

Eine vollständige Liste aller Flags findet man im SDK. MMIO_ALLOCBUF würde einen Standard-Puffer verwenden, für außergewöhnliche Puffergrößen müsste man eine HMMIOINFO-Structure verwenden...

Nach dem Öffnen muss man selber dafür sorgen, daß die Datei wieder geschlossen wird. Der dazugehörige Befehl sieht wie folgt aus:
ausblenden Quelltext
1:
mmioClose(Soundfile, 0);					



Für den „normalen“ Gebrauch gibt es noch die Funktionen mmioRead, mmioSeek, mmioRename und mmioWrite, von denen wir jedoch jetzt keine brauchen. MmioRead wird ist später interessant.

Da wir ja an einer RIFF-Datei arbeiten wollen, können wir auch die Vorzüge der Multimediastreams benutzen und die speziellen RIFF-I/O-Befehle verwenden.

Zuerst müsste man die MMCKIFNO-Structure kennen:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
typedef struct {  
    FOURCC ckid; 
    DWORD  cksize; 
    FOURCC fccType; 
    DWORD  dwDataOffset; 
    DWORD  dwFlags; 
} MMCKINFO;

FOURCC kommt einem zuerst seltsam vor, ist es aber gar nicht. Es steht für Four-Character-Code und ist in Wirklichkeit eine Zahlendarstellung von 4 Buchstaben. Für die Umwandlung eines Strings in einen FCC gibt es die Funktion mmioStringToFOURCC(PChar Code, UINT wFlags), die einen FCC zurückliefert, den man verwenden kann.

Die Fields der MMCKINFO haben folgende Bedeutung:
ckid: Chunk-ID
ckSize: Chunkgröße
fccType: RIFF-Type (hier immer „WAVE“)
dwDataOffset: Offset zwischen Chunkdaten und Dateianfang
dwFlags: Flags, ohne weitere Bedeutung für uns

Da wir nur lesen wollen, brauchen wir noch die beiden Befehle mmioDescend und mmioAscend. MmioDescend „steigt“ in einen Chunk hinab, während mmioAscend diesen Chunk wieder verlässt und zum Parent-Chunk springt (es wird also der Chunk als Paramter übergeben, aus dem gestiegen werden soll, nicht der Parentchunk!).

Deklariert sind sie wie folgt:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
mmioAscend(
    HMMIO hmmio,  
    LPMMCKINFO lpck,  
    UINT wFlags  
);

mmioDescend(
    HMMIO hmmio,  
    LPMMCKINFO lpck,  
    LPMMCKINFO lpckParent,  
    UINT wFlags  
);


Die Flags sind dabei beim mmioAscend „reserved“, also nur beim mmioDescend benutzbar. Die Werte sind folgende:
MMIO_FINDCHUNK: Sucht nach einem Chunk mit entsprechender ckid
MMIO_FINDRIFF: Sucht nach einem RIFF-Chunk vom Typ lpck.fccType
MMIO_FINDLIST: Sucht nach einem LIST-Chunk


Man braucht in einem Programm also jeweils 2 MMCKINFOs. Das eine für den Parentchunk (RIFF), das andere für den jeweils aktuellen Subhcunk (fmt oder data).
Um zum fmt-Chunk zu gelangen, muss man folgendes machen:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
var
  File: HMMIO;
  ParentChunk: MMCKINFO;
  SubChunk: MMCKINFO;


File := mmioOpen(PChar('C:\myfile.wav'), nil, MMIO_READ);

ParentChunk.fccType := mmioStringToFourCC('WAVE', 0);
mmioDescend(File, @ParentChunk, nil, MMIO_FINDRIFF);

{ jetzt sind wir direkt hinter dem RIFF-Identifier, also im 12ten Byte der Datei }

SubChunk.ckid := bChunk.ckid := mmioStringToFourCC('fmt ', 0);
mmioDescend(File, @SubChunk, @ParentChunk, MMIO_FINDCHUNK);

Nachdem dieser Code ausgeführt wurde, befindet man sich direkt hinter dem fmt-Chunkheader. Die Daten, die jetzt gelesen würden wären also die Formatdaten der Audiodatei.


Das war die kleine Einführung in die Multimedia-File-I/O-API. Wer mehr darüber wissen will, zum Beispiel um Dateien aufzunehmen oder sich gar mit custom I/O procedures rumzuschlagen, oder einfach nur die Flags nochmal nachschauen will, findet alles im SDK.


Kommen wir endlich zum echten Waveaudio.
Die API ist so aufgebaut, daß die Waveform-Devices genauso gehandhabt werden wie eine Datei. D.h. Man öffnet sie, schreibt die Daten und schließt die Device wieder. Dabei werden Devicehandles benutzt, wie es bei Windows üblich ist.


Das wichtigste ist die HWAVEOUT-Klasse. Sie ist das Handle für eine WaveOutDevice. Analog ist die HWAVEIN-Klasse für WaveIn-Devices zuständig. Da wir hier die Wiedergabe lernen wollen, werden nur die WaveOut-Funktionen beschrieben. Hat man jedoch das Prinzip einmal verstanden, ist WaveIn genauso zu benutzen, da beide Devicetypen in der API identisch aufgebaut sind.

Ebenso wichtig wie ein Handle ist eine Formatstruktur. Im Laufe der Zeit gab es einige verschiedene davon, aber seit '95 ist die WAVEFORMATEX die aktuellste und auch wichtigste. Die anderen sind sozusagen Vorgänger der WAVEFORMATEX.
Deklariert ist sie folgendermaßen:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
typedef struct {  
    WORD  wFormatTag; 
    WORD  nChannels; 
    DWORD nSamplesPerSec; 
    DWORD nAvgBytesPerSec; 
    WORD  nBlockAlign; 
    WORD  wBitsPerSample; 
    WORD  cbSize; 
} WAVEFORMATEX;

Die Bedeutung der Felder lässt sich mit den vorher erlernten Kenntnissen und ein wenig Englisch leicht nachvollziehen.
Diese Struktur wird dazu verwendet, beim Öffnen der Audiodevice der Device mitzuteilen, welches Format sie zu unterstützen hat. So gibt es zum Beispiel Soundkarten mit nur einem Signalkanal, also nur Mono. In einem solchen Fall gibt es eine Fehlermeldung beim Öffnen der Device.

Wo wir grad' dabei sind, der Befehl zum Öffnen lautet, ganz nach MS-API-Logik (das ist das einzig anständige, was die Redmonder auf die Beine gestellt zu haben scheinen, für alles andere sind sie wohl zu... lassen wir das...) waveOutOpen:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
waveOutOpen(
    LPHWAVEOUT phwo,  
    UINT uDeviceID,  
    LPWAVEFORMATEX pwfx,  
    DWORD dwCallback,  
    DWORD dwCallbackInstance,  
    DWORD fdwOpen  
);


Öffnen wir also mal eine Audiodevice für eine Samplerate von 44.1kHz bei 16bit, Stereo:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
var
  Soundcard: PHWaveOut;
  Format: PWaveFormatEx;



Format := new(PWaveFormatEx);
with Format^ do
begin
  wFormatTag := WAVE_FORMAT_PCM;
  nChannels := 2;
  nSamplesPerSec := 44100;
  nAvgBytesPerSec := 176400; // 44100 * (16 / 8)
  nBlockAlign := 4;          // (16 * 2) / 8
   wBitsPerSample:=16;
end;

Soundcard := new(PHWaveOut);
if waveOutOpen(Soundcard, 0, Format, 0, 0, WAVE_FORMAT_QUERY) = 0 then
  waveOutOpen(Soundcard, WAVE_MAPPER, Format, Handle, 0, Callback_Window);

Was haben wir da gemacht?
Zuerst haben wir Speicher für den Pointer auf die Formatstruktur bereitgestellt. Dann haben wir die Formatstruktur vorbereitet, und zwar mit den Daten, die wir vorher bestimmt haben.
Dannach haben wir auch wieder Speicher für das Devicehandle bereitgestellt. Dann wird es etwas verwirrender. Der fdwOpen-Paramter in der waveOutOpen steht für die Flags, die der Funktion übergeben werden. WAVE_FORMAT_QUERY öffnet die Device nicht, sondern überprüft lediglich, ob das übergebene Format von der Device unterstützt wird. Hätte die Soundkarte keine Samplerate von 44.1kHz unterstützt, wäre also ncihts passiert. Der Rückgabewert sämtlicher waveOut...-Funktionen ist ein Fehlercode. Ist er 0, dann gab's keine Fehler.
Und in der echten Open-Funktion?
Mit dem vierten Paramter haben wir das Fensterhandel übergeben, als Flag „Callback_Window“. Der Soundtreiber benutzt Nachrichten um bestimmte Ereignisse an das Programm zu melden. Callback_Window veranlasst den Treiber, diese Nachrichten an ein Fenster zu schicken. Alternativen sind Callback_Procedure, Callback_Thread, Callback_NULL und Callback_Event. Bei jeder Variante muss an Stelle des Fensterhandles ein anderer Paramter übergeben werden. Im Falle von Callback_Procedure ein Pointer auf die Message-Prozedur, die die Nachrichten auswertet. Ich benutze das Fensterhandle, weil ich es für Demonstrationszwecke einfacher zu verwenden finde. Die Nachrichten, die zurückgegeben werden können sind WOM_OPEN, WOM_CLOSE, MM_WOM_OPEN, MM_WOM_CLOSE, WOM_DONE und MM_WOM_DONE. Die Bedeutung dürfte aufgrund der Namen eigentlich klar sein...


Wenn wir die Wavedevice geöffnet haben, müssen wir sie selbstverständlich auch wieder schließen. Das geht mit der Funktion mmioWaveOutClose.
Am besten schreibt man in das OnClose-Event der Hauptform folgenden Code:
ausblenden Quelltext
1:
wavOutClose(Soundcard^);					

Dadurch wird sichergestellt, daß die Soundkarte vor dem Beenden auf jeden Fall freigegeben wird und wir müssen uns keine weiteren Gedanken mehr darüber machen.

Aber nur durch das Öffnen gibt's noch lange keinen Ton. Bevor wir irgendwelche Audiodaten übergeben können, brauchen wir einen Soundheader. Er enthält die Daten, die Länge der Daten, den Loop-Status usw:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
typedef struct {  
    LPSTR  lpData; 
    DWORD  dwBufferLength; 
    DWORD  dwBytesRecorded; 
    DWORD  dwUser; 
    DWORD  dwFlags; 
    DWORD  dwLoops; 
    struct wavehdr_tag * lpNext; 
    DWORD  reserved; 
} WAVEHDR;

Wichtig für die Ausgabe sind nur lpData (ein Pointer auf die Daten), dwBufferLength (die Länge der Daten in Bytes), dwFlags und dwLoops.
Die Flags werden teilwiese von Windows gesetzt. Zum Beispiel ist ein Header, der das Flag WHDR_INQUEUE trägt, in der Warteschlange des Soundbuffers. Das Programm kann durch solche Informationen den Status des Blocks bestimmen, der vom Header getragen wird. Eine vollständige Liste der Flags findet man im SDK.

Nun haben wir alle Informationen, die wir brauchen, um Töne abzuspielen: Wie kennen die Messages, wir kennen die Structures, wir kennen die Funktionen.
Stellen wir uns also vor, wir hätten die Audiodaten schon gelesen und müssten sie jetzt nur noch abspielen:
ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
interface

type 
[...]
    procedure AfterPlay(var msg: TMessage); message MM_WOM_DONE;
    procedure Button1Click(Sender: TObject); 


var
  Form1: TForm1;
  Soundcard: PHWaveOut;
  Header: PWaveHdr;


implementation

procedure Tform1.AfterPlay(var msg: Tmessage);
begin
  waveOutUnPrepareHeader(Soundcard^, Header, sizeof(TWaveHdr));

  Dispose(Header);


  waveOutReset(Soundcard^);
  FreeMem(Soundcard);
end;


procedure TForm1.Button1Click(Sender: TObject);
var
  AudioFile: HMMIO;
  Format: pWaveFormatEx;
  ParentChunk: MMCKInfo;
  SubChunk: MMCKInfo;
  Filename: String;
begin
  Filename := 'C:\myfile.wav';
  AudioFile := mmioOpen(PChar(Filename), nil, MMIO_READ or MMIO_DENYWRITE);

  ParentChunk.fccType := mmioStringToFOURCC(PChar('WAVE'), 0);
  mmioDescend(AudioFile, @ParentChunk, nil, MMIO_FINDRIFF);

  SubChunk.ckid := mmioStringToFOURCC(PChar('fmt '), 0);
  mmioDescend(AudioFile, @SubChunk, @ParentChunk, MMIO_FINDCHUNK);


  Format := new(pWaveFormatEx);   // Audioformat festlegen
  mmioRead(AudioFile, PChar(Format), SubChunk.cksize);


  Soundcard := new(pHWaveOut);
  if waveOutOpen(Soundcard, 0, Format, 0, 0, WAVE_FORMAT_QUERY) = 0 then
    waveOutOpen(Soundcard, WAVE_MAPPER, Format, Handle, 0, Callback_Window);

  FreeMem(Format);


  // wieder zurück in den Parentchunk
  mmioAscend(AudioFile, @SubChunk, 0);



  // Blockheader vorbereiten
  Header := new(PWaveHdr);
  with Header^ do
  begin
    lpData := AudioData;  // Die Daten stellen wir uns vor
    dwBufferLength := bChunk.ckSize;   // die Daten sind so groß
    dwBytesRecorded := 0;
    dwUser := 0;
    dwFlags := 0;
    dwLoops := 0;
  end;

  // Header für die Karte erzeugen
  waveOutPrepareHeader(Soundcard^, Header, sizeof(WaveHdr));

  // und den Header in die Karte schreiben (Daten sind im Header)
  returnval := waveOutWrite(Soundcard^, Header, sizeof(WaveHdr));


  mmioClose(AudioFile, 0);

end;

Wir haben also zuerst die Formatdaten aus dem Stream gelesen. Der Grund dafür ist, daß in den Headerdaten bereits die Samplerate, Samplegröße, Blockgröße usw enthalten ist. Daher können wir der Read-Funktion einfach einen Pointer übergeben, der dann mit den Daten gefüllt wird. Wesentlich effektiver als das einlesen aller Werte hintereinander...
mmioAscend wird eigentlich nicht benötigt, da wir nichts mehr im Stream brauchen, aber ich habe es zu Demonstrationszwecken trotzdem im Code gelassen. Verdeutlicht wird, daß wir zuerst in den Parentchunk gegangen sind, dann in einen Subchunk und dann wieder aus diesem Subchunk hinaus. Der Dateipointer ist jetzt also wieder am Anfang des Parentchunks.
Erwähnenswert ist vielleicht noch, daß wir den Speicher für die Format-Struktur gleich wieder freigeben, nachdem wir die Soundkarte geöffnet haben. Die Struktur befindet sich dann nämlich bereits im Soundkartentreiber und wird nicht mehr benötigt. Wenn die Device also einmal geöffnet ist, brauchen wir die Formatdaten nachher nie wieder (es sei denn sie interessieren uns für einen anderen Zweck (Effekte)).



Jetzt haben wir aber noch lange keinen Ton!
Es fehlen die Daten, die wir im obigen Beispiel bereits mit AudioData bezeichnet haben. Es ist empfehlenswert, die Daten als Pchar zu deklarieren, weil im Header ein Pchar verlangt wird und mmioRead in einen Pchar liest, also bekommt man keine Konflikte mit den verschiedenen Typen.

Wie man liest, dürfte mittlerweile klar sein. Dazu muss in das data-Chunk gesucht werden und dann in eine Variable chunk.ckSize Bytes gelesen werden. Der Code dafür sieht so aus:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
var
  AudioFile: HMMIO;
  AudioData: Pchar;
  Parentchunk, Subchunk: HCKInfo;

begin
  AudioFile := mmioOpen(PChar('C:\myfile.wav'), nil, MMIO_READ or MMIO_DENYWRITE);

  ParentChunk.fccType := mmioStringToFOURCC(PChar('WAVE'), 0);
  mmioDescend(AudioFile, @ParentChunk, nil, MMIO_FINDRIFF);

  SubChunk.ckid := mmioStringToFOURCC(PChar('data'), 0);
  mmioDescend(AudioFile, @SubChunk, @ParentChunk, MMIO_FINDCHUNK);

  ReallocMem(AudioData, Subchunk.ckSize);
  mmioRead(AudioFile, AudioData, SubChunk.cksize);
end;

Nach diesem Code enthält AudioData sämtliche Audiodaten aus der Datei, und zwar in Form eines Pchars. Die Handhabung ist relativ einfach, wie man sieht, und da das lpData-Field des Headers ebenfalls ein Pchar sein muss, ist die Benutzung von Pchars gegenüber normalen String oder gar Integer-Arrays nur anzuraten.


Wenn man jetzt das Wissen kombiniert, erhält man folgenden Code, der eine Datei vollständig einliest und abspielt, unabhängig von der Dateigeometrie, nur Waveform muss es sein.
ausblenden volle Höhe Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
interface

type 
[...]
    procedure AfterPlay(var msg: TMessage); message MM_WOM_DONE;
    procedure Button1Click(Sender: TObject); 


var
  Form1: TForm1;
  Soundcard: PHWaveOut;
  Header: PWaveHdr;
  AudioData: PChar;


implementation

procedure Tform1.AfterPlay(var msg: Tmessage);
begin
  waveOutUnPrepareHeader(Soundcard^, Header, sizeof(TWaveHdr));

  Dispose(Header);


  waveOutReset(Soundcard^);
  FreeMem(Soundcard);
end;


procedure TForm1.Button1Click(Sender: TObject);
var
  AudioFile: HMMIO;
  Format: pWaveFormatEx;
  ParentChunk: MMCKInfo;
  SubChunk: MMCKInfo;
  Filename: String;
begin
  Filename := 'C:\myfile.wav';
  AudioFile := mmioOpen(PChar(Filename), nil, MMIO_READ or MMIO_DENYWRITE);

  ParentChunk.fccType := mmioStringToFOURCC(PChar('WAVE'), 0);
  mmioDescend(AudioFile, @ParentChunk, nil, MMIO_FINDRIFF);

  SubChunk.ckid := mmioStringToFOURCC(PChar('fmt '), 0);
  mmioDescend(AudioFile, @SubChunk, @ParentChunk, MMIO_FINDCHUNK);


  Format := new(pWaveFormatEx);   // Audioformat festlegen
  mmioRead(AudioFile, PChar(Format), SubChunk.cksize);


  Soundcard := new(pHWaveOut);
  if waveOutOpen(Soundcard, 0, Format, 0, 0, WAVE_FORMAT_QUERY) = 0 then
    waveOutOpen(Soundcard, WAVE_MAPPER, Format, Handle, 0, Callback_Window);

  FreeMem(Format);


  // wieder zurück in den Parentchunk
  mmioAscend(AudioFile, @SubChunk, 0);


  SubChunk.ckid := mmioStringToFOURCC(PChar('data'), 0);
  mmioDescend(AudioFile, @SubChunk, @ParentChunk, MMIO_FINDCHUNK);

  ReallocMem(AudioData, Subchunk.ckSize);
  mmioRead(AudioFile, AudioData, SubChunk.cksize);



  // Blockheader vorbereiten
  Header := new(PWaveHdr);
  with Header^ do
  begin
    lpData := AudioData;  // Die Daten stellen wir uns vor
    dwBufferLength := bChunk.ckSize;   // die Daten sind so groß
    dwBytesRecorded := 0;
    dwUser := 0;
    dwFlags := 0;
    dwLoops := 0;
  end;

  // Header für die Karte erzeugen
  waveOutPrepareHeader(Soundcard^, Header, sizeof(WaveHdr));

  // und den Header in die Karte schreiben (Daten sind im Header)
  returnval := waveOutWrite(Soundcard^, Header, sizeof(WaveHdr));


  mmioClose(AudioFile, 0);
end;

Zu beachten ist, daß wir die Variable Subchunk zweimal verwenden, und zwar für verschiedene Chunks. Das ist erlaubt, da wir das ja nacheinander machen, und wenn wir das Format haben und wieder aus dem fmt-Chunk ausgestiegen sind, brauchen wir die entsprechenden Daten ja nicht mehr, da die WaveFormatEx-Structure bereits gefüllt ist.


Damit sind wir auch am Ende des „kleinen“ Tutorials, das im O³-Writer 11 Seiten umfasst.
Am Anfang habe ich den Begriff Streaming erwähnt, das mit PlaySound nicht möglich ist.
Nun, ich habe ein wenig geflunkert, mit meinem Tutorial ist das auch nicht Möglich. Aber man hat das Wissen dazu. Header werden nämlich zunächst in eine Queue gestellt, die die Soundkarte abzuspielen hat. Man kann also mehrere Header auf einmal in die Soundkarte schreiben. Auf diese Art und Weise lässt sich ähnlich wie beim Grafik-Doubblebuffering ein Buffering der Header erzeugen. Man kann also mehrere Header-Variablen (diese Aufgabe schreit nach einem Array) verwenden, um kleine, gleichgroße Audiostücke in die Soundkarte zu schreiben. Microsoft empfiehlt 2 Header, aber da es nicht Möglich ist, mit nur 2 Headern einen vorzubereiten während der andere noch spielt, sollte man mehr nehmen, bei einer entsprechend handlichen Blocksize. Dazu muss man eine Art Schleife programmieren, die erst alle Blöcke füllt und in die Queue stellt. Durch das Callback kriegt man jedesmal mit, wenn der erste Block frei wird, den füllt man wieder und stellt ihn wieder in die Queue. So „kreisen“ die Blöcke immer und erzeugen einen konstanten Strom an Daten, die zur Soundkarte gelangen.


Man wird sich jetzt fragen, was diese low-level-API außer dem Streaming, das man auch über den TMediaPlayer erreichen kann, noch für Vorteile hat. Der wichtigste wäre, daß man eigene Komprimierungsalgorithmen benutzen kann, ohne daß man einen ACM-Codec braucht. So kann man Blöcke einlesen, die dekodieren und dann erst (unkomprimiert) an die Soundkarte schicken. Ein Beispiel wäre OggVorbis oder der lossless-Algorithmus Monkey Audio.
Außerdem kann man Die Blöcke verändern. Zum Beispiel kann man einen Equalizer, Distortion, Echo oder sonstige Soundeffekte benutzen, die weder mit PlaySound, noch mit dem TMediaPlayer hinbekommen würde.


Für Fragen, Hinweise, Ratschläge oder Morddrohungen bin ich natürlich über PM oder übers Forum erreichbar.


MfG
Thomas

_________________
Your computer is designed to become slower and more unreliable over time, so you have to upgrade. But if you'd like some false hope, I can tell you how to defragment your disk. - Dilbert