您的位置:首页 > 编程语言 > Delphi

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

2011-01-15 23:43 633 查看
建立 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, 但种种原因还是放弃了, 主要还是想了解得透彻些.

程序用到了以前写过的两个函数:

/article/6946015.html

/article/6946015.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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: