Sound Playback with Wave API

Zamrony P. Juhara

In previous article, Sound Recording with Wave API, we have discussed how to utilize Wave API for sound recording. In this topic, we are going to discuss how to play WAV with Wave API. If you don't need access to wave data currently played and only concern about playing WAV file, information in this article may not suit your need. For playing WAV only, you can use PlaySound() or TMediaPlayer. If you need access to wave data, for example, to be able to change wave data by applying filter and effects to wave data, then this article is for you.

WaveOut***

Wave playback functions in Wave API, using naming convention waveOut***, for example, waveOutOpen(), waveOutClose(), etc. We will discuss these functions and how to use them soon. They are declared in MMSystem.pas unit.

Opening Waveform Output Device.

This is the first step you must do to do sound playback.

function waveOutOpen(lphWaveOut: PHWaveOut; uDeviceID: UINT;
lpFormat: PWaveFormatEx; dwCallback, dwInstance, dwFlags: DWORD): MMRESULT; stdcall;

lphWaveOut parameter is variable that will holds handle of output device, HWAVEOUT, that we will use to access waveform audio output device. If we set it to nil, dwFlags must be set. uDevice ID is identifier of waveform audio output device. If you don't know what device ID to use, simply use WAVE _MAPPER constant to let waveOutOpen choose it for us. lpFormat points to TWaveFormatEx data structure which define wave format we are going to play. dwCallback holds address of callback, event handle or window handle that will receive notification when something happened during playback. dwInstance is user data that will be passed into the callback function. dwFlags tells Wave API about callback type we want. You can use one of following flags:

  • CALLBACK_NULL
    Callback mechanism is not used.
  • CALLBACK_FUNCTION
    Callback is a function. If we use this flag, dwCallback must point to callback function address. We are going to discuss about this soon.
  • CALLBACK_WINDOW
    Callback is window handle. If we use this flag, dwCallback must hold a valid window handle.
  • CALLBACK_THREAD
    Callback is thread. dwCallback must holds valid thread ID.
  • CALLBACK_EVENT
    Callback is handle event. dwCallback holds handle event created with CreateEvent().
  • WAVE_FORMAT_QUERY
    If we use this flag, we instruct waveOutOpen to check whether output device is capable to play wave format that we specify, but output device is not open for playback.

Please note that, there are other flags available for waveoutOpen, but I don't discuss it here, because I think, it is not be used very often.

If we succeed, waveOutOpen would return MMSYSERR_NOERROR. Otherwise, it might return error code WAVERR_BADFORMAT for unsupported wave format and etc.

Wave Format

Wave format is defined with TWaveFormatEx dat structure which declared as following:

  PWaveFormatEx = ^TWaveFormatEx;
{$EXTERNALSYM tWAVEFORMATEX}
tWAVEFORMATEX = packed record
wFormatTag: Word; { format type }
nChannels: Word; { number of channels (i.e. mono, stereo, etc.) }
nSamplesPerSec: DWORD; { sample rate }
nAvgBytesPerSec: DWORD; { for buffer estimation }
nBlockAlign: Word; { block size of data }
wBitsPerSample: Word; { number of bits per sample of mono data }
cbSize: Word; { the count in bytes of the size of }
end;

wFormatTag holds wave format type. To play PCM wave which is wave for Windows, we set WAVE_FORMAT_PCM. nChannels holds number of channel, 1 for mono and 2 for stereo. nSamplesPerSec is number of samples per second in Hertz. For WAVE_FORMAT_PCM, typical values are 8000 Hz, 11025 Hz, 22050 Hz and 44100 Hz. nAvgBytesPerSec, holds average required data transfer. For WAVE_FORMAT_PCM, it is a product of nSamplesPerSec and nBlockAlign. nBlockAlign holds data block size.

Callback

There are many type of callback, I use function callback a lot. Function callback must be in the form of following format:

procedure WaveOutProc(Handle:HWAVEOUT;uMsg:UINT;
dwInstance:DWORD;
dwParam1,dwParam2:DWORD);stdcall;

