Juhara.com
Language : English Indonesia

Memainkan WAV dengan Wave API

Zamrony P. Juhara
19 September 2006 11:39:00
 (7191 views)
Artikel berisi topik bahasan penggunaan Wave API untuk memainkan file WAV

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

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

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

Source code bisa diambil disini

Artikel Terkait

Anda suka artikel ini? Bantu website ini berkembang dengan menyumbang. Berapapun jumlahnya akan sangat dihargai.

Atau Anda dapat membantu dengan membuat bookmark. Delicious Bookmark this on Delicious