Sound Recording with Wave API

Zamrony P. Juhara

Introduction

Actually with TMediaPlayer, you can do sound recording and it's simple. For example:

FMediaPlayer.Filename:='test.wav';
FMediaPlayer.DeviceType:=dtWaveAudio;
FMediaPlayer.StartRecording;

but we don't have access to audio data currently being recorded, so if you want to develop professional multiple track sound recorder such as Cakewalk, TMediaPlayer is obviously not an option. Why? Because TMediaPlayer was built on the top of MCI (Media Control Interface). MCI is a generic interface for playback and recording multimedia.

To record audio in Windows, we can use DirectX or Windows's built-in Multimedia API. This article will explain how to do sound recording with wavein*** functions in Windows's Multimedia API.

Source code is available to download here

To understand how to play sound that you record, read Sound Playback with Wave API tutorial

In Delphi, multimedia API functions are declared in mmsystem.pas unit.

Opening Device

To be able to use sound card to record sound we need to open device for recording. Function we need to use is WaveInOpen().

function waveInOpen(lphWaveIn: PHWAVEIN; uDeviceID:UINT;
lpFormatEx: PWaveFormatEx;
dwCallback, dwInstance, dwFlags: DWORD): MMRESULT; stdcall;

If waveInOpen succeeds, this function returns handle (of type of HWAVEIN) in lphwavein. uDeviceID is device ID we are going to open. If we do not know what device to open, just use WAVE_MAPPER constant. lpFormatEx is our requested recording format. See "Recording Format".

dwCallback is callback mechanism that we use. Callback will be called evertytime when an event occured, for example when sound card is finished filling buffer with data, device is open and etc. This parameter is related with dwFlags flag.

If dwFlags=CALLBACK_NULL then no callback mechanism is used, if dwFlags=CALLBACK_FUNCTION then dwCallback must hold address of callback function. For info about callback, please see "Callback". If dwFlags=CALLBACK_WINDOW then dwCallback must hold handle of window to receive messages.

Messages are:

  • MM_WIM_OPEN = device is closed with waveinClose()
  • MM_WIM_CLOSE = device is open with waveinOpen()
  • MM_WIM_DATA = device is finished filling buffer with data.

There are other callback type, but usually CALLBACK_FUNCTION or CALLBACK_WINDOW are used more often.

dwInstance adalah user-defined data yang akan dikirim ke callback.

Recording Format

To set recording format, we use TWaveFormatEx record.

  tWAVEFORMATEX = packed record
wFormatTag: Word;
nChannels: Word;
nSamplesPerSec: DWORD;
nAvgBytesPerSec: DWORD;
nBlockAlign: Word;
wBitsPerSample: Word;
cbSize: Word;
end;
  • wFormatTag holds format type. For PCM (Pulse Code Modulation) format, we set with WAVE_FORMAT_PCM.
  • nChannels, number of channel used, 1=mono, 2=stereo.
  • nSamplePerSec, data sampling frequency. For PCM, we can use frequency 8000 Hz, 11025 Hz, 22050 Hz, 44100 Hz. Higher sample rate means higher sound quality and higher data size.
  • nAvgBytesPerSec, average data transfer speed needed. To calculate data transfer, we multiply nSamplesPerSec with nBlockAlign.
  • nBlockAlign is smallest data unit for a wave format. For PCM, nBlockAlign is nChannels*wBitsPerSample divided by 8 (1 byte=8 bit).
  • wBitsPerSample, number of bits for each data sample. For PCM, it must be 8 or 16. cbSize additional data size in bytes. For PCM, this field is ignored.

Sound Recording Callback

Callback function must be in following format:

procedure waveCallback(const handle:HWAVEIN; 
const
uMsg:integer;
dwInstance,dwParam1,dwParam2:cardinal);stdcall;

handle is handle of device we get via WaveInOpen() call.

uMsg is filled with message of event occured, i.e, one of following messages:

  • WIM_CLOSE = device is closed with waveInClose() call.
  • WIM_OPEN = device is open with waveinOpen() call.
  • WIM_DATA = device is finished filling buffer with data. Most important message is WIM_DATA.

Please note that we usestdcall directive. This directive is very important because callback will be called by device driver. With this directive, callback parameter passing will match parameter passing of Windows.

dwInstance contains user data (see dwInstance parameter in WaveInOpen()'s dwParam1 and dwParam2 message parameters).

We are not allowed to call waveIn*** functions in callback because it may cause deadlock to occur. For more information about deadlock, see "Deadlock".

Preparing Buffer

We need to setup buffer which will be filled with sound sample data. To prepare this buffer, we must allocate memory and then call waveInPrepareHeader().

function waveInPrepareHeader(hWaveIn: HWAVEIN; 
lpWaveInHdr: PWaveHdr;
uSize: UINT): MMRESULT; stdcall;

This function tells device driver about buffer that we are going to use to store recording data, such as how much buffer size and address of buffer, it also initializes internal resources. Parameter of this function is hWaveIn, handle of device recorder. lpWaveInHdr contains pointer to wave header data of type TWaveHdr. See"Wave Header Format" for explanation of this data structure. uSize is TWaveHdr size.

Wave Header Format

Wave header is data structure that we are going to use to hold address of buffer for recorded sound samples. Its type is TWaveHdr.

type
PWaveHdr = ^TWaveHdr;
{$EXTERNALSYM wavehdr_tag}
wavehdr_tag = record
lpData: PChar; { pointer to locked data buffer }
dwBufferLength: DWORD; { length of data buffer }
dwBytesRecorded: DWORD; { used for input only }
dwUser: DWORD; { for client's use }
dwFlags: DWORD; { assorted flags (see defines) }
dwLoops: DWORD; { loop control counter }
lpNext: PWaveHdr; { reserved for driver }
reserved: DWORD; { reserved for driver }
end;
TWaveHdr = wavehdr_tag;
{$EXTERNALSYM WAVEHDR}
WAVEHDR = wavehdr_tag;

lpData holds pointer to buffer. We will use this field quite often. dwBufferLength is size of buffer. dwBytesRecorded is actual recorded data size. dwBytesRecorded is equal or less dwBufferLength. dwUser is user-defined data. We can use it to store our own custom data. dwFlags holds wave header status. It can be one of following values:

  • WHDR_DONE. Sound card driver has finished filling buffer with data.
  • WHDR_PREPARED. Buffer has been prepared with waveInPrepareHeader() call.
  • WHDR_INQUEUE. Buffer is in queue, waiting to be filled with data.

dwLoops holds how many loop buffer is going to be played. For recording, this field is not relevant and should be ignored. lpNext holds pointer to next wave header record. reserved is reserved for future use, and should be set to 0.


Add Buffer to Driver

After we prepare buffer, we must add it to driver by using waveInAddBuffer().

function waveInAddBuffer(hWaveIn: HWAVEIN; 
lpWaveInHdr: PWaveHdr;
uSize: UINT): MMRESULT; stdcall;

Its parameters are same with waveInPrepareHeader().

Starting and Stopping Recording Process

Recording process is started by calling waveInStart(). To stop it, call waveInStop().

function waveInStart(hWaveIn: HWAVEIN): MMRESULT;stdcall;
function waveInStop(hWaveIn: HWAVEIN): MMRESULT;stdcall;

During recording, everytime buffer is full with data, our application will be notified via callback mechanism.

Reset Recording

waveInReset() is similar to waveInStop(). This function also stops recording just like waveInStop(). The difference is, all buffers in queue will be removed and marked as WHDR_DONE, waveInStop() only marks current buffer as WHDR_DONE, other buffers in queue remain unchanged.

function waveInReset(hWaveIn: HWAVEIN): MMRESULT; stdcall;

Deadlock

As we have mentioned before in "Callback", it is not recommended to call waveIn*** functions from inside callback. Function such as waveInStop(), will generate notification that cause our callback get called. If callback is called from inside callback itself, it causes uncontrolled recursive call. If you caught in a situation where your application is crashed inside callback, your code may experience this deadlock.

Recording Many Data

You may ask, how if we want to do very long recording? Do we need to provide an extremely hugh buffer? There's no need! The answer is to use multiple buffers.

With more than one buffer (double buffer or triple buffer or more), while first buffer is being filled with data, we can operate on data in second buffer. If first buffer is completely filled, we send second buffer to driver to be filled. We proceed with data in first buffer while waiting for second buffer to complete. This steps is repeated again and again until recording is stopped. This way, we can store many data with small buffer size.

We already have enough information on how to do sound recording with Wave API. Ok, let us continue with the implementation.


Recording Encapsulation Design

We are going to create class to wrap sound recording detail. Buffer management and handling sound recording notification will be encapsulated in this class. Application that will use this class, is responsible for deciding wave format, starting and stop ping sound recording and handling recorded data, if needed.

This class will use double buffer technique for sound recording, because of that, we need to prepare two buffers of 4 KB sizes each. Every time buffer completely filled, this class will generate event to notify application that buffer is ready. How application handles data is completely application's responsibility.

Because sound recording and sound playback with Wave API consists very similar steps, we will create structure of inheritance class that will make further improvement and maintenance easier. We name class that will encapsulate sound recording as TSoundRecorder.

This is class hierarchy:

sound_recorder_uml_diagram

Fig.1 UML diagram of sound recorder.

TSoundObject

TSoundObject class is base class for child class. This class declares important methods, i.e, Open, Close, Start and Stop. All of it are abstract methods, because their implementation is not yet defined and may differ. For example to open device for playback will differ from open device for recording. In TSoundObject, we will add property, Handle, tostore handle of device. Following code snippet is delaration of this class.

  TSoundObject=class(TObject)
private
protected
FHandle:THandle;
public
procedure
Open;virtual;abstract;
procedure Close;virtual;abstract;
procedure Start;virtual;abstract;
procedure Stop;virtual;abstract;
published
property
Handle:THandle read FHandle;
end;

TWaveObject

TWaveObject is base class for sound playback or recording waveform. TWaveObject class declares method called WaveProc, i.e, callback that will be called when driver is finished with samples data. This procedure is made as abstract . Class derived from TWaveObject should implement it because callback for playback and sound recording may differ.

Sound playback and recording need buffer to store wave format. Therefore, in this class, we declare internal variable of type of TWaveFormatEx. Child class can use this variabel to store wave format. TWaveObject is provided with property to manage wave format. Please note, because we always record PCM data, nBlockAlign and nAvgSamplesPerSec can be calculated from nChannel, nSamplesPerSec and nBitsPerSample, thus only these fields will be available through TWaveObject property.

  TWaveObject=class(TSoundObject)
private
procedure
SetupWaveFormat;
procedure SetBitsPerSample(const Value: word);
procedure SetChannel(const Value: word);
procedure SetSamplePerSec(const Value: cardinal);
protected
FWaveFormat:TWaveFormatEx;

procedure WaveProc(const handle:HWAVEIN;
const msg:UINT;
const dwInstance:cardinal;
const dwParam1,dwParam2:cardinal);virtual;abstract;
public
constructor
Create;virtual;
published
property
Channel:word read FWaveFormat.nChannels write SetChannel;
property SamplePerSec:cardinal read FWaveFormat.nSamplesPerSec write SetSamplePerSec;
property BitsPerSample:word read FWaveFormat.wBitsPerSample write SetBitsPerSample;
end;

TWaveObject has Create constructor. This constructor will initialize wave format to default values, i.e, sampling frequency of 11025 Hz, mono 8 bit.


TSoundRecorder

TSoundRecorder is complete class that will implement all abstract methods derived from its ancestor, i.e, Open, Close, Start, Stop and WaveProc. Because this class utilizes double buffer technique for sound recording, we create SwapBuffers method for swapping buffers to send to driver. To meet our specification, TSoundRecorder only acts as data delivery system, thus, application is responsible to handle data sent to it. Therefore, we need to tell application when there is data available to handle. We add OnDataAvail event as shown in following code snippet.

  
TDataAvailEvent=procedure(sender:TObject;
const Buffer:pointer;
const BufferSize:cardinal;
const BytesRecorded:cardinal) of object;


OnDataAvail sends Sender which is instance of TSoundRecorder. Buffer contains recorded data, BufferSize holds size of Buffer and BytesRecorded contains actual size of recorded data in Buffer. BytesRecorded is equal or less than BufferSize. Application is not allowed to modify or free Buffer memory, because its lifetime is managed by TSoundRecorder.
  TSoundRecorder=class(TWaveObject)
private
FBuffer1,FBuffer2,FCurrentBuffer:PWaveHdr;
FRecording: boolean;
FOnDataAvail: TDataAvailEvent;
procedure SetOnDataAvail(const Value: TDataAvailEvent);
procedure SwapBuffers;
protected
procedure
DoDataAvail(const Buffer:pointer;
const BufferSize:cardinal;
const BytesRecorded:cardinal);virtual;
procedure WaveProc(const handle:HWAVEIN;
const msg:UINT;
const dwInstance:cardinal;
const dwParam1,dwParam2:cardinal);override;
public
constructor
Create;override;
destructor Destroy;override;
procedure Open;override;
procedure Close;override;
procedure Start;override;
procedure Stop;override;
published
property
Recording:boolean read FRecording;
property OnDataAvail:TDataAvailEvent read FOnDataAvail write SetOnDataAvail;
end;

Other important property is Recording property that store status of recording process, whether it currently runs or stops.


Constructor/Destructor

Because buffer lifetime is handled internally, constructor and destructor are fleshed out with code to allocate and deallocate buffer as shown in following code:

constructor TSoundRecorder.Create;
begin
inherited
;
new(FBuffer1);
ZeroMemory(FBuffer1,sizeOf(TWaveHdr));
GetMem(FBuffer1.lpData,MAX_BUFFER_SIZE);
FBuffer1.dwBufferLength:=MAX_BUFFER_SIZE;

new(FBuffer2);
ZeroMemory(FBuffer2,sizeOf(TWaveHdr));
GetMem(FBuffer2.lpData,MAX_BUFFER_SIZE);
FBuffer2.dwBufferLength:=MAX_BUFFER_SIZE;
end;

destructor TSoundRecorder.Destroy;
begin
Close;
FreeMem(FBuffer1.lpData,MAX_BUFFER_SIZE);
FreeMem(FBuffer2.lpData,MAX_BUFFER_SIZE);
dispose(FBuffer1);
dispose(FBuffer2);
inherited;

end;
where MAX_BUFFER_SIZE is declared as
const MAX_BUFFER_SIZE=4*1024;

Open Implementation

This method is called to open recording device.

procedure TSoundRecorder.Open;
var ahandle:HWAVEIN;
status:MMResult;
statusStr:string;

begin
if
Handle=0 then
begin
aHandle:=0;
status:=waveInOpen(@aHandle,
WAVE_MAPPER,
@FWaveFormat,
cardinal(@_WaveInCallback),
cardinal(self),
CALLBACK_FUNCTION);
FHandle:=aHandle;
if Handle=0 then
begin
setlength(statusStr,MAXERRORLENGTH);
waveInGetErrorText(status,pChar(statusStr),
MAXERRORLENGTH);
raise ESndError.Create(statusStr);
end;

WaveInPrepareHeader(Handle,FBuffer1,sizeof(TWaveHdr));
WaveInPrepareHeader(Handle,FBuffer2,sizeof(TWaveHdr));
end;
end;

First step is to check whether Handle is null. If equal, we do opening device steps. We send address of wave format we want, also callback address that will process data. In this class, callback is function called _WaveInCallback. It is appended with underscore to force convenstion that this function should not be called directly by application.

We also send address of instance of TSoundRecorder to callback. We will discuss why we send it, in "Callback Implementation Callback" topic. If it failed (Handle=0), we generate ESndError exception that is derived from Exception. Then we prepare wave header. Note that we only need to prepare header once as long as buffer is remain unchanged. If it is changed, for example if it is resized, we need to do header preparation again. For TSoundBuffer class, buffer size is never changed.


Callback Implementation

As what already mentioned, _WaveInCallback is callback of recording. Code below is its implementation:

procedure _WaveInCallback(const handle:HWAVEIN;
const msg:UINT;
const dwInstance:cardinal;
const dwParam1,dwParam2:cardinal);stdcall;
begin
TSoundRecorder(dwInstance).WaveProc(handle,
msg,
dwInstance,
dwParam1,
dwParam2);
end;

Note that we typecast dwInstance to TSoundRecorder. This is safe to do because, when we call waveInOpen, dwInstance parameter of waveInOpen is set with Self. Then we call WaveProc of TSoundRecorder, real callback that will handle it further. You may ask why we send _WaveInCallback address? Why not WaveProc address? This problem is due to parameter passing difference of ordinary function or procedure with class method.

In class method, when a method is called, register eax is filled with Self, then first parameter is copied to edx and second parameter to ecx. Windows parameter passing does not copy Self address. With this technique, we can use callback that is a class method. Following code is implementation of WaveProc

procedure TSoundRecorder.WaveProc(const handle:HWAVEIN;
const msg:UINT;
const dwInstance:cardinal;
const dwParam1,dwParam2:cardinal);
var wavehdr:PWaveHdr;

begin
case
msg of
WIM_DATA:begin
if
FRecording then
begin
waveHdr:=PWaveHdr(dwParam1);
SwapBuffers;
DoDataAvail(waveHdr.lpData,
waveHdr.dwBufferLength,
waveHdr.dwBytesRecorded);
end;
end;
end;

end;
we check, whether msg contains WIM_DATA. If yes, then we check, if it is still recording , we typecast dwParam1 to PWaveHdr. We swap buffer by calling SwapBuffers, and then we send buffer address and its size to application by generating OnDataAvail event.
procedure TSoundRecorder.DoDataAvail(const Buffer: pointer;
const BufferSize, BytesRecorded: cardinal);
begin
if
Assigned(FOnDataAvail) then
FOnDataAvail(self,Buffer,BufferSize,BytesRecorded);
end;
SwapBuffers method is implemented as following code:
procedure TSoundRecorder.SwapBuffers;
begin
if
FCurrentBuffer=FBuffer1 then
FCurrentBuffer:=FBuffer2
else
FCurrentBuffer:=FBuffer1;

WaveInAddBuffer(Handle,FCurrentBuffer,sizeof(TWaveHdr));
end;

Start Recording Implementation

Before we start recording, ensure that there is no recording currently running by calling Stop().

procedure TSoundRecorder.Start;

begin
if
Handle<>0 then
begin
Stop;

FCurrentBuffer:=FBuffer1;
WaveInAddBuffer(Handle,FBuffer1,sizeof(TWaveHdr));
waveInStart(Handle);
FRecording:=true;
end;
end;

We send first buffer and start recording by calling WaveInStart().

Stop Recording Implementation.

procedure TSoundRecorder.Stop;

begin
if
Handle<>0 then
begin
waveInStop(Handle);
FRecording:=false;
end;
end;

Close Device.

What we have open should be closed to free its resources.

procedure TSoundRecorder.Close;

begin
if
Handle<>0 then
begin
Stop;
waveInUnPrepareHeader(Handle,FBuffer1,Sizeof(TWaveHdr));
waveInUnPrepareHeader(Handle,FBuffer2,Sizeof(TWaveHdr));
waveInClose(Handle);
FHandle:=0;
end;
end;

If we currently records sound, we stop it first. Then buffers is unprepared and device is closed. Please note that we must unprepare buffer before buffer is freed. If we free buffer before unprepare it, device driver might be still accessing .By doing unpreparation, basically we tell device driver to stop accessing it because we are going to free it.


Main Application

To make it full circle, we will discuss how to use TSoundRecorder class in an application. Create new project application. Drag and drop four buttons and rename it as btnStart, btnStop, btnSave and btnPlay. Create handler for OnClick as shown below::

{=========================
(c) 2006 zamrony p juhara
=========================}
unit ufrmMain;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,mmsystem,uSound,XPMan;

type
TfrmMain = class(TForm)
btnStart: TButton;
btnStop: TButton;
btnPlay: TButton;
btnSave: TButton;
SaveDialog1: TSaveDialog;
procedure btnPlayClick(Sender: TObject);
procedure btnStartClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
private
FRecordedStream,FWaveStream:TMemoryStream;
FSoundRecorder:TSoundRecorder;
procedure DataAvail(sender:TObject; const Buffer:pointer;
const BufferSize,bytesRecorded:cardinal);
procedure SaveWaveToStream(recorded,stream:TStream);
public
constructor
Create(Owner:TComponent);override;
destructor Destroy;override;
end;


var
frmMain: TfrmMain;

implementation

{$R *.dfm}

procedure TfrmMain.btnPlayClick(Sender: TObject);
begin
FWaveStream.Clear;
SaveWaveToStream(FRecordedStream,FWaveStream);
PlaySound(FWaveStream.Memory,0,SND_MEMORY OR SND_ASYNC);
btnSave.Enabled:=true;
end;

constructor TfrmMain.Create(Owner: TComponent);
begin
inherited
;
FWaveStream:=TMemoryStream.Create;
FRecordedStream:=TMemoryStream.Create;
FSoundRecorder:=TSoundRecorder.Create;
FSoundRecorder.OnDataAvail:=DataAvail;
FSoundRecorder.Open;
end;


procedure TfrmMain.DataAvail(sender: TObject; const

Buffer:pointer;const BufferSize,
bytesRecorded: cardinal);
begin
FRecordedStream.WriteBuffer(Buffer^,BytesRecorded);
end;

destructor TfrmMain.Destroy;
begin
FWaveStream.Free;
FRecordedStream.Free;
FSoundRecorder.Free;
inherited;
end;


procedure TfrmMain.btnStartClick(Sender: TObject);
begin
FSoundRecorder.Start;
FRecordedStream.Clear;
btnStop.Enabled:=true;
btnStart.Enabled:=false;
btnSave.Enabled:=false;
end;

procedure TfrmMain.btnStopClick(Sender: TObject);
begin
FSoundRecorder.Stop;
btnStop.Enabled:=false;
btnStart.Enabled:=true;
btnPlay.Enabled:=true;
end;

procedure TfrmMain.SaveWaveToStream(recorded, stream: TStream);
var waveformatex:TWaveFormatEx;
datacount,riffcount,tempInt:integer;
const
RiffId: string = 'RIFF';
WaveId: string = 'WAVE';
FmtId: string = 'fmt ';
DataId: string = 'data';


begin
with
WaveFormatEx do
begin
wFormatTag := WAVE_FORMAT_PCM;
nChannels := FSoundRecorder.Channel;
nSamplesPerSec := FSoundRecorder.SamplePerSec;
wBitsPerSample := FSoundRecorder.BitsPerSample;
nBlockAlign := (nChannels * wBitsPerSample) div 8;
nAvgBytesPerSec := nSamplesPerSec * nBlockAlign;
cbSize := 0;
end;
datacount:=recorded.Size;
{hitung panjang data sound dan panjang stream WAV yang harus dihasilkan}
RiffCount := Length(WaveId) + Length(FmtId) + SizeOf(DWORD) +
SizeOf(TWaveFormatEx) + Length(DataId) + SizeOf(DWORD) +

dataCount;
{tulis wave header}
stream.WriteBuffer(RiffId[1], 4); // 'RIFF'
Stream.WriteBuffer(RiffCount, SizeOf(DWORD)); // file data size
Stream.WriteBuffer(WaveId[1], Length(WaveId)); // 'WAVE'
Stream.WriteBuffer(FmtId[1], Length(FmtId)); // 'fmt '
TempInt := SizeOf(TWaveFormatEx);
Stream.WriteBuffer(TempInt, SizeOf(DWORD)); // TWaveFormat data size
Stream.WriteBuffer(WaveFormatEx, SizeOf(TWaveFormatEx)); //

WaveFormatEx record
Stream.WriteBuffer(DataId[1], Length(DataId)); // 'data'
Stream.WriteBuffer(datacount, SizeOf(DWORD)); // sound data size

recorded.Seek(0,soFromBeginning);
Stream.CopyFrom(recorded,dataCount);

end;

procedure TfrmMain.btnSaveClick(Sender: TObject);
var fstream:TFileStream;
begin
if
SaveDialog1.Execute then
begin
fstream:=TFileStream.Create(SaveDialog1.Filename,fmCreate);
try
FWaveStream.Position:=0;
fstream.CopyFrom(FWaveStream,0);
finally
fstream.Free;
end;
end;

end;

end.

In this application, we create two buffer, one for store recorded data and the other to store buffer in WAV format. We need WAV format buffer because we are going to use PlaySound for playback. It's not too hard. Constructor Create will allocate instances of TMemoryStream and TSoundRecorder and open recorder device. Destroy deallocates all of it. What we need to discuss is creating WAV format. Data sent via DataAvail is raw data. To enable PlaySound to play it we need to convert it to valid WAV format.


Short Description of WAV Format

WAV format consist of blocks of data, i.e, outer block is RIFF block.

RIFF Block

  • ['RIFF'][size of RIFF block (4 bytes)] [WAVE block]
    • WAVE Block
      • [WAVE block] contains data:
        • ['WAVE'][Format block][Data block]
          • Format Block
            • ['fmt '][size of following TWaveFormatEx block (4 bytes)][TWaveFormatEx]
          • Data Block
            • ['data'][size of following data block (4 bytes)][sample data]

Above explaination about WAV format, might be little bit confusing, but by examining SaveWaveToStream code, I think it would not be too difficult to understand.

Source code is available to download here. OK, that's all.