实现 Core Audio API 事件

Posted

技术标签:

【中文标题】实现 Core Audio API 事件【英文标题】:Implementing Core Audio API events 【发布时间】:2016-06-15 04:58:01 【问题描述】:

尝试为 Windows Core Audio API(Win7 64 位 Delphi XE5)实现事件。我的目标是跟踪音量混合器中的应用程序,以使不在我的列表中的音频会话静音,并为我的目标应用程序调整音量。我成功地枚举了音频设备和会话,将音频静音并在每个会话的基础上调整音量,但我正在努力处理事件。我需要的是在添加新会话和关闭会话时收到通知,以便我可以再次枚举。我可以使用计时器来枚举会话,但我宁愿避免这种情况。

不工作的具体事件是IAudiosessionNotificationIMMNotificationClient

我的问题如下:

    我为事件派生类的方法是否过于简单?一世 在这里找到了一个更多涉及的示例: Catch audio sessions events ,但它似乎也不起作用(未经个人测试) 虽然IAudioEndpointVolumeCallback 正在“工作”,但我认为代码 有异味,因为我在 OnNotify 函数中引用 UI 元素 所以我想要一些反馈/指针。这是一个有效的实现吗?

我有两个单元:包含主窗体的 uAudioUI 和包含 Core Audio 接口的 MMDevApi 单元。

我当前代码的相关部分如下所示(它是一个测试应用):

MMDevApi.pas

...
  IAudioEndpointVolumeCallback = interface(IUnknown)
  ['657804FA-D6AD-4496-8A60-352752AF4F89']
    function OnNotify(pNotify:PAUDIO_VOLUME_NOTIFICATION_DATA):HRESULT; stdcall;
  end;

  PIMMNotificationClient = ^IMMNotificationClient;
  IMMNotificationClient = interface(IUnknown)
  ['7991EEC9-7E89-4D85-8390-6C703CEC60C0']
     function OnDefaultDeviceChanged(const flow: EDataFlow; const role: ERole; const pwstrDefaultDevice: LPCWSTR):HRESULT; stdcall;
     function OnDeviceAdded(const pwstrDeviceId: LPCWSTR):HRESULT; stdcall;
     function OnDeviceRemoved(const pwstrDeviceId: LPCWSTR):HRESULT; stdcall;
     function OnDeviceStateChanged(const pwstrDeviceID:LPCWSTR; const dwNewState: DWORD):HRESULT; stdcall;
     function OnPropertyValueChanged(const pwstrDeviceID:LPCWSTR; const key: PROPERTYKEY):HRESULT; stdcall;
  end;

  IAudioSessionNotification = interface(IUnknown)
    ['641DD20B-4D41-49CC-ABA3-174B9477BB08']
      function OnSessionCreated(const NewSession: IAudioSessionControl): HResult; stdcall;
  end;

在主窗体单元中,我为所需接口派生类:

