delphiaudiocallbackmute

Delphi - Sound change notification (mute / unmute)


how can I get notified of system audio changes?

Or how to use the callback functions

function RegisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function UnregisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function RegisterEndpointNotificationCallback(pClient: IMMNotificationClient): Hresult; stdcall;

Solution

  • First a disclaimer: I am not an expert on the audio APIs. Still, I can get it to work using the documentation.

    First, we need to get hold of an IMMDeviceEnumerator interface using CoCreateInstance. Then we use the IMMDeviceEnumerator.GetDefaultAudioEndpoint method to obtain the default audio output device. Using the device's Activate method, we request an IAudioEndpointVolume interface and call its RegisterControlChangeNotify method to subscribe to volume notifications, including mute and unmute.

    We must provide a recipient for these notifications, and that recipient must implement the IAudioEndpointVolumeCallback interface, which specifies how the recipient object actually does receive the notifications.

    In a single-form GUI application, like the demo application I wrote for this answer, it makes sense to use the main form. Hence, we must let the form implement the IAudioEndpointVolumeCallback.OnNotify method. This method is called by the audio system when the volume is changed (or (un)muted), and the notification data is passed in a AUDIO_VOLUME_NOTIFICATION_DATA structure.

    I don't want to touch the GUI or risk raising exceptions in this method, so just to feel safe I only let this method post a message to the form with the required data.

    Full code:

    unit OSD;
    
    interface
    
    uses
      Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
      System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, ActiveX,
      ComObj, AudioEndpoint, Gauge;
    
      // Gauge: https://specials.rejbrand.se/dev/controls/gauge/
    
    const
      WM_VOLNOTIFY = WM_USER + 1;
    
    type
      TSndVolFrm = class(TForm, IAudioEndpointVolumeCallback)
        ArcGauge: TArcGauge;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        FDeviceEnumerator: IMMDeviceEnumerator;
        FMMDevice: IMMDevice;
        FAudioEndpointVolume: IAudioEndpointVolume;
        function OnNotify(pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT;
          stdcall;
        procedure WMVolNotify(var Msg: TMessage); message WM_VOLNOTIFY;
      public
      end;
    
    var
      SndVolFrm: TSndVolFrm;
    
    implementation
    
    uses
      Math;
    
    {$R *.dfm}
    
    procedure TSndVolFrm.FormCreate(Sender: TObject);
    begin
    
      if not Succeeded(CoInitialize(nil)) then
        ExitProcess(1);
    
      OleCheck(CoCreateInstance(CLASS_IMMDeviceEnumerator, nil, CLSCTX_INPROC_SERVER,
        IID_IMMDeviceEnumerator, FDeviceEnumerator));
      OleCheck(FDeviceEnumerator.GetDefaultAudioEndpoint(0, 0, FMMDevice));
      OleCheck(FMMDevice.Activate(IID_IAudioEndpointVolume, CLSCTX_INPROC_SERVER, nil, FAudioEndpointVolume));
      OleCheck(FAudioEndpointVolume.RegisterControlChangeNotify(Self));
    
    end;
    
    procedure TSndVolFrm.FormDestroy(Sender: TObject);
    begin
      CoUninitialize;
    end;
    
    function TSndVolFrm.OnNotify(pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT;
    begin
      if pNotify = nil then
        Exit(E_POINTER);
      try
        PostMessage(Handle, WM_VOLNOTIFY, WPARAM(pNotify.bMuted <> False), LPARAM(Round(100 * pNotify.fMasterVolume)));
        Result := S_OK;
      except
        Result := E_UNEXPECTED;
      end;
    end;
    
    procedure TSndVolFrm.WMVolNotify(var Msg: TMessage);
    begin
    
      var LMute := Msg.WParam <> 0;
      var LVolume := Msg.LParam;
    
      if LMute then
      begin
        ArcGauge.ShowCaption := False;
        ArcGauge.FgBrush.Color := $777777;
      end
      else
      begin
        ArcGauge.ShowCaption := True;
        ArcGauge.FgBrush.Color := clHighlight;
      end;
    
      ArcGauge.Position := LVolume;
    
    end;
    
    end.
    

    Screen recorder of application in action, responding to volume changes including mute and unmute.

    Interface unit:

    unit AudioEndpoint;
    
    interface
    
    uses
      Windows,
      Messages,
      SysUtils,
      ActiveX,
      ComObj;
    
    const
      CLASS_IMMDeviceEnumerator : TGUID = '{BCDE0395-E52F-467C-8E3D-C4579291692E}';
      IID_IMMDeviceEnumerator : TGUID = '{A95664D2-9614-4F35-A746-DE8DB63617E6}';
      IID_IAudioEndpointVolume : TGUID = '{5CDF2C82-841E-4546-9722-0CF74078229A}';
    
    type
      PAUDIO_VOLUME_NOTIFICATION_DATA = ^AUDIO_VOLUME_NOTIFICATION_DATA;
      AUDIO_VOLUME_NOTIFICATION_DATA = record
        guidEventContext: TGUID;
        bMuted: BOOL;
        fMasterVolume: Single;
        nChannels: UINT;
        afChannelVolumes: Single;
      end;
    
      IAudioEndpointVolumeCallback = interface(IUnknown)
        ['{657804FA-D6AD-4496-8A60-352752AF4F89}']
        function OnNotify(pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT; stdcall;
      end;
    
      IAudioEndpointVolume = interface(IUnknown)
        ['{5CDF2C82-841E-4546-9722-0CF74078229A}']
        function RegisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): HRESULT; stdcall;
        function UnregisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): HRESULT; stdcall;
        function GetChannelCount(out PInteger): HRESULT; stdcall;
        function SetMasterVolumeLevel(fLevelDB: single; pguidEventContext: PGUID): HRESULT; stdcall;
        function SetMasterVolumeLevelScalar(fLevelDB: single; pguidEventContext: PGUID): HRESULT; stdcall;
        function GetMasterVolumeLevel(out fLevelDB: single): HRESULT; stdcall;
        function GetMasterVolumeLevelScaler(out fLevelDB: single): HRESULT; stdcall;
        function SetChannelVolumeLevel(nChannel: Integer; fLevelDB: double; pguidEventContext: PGUID): HRESULT; stdcall;
        function SetChannelVolumeLevelScalar(nChannel: Integer; fLevelDB: double; pguidEventContext: PGUID): HRESULT; stdcall;
        function GetChannelVolumeLevel(nChannel: Integer; out fLevelDB: double): HRESULT; stdcall;
        function GetChannelVolumeLevelScalar(nChannel: Integer; out fLevel: double): HRESULT; stdcall;
        function SetMute(bMute: Boolean; pguidEventContext: PGUID): HRESULT; stdcall;
        function GetMute(out bMute: Boolean): HRESULT; stdcall;
        function GetVolumeStepInfo(pnStep: Integer; out pnStepCount: Integer): HRESULT; stdcall;
        function VolumeStepUp(pguidEventContext: PGUID): HRESULT; stdcall;
        function VolumeStepDown(pguidEventContext: PGUID): HRESULT; stdcall;
        function QueryHardwareSupport(out pdwHardwareSupportMask): HRESULT; stdcall;
        function GetVolumeRange(out pflVolumeMindB: double; out pflVolumeMaxdB: double; out pflVolumeIncrementdB: double): HRESULT; stdcall;
      end;
    
      IAudioMeterInformation = interface(IUnknown)
      ['{C02216F6-8C67-4B5B-9D00-D008E73E0064}']
      end;
    
      IPropertyStore = interface(IUnknown)
      end;
    
      IMMDevice = interface(IUnknown)
      ['{D666063F-1587-4E43-81F1-B948E807363F}']
        function Activate(const refId: TGUID; dwClsCtx: DWORD;  pActivationParams: PInteger; out pEndpointVolume: IAudioEndpointVolume): HRESULT; stdCall;
        function OpenPropertyStore(stgmAccess: DWORD; out ppProperties: IPropertyStore): HRESULT; stdcall;
        function GetId(out ppstrId: PLPWSTR): HRESULT; stdcall;
        function GetState(out State: Integer): HRESULT; stdcall;
      end;
    
    
      IMMDeviceCollection = interface(IUnknown)
      ['{0BD7A1BE-7A1A-44DB-8397-CC5392387B5E}']
      end;
    
      IMMNotificationClient = interface(IUnknown)
      ['{7991EEC9-7E89-4D85-8390-6C703CEC60C0}']
      end;
    
      IMMDeviceEnumerator = interface(IUnknown)
      ['{A95664D2-9614-4F35-A746-DE8DB63617E6}']
        function EnumAudioEndpoints(dataFlow: TOleEnum; deviceState: SYSUINT; DevCollection: IMMDeviceCollection): HRESULT; stdcall;
        function GetDefaultAudioEndpoint(EDF: SYSUINT; ER: SYSUINT; out Dev :IMMDevice ): HRESULT; stdcall;
        function GetDevice(pwstrId: pointer; out Dev: IMMDevice): HRESULT; stdcall;
        function RegisterEndpointNotificationCallback(pClient: IMMNotificationClient): HRESULT; stdcall;
      end;
    
    implementation
    
    end.