在 Delphi 下使用 DirectSound (3): 播放第一个 Wave 文件


建立 IDirectSound8 对象后, 首先要通过其 SetCooperativeLevel() 方法设置协作优先级;
因为其它应用程序有可能同时使用该设备(声卡), 这是必需的步骤.

function SetCooperativeLevel(

  hwnd: HWND;    //窗口句柄

  dwLevel: DWORD //协作优先级

): HResult; stdcall;



//协作优先级选项

DSSCL_NORMAL       = 1; //普通; 使用次缓冲区, 不能修改、压缩、设置主缓冲区; 不影响使用该设备的其它应用程序

DSSCL_PRIORITY     = 2; //优先; 可设置但不能直接写入主缓冲区, 配合次缓冲区使用

DSSCL_EXCLUSIVE    = 3; //独占; 已过时, 现同 DSSCL_PRIORITY

DSSCL_WRITEPRIMARY = 4; //顶级; 必须直接写入主缓冲区, 不能使用次缓冲区; 这需要自己写混音程序, 或许只有设计者自己有能力这么做



然后通过 IDirectSound8.CreateSoundBuffer() 方法建立缓冲区, 这个过程主要是填写 TDSBufferDesc 结构;
填写 TDSBufferDesc 结构时又同时需要 TWaveFormatEx 结构的指针, 这个 TWaveFormatEx 结构我们会直接从 Wave 文件中读取.

除非优先级设置为 DSSCL_WRITEPRIMARY, 程序至少应该有一个次缓冲区(这同时会自动建立主缓冲区), 次缓冲区的声音最终也是经主缓冲混音输出.

缓冲区又分静态缓冲区和流式缓冲区:
流式缓冲区适合播放较大的声音文件, 其原理是边写入变播放(程序写起来很麻烦);
使用静态缓冲区可以一次性设置到需要的大小, 这样只写入一次就够了, 下面的例子就先使用了这种简单方法.


function CreateSoundBuffer(

  const pcDSBufferDesc: TDSBufferDesc; //描述缓冲区的结构

  out ppDSBuffer: IDirectSoundBuffer;  //缓冲区对象

  pUnkOuter: IUnknown                  //未使用, nil

): HResult; stdcall; //错误码



TDSBufferDesc = packed record

  dwSize: DWORD;              //结构大小(字节); 使用此结构须先给它赋值

  dwFlags: DWORD;             //功能标识

  dwBufferBytes: DWORD;       //缓冲区大小

  dwReserved: DWORD;          //未使用, 须为 0

  lpwfxFormat: PWaveFormatEx; //TWaveFormatEx 结构的指针

  guid3DAlgorithm: TGUID;     //关于 3D 算法的 GUID 常量; DX7 后的版本可用, 当前结构比之前的 TDSBufferDesc1 就多出了这个字段

end;



//TDSBufferDesc.dwFlags:

DSBCAPS_PRIMARYBUFFER       = $00000001; //使用主缓冲区, 默认是使用次缓冲区

DSBCAPS_STATIC              = $00000002; //静态缓冲区, 若有可能会将缓冲区建立在声卡上; 默认是创建流式缓冲区

DSBCAPS_LOCHARDWARE         = $00000004; //强制使用硬缓冲

DSBCAPS_LOCSOFTWARE         = $00000008; //强制使用软缓冲

DSBCAPS_CTRL3D              = $00000010; //缓冲区具有 3D 控制能力

DSBCAPS_CTRLFREQUENCY       = $00000020; //缓冲区具有频率控制能力

DSBCAPS_CTRLPAN             = $00000040; //缓冲区具有相位控制能力

DSBCAPS_CTRLVOLUME          = $00000080; //缓冲区具有音量控制能力

DSBCAPS_CTRLPOSITIONNOTIFY  = $00000100; //缓冲区具有位置通知能力

DSBCAPS_CTRLFX              = $00000200; //缓冲区支持特效

DSBCAPS_STICKYFOCUS         = $00004000; //当程序切换到其它不使用 DirectSound 的程序时, 可继续播放, 否则会静音

DSBCAPS_GLOBALFOCUS         = $00008000; //当程序即使切换到其它使用 DirectSound 的程序, 该缓冲区仍可用, 除非其它程序有优先设置

DSBCAPS_GETCURRENTPOSITION2 = $00010000; //使 GetCurrentPosition 能获取更精确的播放位置

DSBCAPS_MUTE3DATMAXDISTANCE = $00020000; //衰减的最大距离, 仅适用于软缓冲区

DSBCAPS_LOCDEFER            = $00040000; //让 DirectSound 自动延迟决定是使用硬缓冲还是软缓冲

DSBCAPS_TRUEPLAYPOSITION    = $00080000; //强制 GetCurrentPosition 返回真实的播放位置, 仅在 Vista 之后的版本有效



向缓冲区写入数据前需要先使用 IDirectSoundBuffer.Lock() 方法锁定内存(先禁止 Windows 自动管理这块内存).

Lock() 会通过其 var 参数返回写入指针和要写入的数据大小(这里的缓冲区特别是设备提供的缓冲区不会太大, 所以大小不会太随意).

Lock() 返回两个写入指针和两个数据大小(一对); 当写到缓冲区尾部还不能写完时, 就要绕回来从头写, 此时就需要第二个指针和大小.
写这个双指针的程序时也有点绕, 幸好本例暂时只用到一个指针.

Lock() 还有两个锁定标识常量, 本例使用 DSBLOCK_ENTIREBUFFER, 标识锁定整个缓冲区, 这样其前两个参数也暂时不用考虑了.

写入完成后还要解锁.

function Lock(

  dwOffset: DWORD;        //锁定起始处的偏移量

  dwBytes: DWORD;         //要锁定的字节数

  ppvAudioPtr1: PPointer; //输出第一个内存指针

  pdwAudioBytes1: PDWORD; //输出已锁定的字节数

  ppvAudioPtr2: PPointer; //输出第二个内存指针

  pdwAudioBytes2: PDWORD; //输出已锁定的字节数

  dwFlags: DWORD          //锁定控制标志

): HResult; stdcall; //错误码



//Lock.dwFlags

DSBLOCK_FROMWRITECURSOR = $00000001; //从写入位置锁定, 参数 dwOffset 将被忽略 

DSBLOCK_ENTIREBUFFER    = $00000002; //锁定整个缓冲区, 参数 dwBytes 将被忽略 



//

function Unlock(

  pvAudioPtr1: Pointer; //第一个锁定的偏移量

  dwAudioBytes1: DWORD; //需要解锁的字节数

  pvAudioPtr2: Pointer; //第二个锁定的偏移量

  dwAudioBytes2: DWORD  //需要解锁的字节数

): HResult; stdcall; //错误码



写入后, 就可以通过 IDirectSoundBuffer.Play()、Stop() 控制播放了:

function Play(

  dwReserved1: DWORD; //未使用, 0

  dwPriority: DWORD;  //未使用, 0

  dwFlags: DWORD      //播放控制标志; 如果只播放一次可以直接给个 0

): HResult; stdcall; //



//Play.dwFlags

DSBPLAY_LOOPING              = $00000001; //循环播放

DSBPLAY_LOCHARDWARE          = $00000002; //仅播放硬缓冲区的声音

DSBPLAY_LOCSOFTWARE          = $00000004; //仅播放软缓冲区的声音

DSBPLAY_TERMINATEBY_TIME     = $00000008; //暂未学习

DSBPLAY_TERMINATEBY_DISTANCE = $00000010; //暂未学习

DSBPLAY_TERMINATEBY_PRIORITY = $00000020; //暂未学习



//

function Stop: HResult; stdcall; //叫暂停更合适



学写下面的程序前, 我曾想过是否使用前人写过的 DSUtil.pas, 但种种原因还是放弃了, 主要还是想了解得透彻些.

程序用到了以前写过的两个函数:
http://www.cnblogs.com/del/archive/2009/11/06/1597735.html
http://www.cnblogs.com/del/archive/2009/11/06/1597735.html

测试程序只用到了三个 Button, 还有准备一个测试文件(C:\Temp\Test.wav).


unit Unit1;



interface



uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls;



type

  TForm1 = class(TForm)

    Button1: TButton;

    Button2: TButton;

    Button3: TButton;

    procedure FormCreate(Sender: TObject);

    procedure FormDestroy(Sender: TObject);

    procedure Button1Click(Sender: TObject);

    procedure Button2Click(Sender: TObject);

    procedure Button3Click(Sender: TObject);

  end;



var

  Form1: TForm1;



implementation



{$R *.dfm}



uses DirectSound, MMSystem;



const wavPath = 'C:\Temp\Test.wav'; //测试用的 Wave, 须保证文件存在并注意路径权限, 且只能是 PCM 格式的 Wave 文件



var

  myDSound: IDirectSound8;

  buf: IDirectSoundBuffer; //缓冲区对象



{从 Wave 文件中获取 TWaveFormatEx 结构的函数}

function GetWaveFmt(FilePath: string; var fmt: TWaveFormatEx): Boolean;

var

  hFile: HMMIO;

  ckiRIFF,ckiFmt: TMMCKInfo;