uAudioUI.pas
...
type

  TEndpointVolumeCallback = class(TInterfacedObject, IAudioEndpointVolumeCallback)
  public
    function OnNotify(pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT; stdcall;
  end;

  TMMNotificationClient = class(TInterfacedObject, IMMNotificationClient)
    function OnDefaultDeviceChanged(const flow: EDataFlow; const role: ERole; const pwstrDefaultDevice: LPCWSTR):HRESULT; stdcall;
    function OnDeviceAdded(const pwstrDeviceId: LPCWSTR):HRESULT; stdcall;
    function OnDeviceRemoved(const pwstrDeviceId: LPCWSTR):HRESULT; stdcall;
    function OnDeviceStateChanged(const pwstrDeviceID:LPCWSTR; const dwNewState: DWORD):HRESULT; stdcall;
    function OnPropertyValueChanged(const pwstrDeviceID:LPCWSTR; const key: PROPERTYKEY):HRESULT; stdcall;
  end;

  TAudioMixerSessionCallback = class(TInterfacedObject, IAudioSessionEvents)
    function OnDisplayNameChanged(NewDisplayName:LPCWSTR; EventContext:pGuid):HResult; stdcall;
    function OnIconPathChanged(NewIconPath:LPCWSTR; EventContext:pGuid):HResult; stdcall;
    function OnSimpleVolumeChanged(NewVolume:Single; NewMute:LongBool; EventContext:pGuid):HResult; stdcall;
    function OnChannelVolumeChanged(ChannelCount:uint; NewChannelArray:PSingle; ChangedChannel:uint;
                                EventContext:pGuid):HResult; stdcall;
    function OnGroupingParamChanged(NewGroupingParam, EventContext:pGuid):HResult; stdcall;
    function OnStateChanged(NewState:uint):HResult; stdcall;  // AudioSessionState
    function OnSessionDisconnected(DisconnectReason:uint):HResult; stdcall; // AudioSessionDisconnectReason
  end;

  TAudioSessionCallback = class(TInterfacedObject, IAudioSessionNotification)
    function OnSessionCreated(const NewSession: IAudioSessionControl): HResult; stdcall;
  end;

为简单起见,我使用全局变量

  private
     Private declarations   
    FDefaultDevice           : IMMDevice;
    FAudioEndpointVolume     : IAudioEndpointVolume;
    FDeviceEnumerator        : IMMDeviceEnumerator;
    FAudioClient             : IAudioClient;
    FAudioSessionManager     : IAudioSessionManager2;
    FAudioSessionControl     : IAudioSessionControl2;
    FEndpointVolumeCallback  : IAudioEndpointVolumeCallback;
    FAudioSessionEvents      : IAudioSessionEvents;
    FMMNotificationCallback  : IMMNotificationClient;
    FPMMNotificationCallback : PIMMNotificationClient;
    FAudioSessionCallback    : TAudioSessionCallback;

...

procedure TForm1.FormCreate(Sender: TObject);
var
  ...
begin
  hr := CoCreateInstance(CLASS_IMMDeviceEnumerator, nil, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator, FDeviceEnumerator);
  if hr = ERROR_SUCCESS then
  begin
    hr := FDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, FDefaultDevice);
    if hr <> ERROR_SUCCESS then Exit;

    //get the master audio endpoint
    hr := FDefaultDevice.Activate(IID_IAudioEndpointVolume, CLSCTX_INPROC_SERVER, nil, IUnknown(FAudioEndpointVolume));
    if hr <> ERROR_SUCCESS then Exit;
    hr := FDefaultDevice.Activate(IID_IAudioClient, CLSCTX_ALL, nil, IUnknown(FAudioClient));
    if hr <> ERROR_SUCCESS then Exit;

    //volume handler
    FEndpointVolumeCallback := TEndpointVolumeCallback.Create;
    if FAudioEndpointVolume.RegisterControlChangeNotify(FEndPointVolumeCallback) = ERROR_SUCCESS then
      FEndpointVolumeCallback._AddRef;

    //device change / ex: cable unplug handler
    FMMNotificationCallback := TMMNotificationClient.Create;
    FPMMNotificationCallback := @FMMNotificationCallback;
    if FDeviceEnumerator.RegisterEndpointNotificationCallback(FPCableUnpluggedCallback) = ERROR_SUCCESS then
      FMMNotificationCallback._AddRef;

...最后是类函数

 TEndpointVolumeCallback 
