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