begin

  Result := False;

  hFile := mmioOpen(PChar(FilePath), nil, MMIO_READ);

  if hFile = 0 then Exit;

  ZeroMemory(@ckiRIFF, SizeOf(TMMCKInfo));

  ZeroMemory(@ckiFmt, SizeOf(TMMCKInfo));

  ZeroMemory(@fmt, SizeOf(TWaveFormatEx));

  ckiFmt.ckid := mmioStringToFOURCC('fmt', 0);

  mmioDescend(hFile, @ckiRIFF, nil, MMIO_FINDRIFF);

  if (ckiRIFF.ckid = FOURCC_RIFF) and

     (ckiRIFF.fccType = mmioStringToFOURCC('WAVE',0)) and

     (mmioDescend(hFile, @ckiFmt, @ckiRIFF, MMIO_FINDCHUNK) = MMSYSERR_NOERROR) then

     Result := (mmioRead(hFile, @fmt, ckiFmt.cksize) = ckiFmt.cksize);

  mmioClose(hFile, 0);

end;



{从 Wave 文件中获取波形数据的函数}

function GetWaveData(FilePath: string; var stream: TMemoryStream): Boolean;

var

  hFile: HMMIO;

  ckiRIFF,ckiData: TMMCKInfo;

begin

  Result := False;

  hFile := mmioOpen(PChar(FilePath), nil, MMIO_READ);

  if hFile = 0 then Exit;

  ZeroMemory(@ckiRIFF, SizeOf(TMMCKInfo));

  ZeroMemory(@ckiData, SizeOf(TMMCKInfo));

  ckiData.ckid := mmioStringToFOURCC('data', 0);

  mmioDescend(hFile, @ckiRIFF, nil, MMIO_FINDRIFF);

  if (ckiRIFF.ckid = FOURCC_RIFF) and

     (ckiRIFF.fccType = mmioStringToFOURCC('WAVE',0)) and

     (mmioDescend(hFile, @ckiData, @ckiRIFF, MMIO_FINDCHUNK) = MMSYSERR_NOERROR) then

    begin

      stream.Size := ckiData.cksize;

      Result := (mmioRead(hFile, stream.Memory, ckiData.cksize) = ckiData.cksize);

    end;

  mmioClose(hFile, 0);

end;



{程序初始化}

procedure TForm1.FormCreate(Sender: TObject);

begin

  Button1.Caption := '建立并播放';

  Button2.Caption := '反复播放';

  Button3.Caption := '暂停';

  Button2.Enabled := False;

  Button3.Enabled := False;

  system.ReportMemoryLeaksOnShutdown := True; //让程序自动报告内存泄露

end;



{主要程序}

procedure TForm1.Button1Click(Sender: TObject);

var

  bufDesc: TDSBufferDesc;   //建立缓冲区需要的结构

  wavFormat: TWaveFormatEx; //从 Wave 中提取的结构

  wavData: TMemoryStream;   //从 Wave 中提取的波形数据

  p1: Pointer;              //从缓冲区获取的写指针

  n1: DWORD;                //要写入缓冲区的数据大小

begin

  {从 Wave 文件中读取格式与波形数据}

  if not GetWaveFmt(wavPath, wavFormat) then Exit;

  wavData := TMemoryStream.Create;

  if not GetWaveData(wavPath, wavData) then begin wavData.Free; Exit; end;



  {建立设备对象, 并设置写作优先级}

  DirectSoundCreate8(nil, myDSound, nil);

  myDSound.SetCooperativeLevel(Self.Handle, DSSCL_NORMAL);



  {填充建立缓冲区需要的结构}

  ZeroMemory(@bufDesc, SizeOf(TDSBufferDesc));

  bufDesc.dwSize := SizeOf(TDSBufferDesc);

  bufDesc.dwFlags := DSBCAPS_STATIC;     //指定使用静态缓冲区

  bufDesc.dwBufferBytes := wavData.Size; //数据大小

  bufDesc.lpwfxFormat := @wavFormat;     //数据格式

//  bufDesc.guid3DAlgorithm := DS3DALG_DEFAULT; //这个暂不需要



  {建立缓冲区}

  myDSound.CreateSoundBuffer(bufDesc, buf, nil);



  {锁定缓冲区内存以获取写入地址和写入大小}

  buf.Lock(0, 0, @p1, @n1, nil, nil, DSBLOCK_ENTIREBUFFER);

  {写入}

  wavData.Position := 0;

  CopyMemory(p1, wavData.Memory, n1);

  wavData.Free;

  {解锁}

  buf.Unlock(p1, n1, nil, 0);



  {播放}

  buf.Play(0, 0, 0);



  Button1.Enabled := False;

  Button2.Enabled := True;

  Button3.Enabled := True;

end;



{循环播放}

procedure TForm1.Button2Click(Sender: TObject);

begin

  buf.Play(0, 0, DSBPLAY_LOOPING);

end;



{暂停播放}

procedure TForm1.Button3Click(Sender: TObject);

begin

  buf.Stop;

end;



//释放接口, 不然会有内存泄露(因为此缓冲区的生命周期可能会长于应用程序); 且释放顺序也很重要

procedure TForm1.FormDestroy(Sender: TObject);

begin

  buf := nil;

  myDSound := nil;

end;



end.


你可能感兴趣的:(Delphi)