在 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 下使用 DirectSound (3): 播放第一个 Wave 文件)