Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing Core Audio API events

Trying to implement events for Windows Core Audio API (Win7 64-bit Delphi XE5). My objective is to track the applications in the Volume Mixer to mute audio sessions that are not in my list and to adjust the volume for my target applications. I successfully enumerate the audio devices and the sessions, mute the audio and adjust the volume on a per-session basis but I am struggling with events. What I need is to get notified when new sessions are added and when sessions close so that I can enumerate again. I could use a timer to enumerate the session but I would prefer to avoid that.

The specific events that are not working are IAudioSessionNotification and IMMNotificationClient.

My questions are follows:

  1. Is my approach to deriving classes for events too simplistic? I found an example that is much more involved here: Catch audio sessions events , but it does not seem to work either (not tested personally)
  2. Although IAudioEndpointVolumeCallback is "working" I think the code smells because I am referencing UI elements in the OnNotify function so I'd like some feedback/pointers. Is that a valid implementation?

I have two units: uAudioUI which contains the main form and MMDevApi unit that contains Core Audio interface.

The relevant parts of my code current looks like this (its a test app):

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;

In the main form unit I derive classes for the required interfaces:

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;

For simplicity I use globals

  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;

... and then finally, the class functions

{ 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;
like image 708
Alain Thiffault Avatar asked Apr 24 '26 05:04

Alain Thiffault


1 Answers

I think the code is a translation from C/C++ ? When using the TInterfacedObject, you don't need the _AddRef etc. methods, because the TInterfacedObject will handle those.

Another suggestion: I'm missing the threading implementation. Normally this is declared in the constructor or initialization section.

Example:

initialization
  CoInitializeEx(Nil,
                 COINIT_APARTMENTTHREADED);

or

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

This is important when using an UI implementation. Otherwise you will not receive any events. Non UI implementations (like drivers) should use the COINIT_MULTITHREADED model.

Some notes:

Instead of using pointers, like PGUID, use TGUID. When a field is declared in C++, it could be starting with ie pSingle. In Delphi this should be Single. When C++ is using pointer to pointers (like ppSingle) then - in most cases - in Delphi this would be a PSingle.

Also you declared function OnChannelVolumeChanged wrong.

It should be:

function OnChannelVolumeChanged(ChannelCount: UINT;
                                NewChannelArray: Array of Single;
                                ChangedChannel: UINT;
                                EventContext: TGUID): HResult; stdcall;
like image 187
ToKa Avatar answered Apr 26 '26 18:04

ToKa



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!