Memainkan Suara dengan Wave API


Format WAV adalah format audio default pada sistem operasi Windows. Artikel ini berisi topik bahasan penggunaan Wave API untuk memainkan file WAV dengan Delphi.

Pada artikel sebelumnya, Merekam suara dengan Wave API, kita telah membahas cara penggunaan Wave API untuk melakukan perekaman suara. Pada topik kali ini, kita akan membahas bagaimana memainkan file WAV menggunakan Wave API. Jika anda tidak membutuhkan akses ke data wave yang sedang dimainkan dan hanya ingin memainkan file WAV, artikel ini mungkin tidak terlalu berguna bagi anda. Untuk sekedar memainkan suara WAV, anda bisa menggunakan PlaySound() atau TMediaPlayer. Jika anda membutuhkan akses ke data wave yang hendak dimainkan, misalnya untuk mengubah data wave dengan menerapkan filter dan efek pada data wave, maka artikel ini tepat untuk anda.
WaveOut***

Fungsi-fungsi playback wave pada Wave API, menggunakan penamaan waveOut***, contohnya waveOutOpen(), waveOutClose() dan lain-lain. Kita akan membahas fungsi-fungsi ini dan cara penggunaannya segera. Deklarasi fungsi-fungsi ini terletak pada unit MMSystem.pas.
Membuka Waveform Output Device.

Ini adalah langkah pertama yang harus kita jalankan untuk melakukan playback suara.

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

Parameter lphWaveOut adalah variabel yang akan menerima handle HWAVEOUT yang akan dipergunakan untuk mengakses waveform audio output device. Jika diisi nil, dwFlags harus diisi WAVE_FORMAT_QUERY. uDevice ID adalah ID waveform audio output device. Jika anda tidak tahu ID device yang hendak anda pergunakan, gunakan saja WAVE _MAPPER agar waveOutOpen sendiri yang mengisinya untuk kita. lpFormat berisi alamat ke srtuktur data TWaveFormatEx yang menentukan format wave yang akan kita mainkan. dwCallback berisi alamat fungsi callback, event handle atau handle window yang akan diberi notifikasi ketika terjadi event saat playback. dwInstance adalah user data yang akan dikirimkan ke callback. dwFlags memberitahukan tipe callback yang kita inginkan juga informasi mengenai wave. Dapat diisi salah satu flag berikut:

CALLBACK_NULL
Mekanisme callback tidak digunakan.
CALLBACK_FUNCTION
Callback menggunakan fungsi. Jika menggunakan flag ini, dwCallback harus berisi alamat fungsi callback. Kita akan bahas fungsi callback segera.
CALLBACK_WINDOW
Callback menggunakan handle window. Jika menggunakan flag ini, dwCallback harus berisi handle window.
CALLBACK_THREAD
Callback menggunakan thread. Jika menggunakan flag ini, dwCallback harus berisi thread ID
CALLBACK_EVENT
Callback menggunakan handle event. Jika menggunakan flag ini, dwCallback harus berisi handle event.
WAVE_FORMAT_QUERY
Jika menggunakan flag ini, kita menginstruksikan waveOutOpen untuk melakukan pengecekan apakah output device sanggup memainkan format wave yang kita tentukan, namun output device tidak dibuka untuk playback.

Perhatikan bahwa selain flag-flag di atas masih ada flag lain, namun sengaja tidak dibahas karena menurut saya tidak terlalu sering dipergunakan.

Jika sukses waveOutOpen akan mengembalikan nilai MMSYSERR_NOERROR. Jika gagal, kode kesalahan dapat berupa WAVERR_BADFORMAT untuk format wave yang tidak didukung dan lain-lain.
Format Wave

Format wave kita definisikan menggunakan struktur data TWaveFormatEx yang deklarasinya seperti berikut :

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 berisi format wave, untuk memainkan wave standard milik Windows kita isi denganWAVE_FORMAT_PCM. nChannels berisi jumlah channel 1 untuk mono dan 2 untuk stereo. nSamplesPerSec berisi jumlah sample yang dimainkan per detik dalam satuan Hertz. Untuk WAVE_FORMAT_PCM, nilai yang umum adalah 8000 Hz, 11025 Hz, 22050 Hz dan 44100 Hz. nAvgBytesPerSec, berisi rata-rata laju data transfer yang diperlukan, untuk WAVE_FORMAT_PCM, nilainya adalah hasil perkalian antara nSamplesPerSec dan nBlockAlign. nBlockAlign berisi ukuran blok data
Callback

Callback terdiri atas beberapa, yang paling sering saya pergunakan adalah callback berupa fungsi. Callback fungsi menggunakan funsgi dengan format berikut:

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

Handle adalah handle wave out yang diperoleh melalui pemanggilan waveOutOpen(). uMsg adalah tipe kejadian yang terjadi yakni WOM_OPEN, WO_CLOSE dan WOM_DONE. Yang paling kita perlukan adalah WOM_DONE, pesan ini memberitahukan bahwa device driver telah selesai memproses data. dwInstance adalah user data yang kita kirim pada saat pemanggilan waveOutOpen(), dwParam1 dan dwParam2 adalah parameter. Pada saat WOM_DONE, dwParam1 akan berisi pointer ke struktur data yang dimainkan.

Contoh kode berikut ini membuka device untuk playback, menggunakan fungsi callback dimana nama fungsinya adalah _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;

Contoh berikut menguji apakah wave format 8 bit stereo, sample rate 22,05 KHz dapat dimainkan oleh 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;

Menyiapkan Buffer

Kita perlu menyiapkan buffer yang akan menampung data wave. Kita bertanggung jawab atas lifetime manajemen memori buffer ini. Buffer ini kemudian wajib kita beritahukan ke device driver menggunakan waveOutPrepareHeader() sebelum kita menggunakannya untuk mengirimkan data wave ke device driver.

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

hWaveOut adalah handle wave out, lpWaveOutHdr menyimpan informasi tentang buffer kita. uSize adalah ukuran wave header. Berikut ini adalah deklarasi 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;

Untuk keperluan kita memainkan WAV, field yang penting adalah lpData berisi alamat memori buffer, panjang buffer disimpan di dwBufferLength. dwBytesRecorded hanya dipergunakan untuk proses recording. dwUser dapat kita pergunakan untuk menyimpan user data. dwFlags berisi flag mengenai buffer.
Mengisi Buffer dengan Data

Proses mengisi buffer dengan data-data waveform sepenuhnya adalah tanggung jawab kita. Untuk mengisi buffer anda bisa menggunakan fungsi-fungsi pengkopian data seperti Move() atau CopyMemory(). Yang perlu kita ingat, jika jumlah data yang kita kopi kedalam buffer lebih kecil dari ukuran buffer, dwBufferLength perlu kita set sama dengan panjang data, harap diperhatikan bahwa ukuran buffer tidak berubah. Jika kita perlu mengubah ukuran buffer, kita wajib memberitahukan perubahan ini ke deice driver dengan terlebih dahulu memanggil waveOutUnPrepareHeader() (fungsi ini akan kita bahas segera) mengubah ukuran buffer dan kemudian memanggil lagi waveOutPrepareHeader().
Mengirimkan Buffer ke Device Driver

Setelah buffer terisi data, kita siap memainkannya. Untuk itu kita panggil waveOutWrite().

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

lpWaveOutHdr adalah wave header yang sebelumnya telah kita prepare. uSize adalah ukuran data wave header. Ketika kita mengirimkan blok data pertama ke device driver maka playback akan dimulai. Kita dapat saja mengirimkan seluruh data WAV ke device driver dengan menggunakan pemanggilan waveOutWrite sekali saja, namun harap diperhatikan bahwa, pada beberapa soundcard (terutama soundcard lama), maksimum ukuran buffer yang dapat diproses adalah 64 KB, sehingga jika kita memiliki data waveform yang besarnya melebihi 64 KB, pemanggilan waveOutWrite harus dilakukan lebih dari satu kali dengan menggunakan blok-blok data yang lebih kecil.

Masalah lain yang dapat timbul berkaitan dengan proses memainkan blok-blok data beruuran kecil adalah supply data ke dievice driver harus kontinu. Sedikit saja ada kelambatan maka suara akan terdengar putus-putus akibat adanya gap antara kecepatan soundcard memproses data dan kecepatan aplikasi kita menyuplai data. Dari pengalaman saya, gap semacam ini biasanya timbul karena penggunaan ukuran buffer yang terlalu kecil. Buffer kecil, mengakibatkan soundcard membutuhkan waktu relatif cepat untuk memproses data, jauh lebih cepat dari waktu yang dibutuhkan aplikasi kita mengirim data ke device driver. Untuk kelas yang akan kita bangun, proses pengiriman data wave akan dipecah menjadi blok-blok kecil dengan ukuran blok 64 KB.
Menghentikan Playback

Fungsi waveOutReset() kita pergunakan untuk menghentikan proses playback.

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

Pause/Resume Playback

Untuk menghentikan sementara playback kita menggunakan waveOutPause() dan untuk meresume playback yang dihentikan sementara kita gunakan waveOutRestart().
Membebaskan Buffer

Setelah kita, selesai memainkan data wave, sebelum membebaskan buffer pastikan kita memanggil waveOutUnPrepareHeader() untuk memberitahukan bahwa buffer akan dibebaskan. Dengan pemanggilan waveOutUnPrepareHeader(), device driver diberitahu agar tidak lagi menggunakan buffer ini. Setelah proses unprepare, buffer dapat kita free dengan aman.

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

Menutup Device

Agar aplikasi kita terlihat sopan di mata Windows, resource yang sudah kita pergunakan kita kembalikan lagi yakni dengan menutup waveform audio output device.

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

Langkah-langkah di atas adalah dasar untuk memainkan wave dengan Wave API. Ada beberapa fungsi tambahan berkaitan dengan urusan playback wave. Dua yang akan saya bahas adalah mendapatkan informasi progress playback, mengatur volume dan mendapatkan string status kesalahan.
Mendapatkan Progress Playback

Status posisi playback dapat diminta dengan memanggil waveOutGetPosition(). Perhatikan bahwa kita hanya dapat meminta informasi posisi, namun tidak dapat mengubah posisi playback.

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

lpInfo akan diisi dengan informasi posisi playback, format informasinya bermacam-macam. Sebelum memanggil fungsi ini format informasinya perlu kita tentukan. uSize berisi ukuran data lpInfo. Deklarasi PMMTime sendiri adalah sebagai berikut:

{ 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 harus kita tentukan. Nilai yang valid adalah TIME_BYTES untuk mendapatkan posisi playback dalam satuan bytes yang telah diproses. Jika menggunakan TIME_BYTES maka data field cb akan berisi informasi posisi. TIME_MS untuk mengembalikan posisi playback dalam satuan milisecond, untuk tipe posisi ini field ms akan berisi informasi yang kita butuhkan. Untuk kelas kita nanti, kita hanya akan menggunakan dua macam tipe ini.
Mendapatkan Status Pesan Kesalahan
Untuk mendapatkan pesan kesalahan dari kode kesalahan yang dikembalikan fungsi waveOut***, kita menggunakan waveOutGetErrorText(). lpText kita isi dengan buffer yang akan menampung pesan kesalahan. uSize menentukan ukuran buffer tersebut.

function waveOutGetErrorText(mmrError: MMRESULT;
lpText: PChar;
uSize: UINT): MMRESULT; stdcall;

Mengatur Volume Playback

Volume speaker kiri dan kanan dapat diatur menggunakan waveOutSetVolume(). Untuk meminta informasi volume, kita menggunakan waveOutGetVolume.

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

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

Volume kiri dan kanan menjadi satu dalam lpdwVolume dan dwVolume. Volume speaker kiri adalah low word dwVolume dan volume speaker kanan adalah high word.
Membuat TSoundPlayer
Desain

TSoundPlayer adalah kelas yang akan membungkus fungsionalitas waveOut***. Kelas ini akan diturunkan dari kelas TWaveObject (pembahasannya ada pada artikel Merekam suara dengan Wave API). Prosedur abstrak Open, Close, Start, Stop akan kita override. Konstructor dan destruktor diisi dengan kode alokasi dan dealokasi buffer, juga ditambahkan metode untuk patse dan resume playback.

Kita juga menambahkan property untuk mengatur volume kiri dan kanan, property posisi playback dan property event yang akan dibangkitkan ketika instance kelas membutuhkan data untuk dikirim ke device driver.
Implementasi

TSoundPlayer dideklarasikan dalam unit yang sama dengan kelas TSoundRecorder (Merekam suara dengan Wave API) yakni 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;

Kode implementasinya adalah sebagai berikut:

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 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;

Yang sedikit saya bahas adalah metode WriteData(). Ketika dipanggil, kita asumsikan data dalam buffer tidak ada (actBytesInBuffer=0). WriteData kemudian akan memanggil DoDataRequired() untuk membangkitkan event OnDataRequired ke aplikasi. Aplikasi akan diminta mengkopi data ke buffer yang sudah disediakan. Ukuran buffer juga diberitahukan ke aplikasi agar aplikasi tidak mengkopi data lebih dari ukuran buffer. Jumlah aktual data yang dikopi ke buffer harus dikembalikan ke TSoundPlayer melalui variabel ActBytesInBuffer. Jika ActBytesInBuffer =0 diasumsikan tidak ada data yang harus dimainkan. Jika tidak nol, kita cek apakah jumlah data yang dikopi lebih kecil dari ukuran buffer. Jika ya, kita asumsikan bahwa blok data ini adalah blok terakhir. dwBufferLength kita set sama dengan ActBufferInBytes dan kita kirim data ke device driver. Jika lainnya, buffer langsung kita mainkan. WriteData akan dipanggil berulang-ulang hingga tidak ada blok data yang harus dimainkan. Yang bertanggung jawab menentukan kapan blok data habis adlah aplikasi pemanggil.

Ok, kita sudah memiliki kelas TSoundPlayer. Mari kita buat aplikasi demo yang akan memanfaatkan fitur-fitur kelas ini.
Membuat aplikasi demo

Buat aplikasi dan drag drop kontrol ke form sehingga menjadi seperti gambar berikut:

Screenshot desain form aplikasi sound player

Rename nama-nama kontrol, ubah property Enabled menjadi false kecuali tombol Open dan lengkapi kodenya sehingga menjadi kode berikut:

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 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.

Untuk memainkan buka sebuah file WAV dan klik button Play. Untuk mengubah-ubah volume speaker kiri dan kanan, geser-geser trackbar kiri dan kanan.

Komentar:

Posting Komentar

 
© 2009 | Desire | Por Templates para Você