Handle is wave out handle we get via waveOutOpen(). uMsg is type of message, i.e WOM_OPEN, WOM_CLOSE and WOM_DONE. We are going to use WOM_DONE very often. This message tells us that device driver is complete processing data. dwInstance is our user data passed when we called waveOutOpen(), dwParam1 and dwParam2 is parameters. When we receive WOM_DONE, dwParam1 will hold pointer to data structure being played.

Following snippet shows hwo to open device for playback with function callback where function's name is _WaveOutProc.

procedure open_wave;
var awaveFormat:TWaveFormatEx;
begin
//siapkan format wave
aWaveFormat.wFormatTag:=WAVE_FORMAT_PCM;
aWaveFormat.wBitsPerSample:=8;
awaveFormat.nChannels:=2;
aWaveFormat.nSamplesPerSec:=22050;
aWaveFormat.nBlockAlign:=awaveFormat.nChannels*aWaveFormat.wBitsPerSample div 8;
aWaveFormat.nAvgBytesPerSec:=aWaveFormat.nSamplesPerSec*aWaveFormat.nBlockAlign;
aWaveFormat.cbSize:=0;


if (WaveOutOpen(@FHandle,WAVE_MAPPER,
@awaveFormat,
cardinal(@_WaveOutProc),
cardinal(Self),
CALLBACK_FUNCTION)<>MMSYSERR_NOERROR) then
begin
raise
Exception.Create('Gagal membuka sound device');
end;

end;

Following snippet tests whether wave format of 8 bit stereo, sample rate 22,05 KHz can be played by waveform audio output device.

function isSupportedFormat:boolean;
var awaveFormat:TWaveFormatEx;
begin
//siapkan format wave
aWaveFormat.wFormatTag:=WAVE_FORMAT_PCM;
aWaveFormat.wBitsPerSample:=8;
awaveFormat.nChannels:=2;
aWaveFormat.nSamplesPerSec:=22050;
aWaveFormat.nBlockAlign:=awaveFormat.nChannels*aWaveFormat.wBitsPerSample div 8;
aWaveFormat.nAvgBytesPerSec:=aWaveFormat.nSamplesPerSec*aWaveFormat.nBlockAlign;
aWaveFormat.cbSize:=0;


result:=(WaveOutOpen(nil,WAVE_MAPPER,
@awaveFormat,
0,
0,
WAVE_FORMAT_QUERY)=MMSYSERR_NOERROR);
end;

Preparing Buffers

We need to set up buffers that will holds wave data. We are responsible for managing memory for this buffer. We must tell device driver about this buffers by using waveOutPrepareHeader() before we use it to send wave data to device driver.

function waveOutPrepareHeader(hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr;
uSize: UINT): MMRESULT; stdcall;

hWaveOut is handle wave out, lpWaveOutHdr holds our buffers. uSize is size of wave header. Following code snippet is declaration of PWaveHdr.

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;

For WAV playback, the most important field islpData which holds buffer address. Size of buffer is kept in dwBufferLength. dwBytesRecorded is not used. It is only for recording. dwUser can be used to passed user data. dwFlags holds bufer status information.

Fill Buffer with Data

Filling buffer with waveform data is our responsibility. To fill buffer, you can use data copying function like Move() or CopyMemory(). What you must remember, if size of data that we copy in buffer is smaller than actual buffer size, dwBufferLength must be set with the size of data. Please note that you must not change buffer size. If you need to change buffer size, you must tell this change to device driver. First you must call waveOutUnPrepareHeader() (we will discuss it later), change buffer size and call waveOutPrepareHeader() again with new buffer size.

Sending Buffer To Device Driver

After buffer filled with data, we are ready to play it. To play it, we call waveOutWrite().

function waveOutWrite(hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr;
uSize: UINT): MMRESULT; stdcall;

lpWaveOutHdr is wave header that we have been prepared previously. uSize is wave header size. When we send first data block to device driver, playback is started. We can send all WAV data to device driver by calling waveOutWrite once, but please note that, on some soundcards (especially the old ones), maximum buffer size can be processed is 64 KB, so if you have waveform data bigger than 64 KB, waveOutWrite must be called more than once with smaller data blocks.

Other problem may rise regarding playing smaller data blocks is data supply to device driver must be done continously. Otherwise, sound will be jerky becuase of gap between speed of soundcard data processing and speed of our application data supply. From my experience, this gap may rise because of the use of too small buffer size. Small buffer makes soundcard finish processing data faster, much faster than our application data supply to device driver. For our class that we are going to develop, Wave data will be separated into small data blocks of size of 64 KB.

Stop Playback

waveOutReset() we use to stop playback.

function waveOutReset(hWaveOut: HWAVEOUT): MMRESULT; stdcall;

Pause/Resume Playback

To pause playback we use waveOutPause() and to resume playback we use waveOutRestart().

Freeing Buffer

After we finish, before we free buffer make sure we call waveOutUnPrepareHeader() to let device driver knows that we are going to free it. By calling waveOutUnPrepareHeader(), device driver is told to not use it again. After unpreparing, buffer can be destroyed safely.

function waveOutUnprepareHeader(hWaveOut: HWAVEOUT; lpWaveOutHdr: PWaveHdr;
uSize: UINT): MMRESULT; stdcall;

Close Device

Our applcation must be polite toWindows, resource we used we must return it back by closing waveform audio output device.

function waveOutClose(hWaveOut: HWAVEOUT): MMRESULT; stdcall;

Steps above are basic steps to play wave with Wave API. There ara some additional functions available regarding wave play back. I will discuss how to get playback progress, setting up volume and get error status.

Get Playback Progress

Status playback position can be queried with waveOutGetPosition(). We only can get get playback position but we cannot change it.

function waveOutGetPosition(hWaveOut: HWAVEOUT; 
lpInfo: PMMTime;
uSize: UINT): MMRESULT; stdcall;

lpInfo will be fiiled with playback position information, there are many formats for playback position. Before calling this function we must set its format. uSize is size of lpInfo. Declaration of PMMTime is as following:

{ MMTIME data structure }
type
PMMTime = ^TMMTime;
{$EXTERNALSYM mmtime_tag}
mmtime_tag = record
case
wType: UINT of { indicates the contents of the variant record }
TIME_MS: (ms: DWORD);
TIME_SAMPLES: (sample: DWORD);
TIME_BYTES: (cb: DWORD);
TIME_TICKS: (ticks: DWORD);
TIME_SMPTE: (
hour: Byte;
min: Byte;
sec: Byte;
frame: Byte;
fps: Byte;
dummy: Byte;
pad: array[0..1] of Byte);
TIME_MIDI : (songptrpos: DWORD);
end;
TMMTime = mmtime_tag;
{$EXTERNALSYM MMTIME}
MMTIME = mmtime_tag;

Field wType must be set. Valid value are TIME_BYTES to get playback position in unit of bytes .If we useTIME_BYTES data field cb will contain position information. TIME_MS to get playback position in unit of miliseconds. For this value field ms will contain data we need. For our classes, we only use these two values.

Get Error Message

To get error message from error code returned by waveOut***, we use waveOutGetErrorText(). lpText must point to buffer that will recieve error message. uSize holds size of the buffer.
function waveOutGetErrorText(mmrError: MMRESULT; 
lpText: PChar;
uSize: UINT): MMRESULT; stdcall;

Set up Playback Volume

Left and right speaker volume can be set via waveOutSetVolume(). To query volume information, we use waveOutGetVolume.

function waveOutGetVolume(hwo: HWAVEOUT; lpdwVolume: PDWORD): MMRESULT; stdcall;

function waveOutSetVolume(hwo: HWAVEOUT; dwVolume: DWORD): MMRESULT; stdcall;

Left and right volume are combined into one value i.e lpdwVolume dan dwVolume. Left volume is low word of dwVolume and right volume is high word.

Creating TSoundPlayer

Design

TSoundPlayer is wrapper class for waveOut*** functionalities. This class is derived from TWaveObject (discussion of this class can be found in Sound Recording with Wave API article). Abstract method Open, Close, Start, Stop are overriden. Constructor and destructor are overriden with buffer allocation/deallocation code, we also add method to pause anda resume playback.

We add some properties to adjust left and right volume, playback position and event handler property which will be generated when class instance need more data to send to device driver.

Implementation

TSoundPlayer is declared in same unit with TSoundRecorder class (Sound Recording with Wave API) i.e usound.pas

  TSoundPlayer=class(TWaveObject)
private
FBuffer1,FBuffer2,FCurrentBuffer:PWaveHdr;

FPlaying: boolean;
FOnDataRequired: TDataRequiredEvent;
FLeftVolume,FRightVolume:word;

procedure SetPlaying(const Value: boolean);
procedure SwapBuffers;
procedure SetOnDataRequired(const Value: TDataRequiredEvent);
procedure WriteData;

function GetCurrentPosTime: cardinal;
function GetCurrentPosBytes: cardinal;
procedure SetLeftVolume(const Value: word);
function GetLeftVolume: word;
procedure SetRightVolume(const Value: word);
function GetRightVolume: word;
protected
procedure
DoDataRequired(const Buffer:pointer;
const BufferSize:cardinal;
var BytesInBuffer:cardinal);virtual;
procedure WaveProc(const handle:THandle;
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;

procedure Pause;
procedure Resume;
published
property
CurrentPosTime:cardinal read GetCurrentPosTime;
property CurrentPosBytes:cardinal read GetCurrentPosBytes;

property LeftVolume:word read GetLeftVolume write SetLeftVolume;
property RightVolume:word read GetRightVolume write SetRightVolume;

property Playing:boolean read FPlaying write SetPlaying;
property OnDataRequired:TDataRequiredEvent
read
FOnDataRequired write SetOnDataRequired;
end;

Impelementation's code looks like following code:

const MAX_BUFFER_SIZE=4*1024;
PLAYBACK_BUFFER_SIZE=64*1024;

{ TSoundPlayer }

procedure TSoundPlayer.Close;
begin
if
FHandle<>0 then
begin
Stop;
waveOutUnPrepareHeader(Handle,FBuffer1,Sizeof(TWaveHdr));
waveOutUnPrepareHeader(Handle,FBuffer2,Sizeof(TWaveHdr));
WaveOutClose(FHandle);
FHandle:=0;
end;
end;

procedure _WaveOutProc(Handle:HWAVEOUT;uMsg:UINT;
dwInstance:DWORD;
dwParam1,dwParam2:DWORD);stdcall;

begin
TSoundPlayer(dwInstance).WaveProc(handle,
uMsg,
dwInstance,
dwParam1,
dwParam2);
end;

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

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

destructor TSoundPlayer.Destroy;
begin
Close;
FreeMem(FBuffer1.lpData,PLAYBACK_BUFFER_SIZE);
FreeMem(FBuffer2.lpData,PLAYBACK_BUFFER_SIZE);
dispose(FBuffer1);
dispose(FBuffer2);
inherited;
end;


procedure TSoundPlayer.DoDataRequired(const Buffer: pointer;
const BufferSize: cardinal; var BytesInBuffer: cardinal);
begin
if
Assigned(FOnDataRequired) then
FOnDataRequired(self,Buffer,BufferSize,BytesInBuffer);
end;

function TSoundPlayer.GetCurrentPosBytes: cardinal;
var posInfo:TMMTime;

begin
if
(Handle<>0) then
begin
ZeroMemory(@posInfo,sizeof(TMMTime));
PosInfo.wType:=TIME_BYTES;
waveOutGetPosition(Handle,@posInfo,sizeof(TMMTime));
result:=posInfo.cb;
end else
result:=0;
end;

function TSoundPlayer.GetCurrentPosTime: cardinal;
var posInfo:TMMTime;
begin
result:=0;
if Handle<>0 then
begin
PosInfo.wType:=TIME_MS;
waveOutGetPosition(Handle,@posInfo,sizeof(TMMTime));
result:=posInfo.ms;
end;

end;

function TSoundPlayer.GetLeftVolume: word;
var dwVolume:cardinal;
begin
waveOutGetVolume(FHandle,@dwVolume);
FLeftVolume:=LoWord(dwVolume);
FRightVolume:=HiWord(dwVolume);
result:=FLeftVolume;
end;

function TSoundPlayer.GetRightVolume: word;
var dwVolume:cardinal;
begin
waveOutGetVolume(FHandle,@dwVolume);
FLeftVolume:=LoWord(dwVolume);
FRightVolume:=HiWord(dwVolume);
result:=FRightVolume;
end;


procedure TSoundPlayer.Open;
var ahandle:HWAVEOUT;
status:MMResult;
statusStr:string;
begin
if
Handle=0 then
begin
status:=WaveOutOpen(@aHandle,
WAVE_MAPPER,
@FWaveFormat,
cardinal(@_WaveOutProc),
cardinal(Self),
CALLBACK_FUNCTION);

FHandle:=aHandle;
if status<>MMSYSERR_NOERROR then
begin
setlength(statusStr,MAXERRORLENGTH);
waveOutGetErrorText(status,pChar(statusStr),
MAXERRORLENGTH);
raise ESndError.Create(statusStr);
end;

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

end;

procedure TSoundPlayer.Pause;
begin
if
FHandle<>0 then
WaveOutPause(FHandle);
end;

procedure TSoundPlayer.Resume;
begin
if
FHandle<>0 then
WaveOutRestart(FHandle);

end;

procedure TSoundPlayer.SetLeftVolume(const Value: word);
var dwVolume:cardinal;
begin
FLeftVolume:=value;
dwVolume:=(FRightVolume shl 16) or FLeftVolume;
waveOutSetVolume(FHandle,dwVolume);
end;

procedure TSoundPlayer.SetOnDataRequired(const Value: TDataRequiredEvent);

begin
FOnDataRequired := Value;
end;

procedure TSoundPlayer.SetPlaying(const Value: boolean);
begin
Stop;
end;

procedure TSoundPlayer.SetRightVolume(const Value: word);
var dwVolume:cardinal;

begin
FRightVolume:=value;
dwVolume:=(FRightVolume shl 16) or FLeftVolume;
waveOutSetVolume(FHandle,dwVolume);
end;

procedure TSoundPlayer.Start;
begin
if
Handle<>0 then
begin
Stop;
FCurrentBuffer:=FBuffer1;
//pake buffer 64KB, kalo buffer kecil misal 4KB
//suara terdengar putus-putus
FBuffer1.dwBufferLength:=PLAYBACK_BUFFER_SIZE;
FBuffer2.dwBufferLength:=PLAYBACK_BUFFER_SIZE;
WriteData;
FPlaying:=true;
end;

end;

procedure TSoundPlayer.Stop;
begin
FPlaying:=false;
if FHandle<>0 then
WaveOutReset(FHandle);
end;

procedure TSoundPlayer.SwapBuffers;
begin
if
FCurrentBuffer=FBuffer1 then
FCurrentBuffer:=FBuffer2
else
FCurrentBuffer:=FBuffer1;

end;

procedure TSoundPlayer.WaveProc(const handle:THandle;
const msg:UINT;
const dwInstance:cardinal;
const dwParam1,dwParam2:cardinal);
begin
case
msg of
WOM_DONE:begin
if
FPlaying then
begin
//tukar buffer
SwapBuffers;
WriteData;
end;
end;
end;

end;

procedure TSoundPlayer.WriteData;
var ActBytesInBuffer:cardinal;
begin
ActBytesInBuffer:=0;
DoDataRequired(FCurrentBuffer.lpData,
FCurrentBuffer.dwBufferLength,
ActBytesInBuffer);

if ActBytesInBuffer=0 then
begin

FPlaying:=false;
exit;
end;

if ActBytesInBuffer<FCurrentBuffer.dwBufferLength then
begin
//data yang harus dimainkan sudah
//habis, isi panjang buffer dengan sisa data yang ada
FCurrentBuffer.dwBufferLength:=ActBytesInBuffer;
WaveOutWrite(Handle,FCurrentBuffer,sizeof(TWaveHdr));
FPlaying:=false;
end else
WaveOutWrite(Handle,FCurrentBuffer,sizeof(TWaveHdr));
end;

What I will discuss is WriteData() method. When it called, we assumed buffer is empty (actBytesInBuffer=0). WriteData will call DoDataRequired() to generate OnDataRequired event to application. Application is requested to copy data into buffer provided. Size of buffer is also sent to let application knows that is should not copy data more than buffer size. Actual size of data being copied must be returned to TSoundPlayer by setting ActBytesInBuffer value. If ActBytesInBuffer =0, it is assumed that there are no more data to play. Otherwise, we check whether actual data size is smaller than buffer size. If yes, we assume this is is the last block. We set dwBufferLength with value of ActBufferInBytes and then play it to device driver. If not, we play it. WriteData will be called repeatly until no more block to play. Application must decide when data block is finished.

Ok, now we have TSoundPlayer class. Let's create demo to utilize features offered by this class.

Creating Application Demo

Create new apllication and drag drop control onto the form just like following figure:

Screenshot of sound player application form design

Rename control names, Set Enabled property to false except Open button. Complete its code so it looks like following code:

unit ufrmMain;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,MMSystem,
uSound, ExtCtrls, ComCtrls;

type
TForm1 = class(TForm)
OpenDialog1: TOpenDialog;
btnOpen: TButton;
btnPlay: TButton;
btnStop: TButton;
Timer1: TTimer;
ProgressBar1: TProgressBar;
trkbrLeft: TTrackBar;
trkbrRight: TTrackBar;
Label1: TLabel;
Label2: TLabel;
procedure btnPlayClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
procedure btnOpenClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure trkbrLeftChange(Sender: TObject);
procedure trkbrRightChange(Sender: TObject);
private
SoundPlayer:TSoundPlayer;
FWaveStream:TMemoryStream;

procedure DataRequired(sender: TObject; const Buffer: pointer;
const BufferSize: cardinal; var BytesInBuffer: cardinal);
procedure LoadFormat(FMem: TMemoryStream; var FWaveFormatEx: TWaveFormatEx);
{ Private declarations }
public
constructor
Create(AOwner:TComponent);override;
destructor Destroy;override;
{ Public declarations }
end;


var
Form1: TForm1;

implementation

{$R *.dfm}

{ TForm1 }

procedure TForm1.DataRequired(sender:TObject;
const Buffer:pointer;
const BufferSize:cardinal;
var BytesInBuffer:cardinal);
begin
if
FWaveStream.Position+BufferSize<FWaveStream.Size then
begin
BytesInBuffer:=BufferSize;
FWaveStream.ReadBuffer(Buffer^,BufferSize);
end else
begin
BytesInBuffer:=FWaveStream.Size-FWaveStream.Position;
FWaveStream.ReadBuffer(Buffer^,BytesInBuffer);
end;

end;

constructor TForm1.Create(AOwner: TComponent);
begin
inherited
;
FWaveStream:=TMemoryStream.Create;

SoundPlayer:=TSoundPlayer.Create;
SoundPlayer.OnDataRequired:=DataRequired;
end;

destructor TForm1.Destroy;
begin
SoundPlayer.Free;
FWaveStream.Free;
inherited;
end;

procedure TForm1.btnPlayClick(Sender: TObject);

var dataOffset:integer;
begin
//hitung start data pada file WAV
dataOffset:=4+ SizeOf(DWORD)+
4 + 4 +SizeOf(DWORD)+
SizeOf(TWaveFormatEx) + 4 + SizeOf(DWORD);
//geser pointer ke posisi data block
FWaveStream.Seek(dataOffset,soFromBeginning);

ProgressBar1.Max:=FWaveStream.Size-dataOffset;
ProgressBar1.Min:=0;
ProgressBar1.Position:=0;

SoundPlayer.Start;

btnStop.Enabled:=true;
btnPlay.Enabled:=false;
Timer1.Enabled:=true;
end;

procedure TForm1.btnStopClick(Sender: TObject);
begin
SoundPlayer.Stop;

ProgressBar1.Position:=0;
btnPlay.Enabled:=true;
btnStop.Enabled:=false;
end;

procedure TForm1.LoadFormat(FMem: TMemoryStream;
var FWaveFormatEx:TWaveFormatEx);

var id:array[0..3] of char;
len:integer;
begin
FMem.Seek(0,soFromBeginning);
FMem.ReadBuffer(id[0],4);
if (id='RIFF') then
begin
FMem.Seek(4,soFromCurrent);
FMem.ReadBuffer(id[0],4);
if (id='WAVE') then
begin
FMem.ReadBuffer(id[0],4);
if (id='fmt ') then
begin
FMem.ReadBuffer(len,4);
if (len=Sizeof(TWaveFormatEx)) then
begin
FMem.ReadBuffer(FWaveFormatEx,len);
end else
if
(len=Sizeof(TPCMWaveFormat)) then
begin
FMem.ReadBuffer(FWaveFormatEx,len);
FWaveFormatEx.cbSize:=0;
end else
begin
FMem.Clear;
raise Exception.Create('Format file WAV tidak disupport.');
end;
end else
begin
FMem.Clear;
raise Exception.Create('Format file WAV invalid.');
end;
end else
begin
FMem.Clear;
raise Exception.Create('Bukan format file WAV.');
end;
end else
begin
FMem.Clear;
raise Exception.Create('Bukan format file WAV.');
end;

end;

procedure TForm1.btnOpenClick(Sender: TObject);
var afile:TFileStream;
awaveFormat:TWaveFormatEx;
begin
SoundPlayer.Stop;
if OpenDialog1.Execute then
begin
afile:=TFileStream.Create(OpenDialog1.FileName,fmOpenRead);
try
FWaveStream.Clear;
FWaveStream.CopyFrom(afile,0);
LoadFormat(FWaveStream,awaveFormat);
SoundPlayer.Channel:=awaveFormat.nChannels;
SoundPlayer.SamplePerSec:=awaveFormat.nSamplesPerSec;
SoundPlayer.BitsPerSample:=awaveFormat.wBitsPerSample;

SoundPlayer.Open;

trkbrLeft.Position:=SoundPlayer.LeftVolume;
trkbrRight.Position:=SoundPlayer.RightVolume;

trkbrLeft.enabled:=true;
trkbrRight.enabled:=true;
btnPlay.Enabled:=true;
finally
afile.Free;
end;
end;

end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
if
ProgressBar1.Position=ProgressBar1.Max then
begin
btnPlay.Enabled:=true;
btnStop.Enabled:=false;

Timer1.Enabled:=false;
ProgressBar1.Position:=0;
SoundPlayer.Stop;
end else
ProgressBar1.Position:=SoundPlayer.CurrentPosBytes;
end;

procedure TForm1.trkbrLeftChange(Sender: TObject);
begin
SoundPlayer.LeftVolume:=trkbrLeft.Position;
end;


procedure TForm1.trkbrRightChange(Sender: TObject);
begin
SoundPlayer.RightVolume:=trkbrRight.Position;
end;

end.

To play a WAV file, click Play button. To change left and right volume, slide trackbar to left or right.

Source code can be downloaded here.