zoukankan      html  css  js  c++  java
  • 在 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.
    

  • 相关阅读:
    【C/C++开发】c++ 工具库 (zz)
    【机器学习】半监督学习
    【Python开发】Pycharm下的Anaconda配置
    【C/C++开发】emplace_back() 和 push_back 的区别
    【C/C++开发】容器set和multiset,C++11对vector成员函数的扩展(cbegin()、cend()、crbegin()、crend()、emplace()、data())
    【C/C++开发】C++11 并发指南三(std::mutex 详解)
    【C/C++开发】C++11 并发指南二(std::thread 详解)
    【C/C++开发】C++11 并发指南一(C++11 多线程初探)
    【C/C++开发】STL内嵌数据类型: value_type
    个股实时监控之综述
  • 原文地址:https://www.cnblogs.com/del/p/1936506.html
Copyright © 2011-2022 走看看