function TEndpointVolumeCallback.OnNotify(pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT;
var
  audioLevel : integer;
begin
  //NOTE: this works..
  audioLevel := Round(pNotify.fMasterVolume * 100);
  Form1.trackVolumeLevel.Position := audioLevel;

  if pNotify.bMuted then
  begin
    form1.trackVolumeLevel.Enabled := False; 
    form1.spdMute.Caption := 'X';
  end
  else
  begin
    form1.trackVolumeLevel.Enabled := True; 
    form1.spdMute.Caption := 'O';
  end;

  Result := S_OK;

end;

 TMMNotificaionClient 
function TMMNotificationClient.OnDefaultDeviceChanged(const flow: EDataFlow; const role: ERole; const pwstrDefaultDevice: LPCWSTR): HRESULT;
begin
  //NOTE: this crashes - referencing a pointer to add 000000000
  Form1.Label2.Caption := 'Audio device changed';
  Result := S_OK;
end;

 AudioMixerSessionCallback 

function TAudioMixerSessionCallback.OnSimpleVolumeChanged(NewVolume: Single; NewMute: LongBool; EventContext: PGUID): HRESULT;
begin
  //NOTE: This works...
  Form1.trackSessionVolumeLevel.Position := Round(NewVolume * 100);
  Form1.Label2.Caption := EventContext.ToString;
  Result := S_OK;
end;

 AudioSessionCallback 

function TAudioSessionCallback.OnSessionCreated(const NewSession: IAudioSessionControl): HRESULT;
begin
  //NOTE: This never gets called...
  Form1.Label2.Caption := 'New audio session created';
  Result := S_OK;

end;

【问题讨论】:

问题2:用GetCurrentThreadId()检查事件是否发生在主线程中,如果没有,则必须sync()。 @whordaddy,谢谢。 GetCurrentThreadId() 表明偶数没有在主线程中运行。从我读到的 synchronize() 似乎设计不好,而 PostMessage/SendMessage 会更好。 关于问题1,我实现了一个类似***.com/questions/858974/…的类,效果一样;事件不会被调用。不过,这个练习让我明白了这样一个带有 PostMessage 的类的价值。我可以将事件数据存储在类中,调用 PostMessage,然后在我的主线程中从类中检索数据。 Lowrider:Thread.Queue() 类似于 PostMessage。使用你喜欢的,记住你需要一个 PostMessage 的窗口句柄(可能并不总是存在) 【参考方案1】:

我认为代码是从 C/C++ 翻译而来的? 使用 TInterfacedObject 时,不需要 _AddRef 等方法,因为 TInterfacedObject 会处理这些。

另一个建议:我缺少线程实现。通常这是在构造函数或初始化部分中声明的。

例子:

initialization
  CoInitializeEx(Nil,
                 COINIT_APARTMENTTHREADED);

//Create method
  inherited Create(); 
  CoInitializeEx(Nil,
                 COINIT_APARTMENTTHREADED);

这在使用 UI 实现时很重要。否则您将不会收到任何事件。 非 UI 实现(如驱动程序)应使用 COINIT_MULTITHREADED 模型。

一些注意事项:

不要使用指针,如PGUID,而是使用TGUID。当在 C++ 中声明一个字段时,它可以以 ie pSingle 开头。在 Delphi 中,这应该是 Single。当 C++ 使用指向指针的指针(如 ppSingle)时 - 在大多数情况下 - 在 Delphi 中,这将是一个 PSingle。

你还声明了function OnChannelVolumeChanged 错误。

应该是:

function OnChannelVolumeChanged(ChannelCount: UINT;
                                NewChannelArray: Array of Single;
                                ChangedChannel: UINT;
                                EventContext: TGUID): HResult; stdcall;

【讨论】:

抱歉延迟回复,但我已经搁置了一段时间。此回复中的提示帮助我找到了答案。完整的答案是 Delphi 在单线程模式下自动初始化 COM,所以我必须取消默认值,然后如上所述使用线程 CoInitializeEx 重新初始化。然后我开始接收事件。

以上是关于实现 Core Audio API 事件的主要内容,如果未能解决你的问题,请参考以下文章

video/audio API 事件

ASP.NET Core Web API下事件驱动型架构的实现:一个简单的实现

使用 Web Audio API 将两个声音分配给两个 Div,并通过带有 javascript 的 click 事件独立播放

无法从 ASP.NET Core 6.0 API NServiceBus 端点订阅事件

具有未处理异常的 Nlog 事件属性的 .Net Core Web Api

从 Audio-Unit 渲染回调实时触发事件并获得严重失真