Memainkan WAV dengan Wave API

September 19, 2006 at 1:51 pm | Posted in General | 8 Comments

by Zamrony P Juhara

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

screen_shot_design_form_sound_player.gif

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

About these ads

8 Comments »

RSS feed for comments on this post. TrackBack URI

  1. Thx atas tutorialnya …

    Mas klo ngerekam pake line in tp cuman left channel or right channel aja yg di rekam gimana mas ? so dlam 1 sound card line in bisa kita rekam 2 channel…

    makasih B4

  2. mohon maaf pak…
    kalo kita mau mengakses sample data wavenya di listing programnya di bagian mana pak..ya
    Terima kasih Pak

  3. TSoundPlayer yang saya buat diatas mengharuskan aplikasi yang mensupply data WAV. Tiap kali TSoundPlayer membutuhkan data, event OnDataRequired di generate. Aplikasi harus menyuplai data WAV dengan mengkopi data sample ke buffer milik TSoundPlayer. Alamat memorinya dan ukuran buffer disimpan di paramater buffer dan bufferSize (lihat contoh event handler TForm1.DataRequired). Jika Anda bermaksud mengubah-ubah data WAV sebelum dikirim ke device driver untuk dimainkan lakukan di event OnDataRequired. Namun usahakan prosesnya secepat mungkin lebih cepat dari waktu yang diperlukan oleh sound card untuk memainkan sample data, jika tidak, suara akan menjadi terputus-putus karena kontinuitas supplai data terlambat

  4. klo waveOutGetPitch dan waveOutSetPitch, kira2 gimana contoh codingnya ? trims.

  5. keren!

  6. mas, mo tanya gimana play beberapa file wav di listbox secara random/repeat all?

    saya pake perintah dibawah, hanya untuk satu file WAV :
    SndPlaySound (‘C:\My Documents\suara.wav’,
    SND_LOOP or SND_ASYNC);

    TQ

  7. SndPlaySound() sederhana namun tidak cocok digunakan bila hendak digunakan untuk membuat audio player dengan playlist. Contoh Anda diatas akan memainkan suara.wav secara berulang-ulang dan asynchronous (kontrol akan dikembalikan ke aplikasi segera sebelum proses memainkan suara selesai).

    Anda tidak tahu kapan suara tersebut selesai dimainkan. Solusinya gunakan TSoundPlayer di atas atau TMediaPlayer.
    Bila menggunakan TSoundPlayer, pada event OnDataRequired. Bila jumlah data yang harus dimainkan lebih kecil dari ukuran buffer maka suara hampir mendekati selesai. lakukan persiapan untuk membaca file suara berikutnya.

    Bila menggunakan TMediaPlayer, buat handler event OnNotify, set property Notify:=true dan cek apakah suara telah selesai, bila ya dan bila random pilih suara yang dimainkan secara random, bila tidak random, ambil suara berikutnya

  8. meskipun ini tread udah lama, jika saya play sound trus diform yg sama juga ada running text. tp koq running textnya berhenti saat musiknya sedang play ya?


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com. | The Pool Theme.
Entries and comments feeds.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: