Reputation: 81
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;
Upvotes: 1
Views: 1317
Reputation: 12292
I have some code for you, 3 source code files: A unit with a class handling volume control notification, a unit to interface with Windows API and a simple demo program. The demo is actually all you have to look in details. The rest can be considered as obscure support routines :-)
Let's see the demo program. It is a simple VCL form having only a TMemo on it. It register for volume control notification and display a simple message in the memo (You probably want a nice UI instead).
The code is really very simple: create an interface pointing to TVolumeControl, assign an event handler to the OnVolumeChange and call the Initialize method. When the event fires, call GetLevelInfo to get the information and display it. When the form is destroyed, call Dispose method to stop getting notification.
unit SoundChangeNotificationDemoMain;
interface
uses
Winapi.Windows, Winapi.Messages, Winapi.ActiveX,
System.SysUtils, System.Variants, System.Classes, System.SyncObjs,
System.Win.ComObj,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Ovb.VolumeMonitor,
Ovb.MMDevApi;
type
TSoundChangeDemoForm = class(TForm)
Memo1: TMemo;
protected
FVolumeMonitor : IVolumeMonitor;
procedure VolumeMonitorVolumeChange(Sender : TObject);
public
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
end;
var
SoundChangeDemoForm: TSoundChangeDemoForm;
implementation
{$R *.dfm}
constructor TSoundChangeDemoForm.Create(AOwner: TComponent);
var
HR : HRESULT;
begin
inherited;
FVolumeMonitor := TVolumeMonitor.Create;
FVolumeMonitor.OnVolumeChange := VolumeMonitorVolumeChange;
HR := FVolumeMonitor.Initialize();
if not SUCCEEDED(HR) then
ShowMessage('Volume control initialization failed');
end;
destructor TSoundChangeDemoForm.Destroy;
begin
FVolumeMonitor.Dispose;
inherited Destroy;
end;
procedure TSoundChangeDemoForm.VolumeMonitorVolumeChange(Sender: TObject);
var
Info: TVOLUME_INFO;
begin
FVolumeMonitor.GetLevelInfo(Info);
Memo1.Lines.Add(Format('Volume change: nStep=%d cSteps=%d Mute=%d',
[Info.nStep, Info.cSteps, Ord(Info.bMuted)]));
end;
The hard work is done is a unit I named Ovb.VolumeMonitor. This unit interact with Windows API to request notification when the volume is changed on the default audio device.
Note that this is not a component but a class and you use this class thru an interface. See the demo app above.
unit Ovb.VolumeMonitor;
interface
uses
Winapi.Windows, Winapi.Messages, Winapi.ActiveX,
System.SysUtils, System.Variants, System.Classes, System.SyncObjs,
System.Win.ComObj,
Ovb.MMDevApi;
const
WM_VOLUMECHANGE = (WM_USER + 12);
WM_ENDPOINTCHANGE = (WM_USER + 13); // Not implemented yet
type
IVolumeMonitor = interface
['{B06EE2E9-E707-4086-829A-D5664978069F}']
function Initialize() : HRESULT;
procedure Dispose;
function GetLevelInfo(var Info: TVOLUME_INFO) : HRESULT;
function GetOnVolumeChange: TNotifyEvent;
procedure SetOnVolumeChange(const Value: TNotifyEvent);
property OnVolumeChange : TNotifyEvent read GetOnVolumeChange
write SetOnVolumeChange;
end;
TVolumeMonitor = class(TInterfacedObject,
IVolumeMonitor,
IMMNotificationClient,
IAudioEndpointVolumeCallback)
private
FRegisteredForEndpointNotifications : BOOL;
FRegisteredForVolumeNotifications : BOOL;
FDeviceEnumerator : IMMDeviceEnumerator;
FAudioEndpoint : IMMDevice;
FAudioEndpointVolume : IAudioEndpointVolume;
FEndPointCritSect : TRTLCriticalSection;
FWindowHandle : HWND;
FOnVolumeChange : TNotifyEvent;
procedure WndProc(var Msg: TMessage);
procedure WMVolumeChange(var Msg: TMessage);
function GetOnVolumeChange: TNotifyEvent;
procedure SetOnVolumeChange(const Value: TNotifyEvent);
function AttachToDefaultEndpoint() : HRESULT;
function OnNotify(pNotify : PAUDIO_VOLUME_NOTIFICATION_DATA) : HRESULT; stdcall;
public
constructor Create; virtual;
destructor Destroy; override;
function Initialize() : HRESULT;
procedure DetachFromEndpoint();
procedure Dispose;
function GetLevelInfo(var Info: TVOLUME_INFO) : HRESULT;
property OnVolumeChange : TNotifyEvent read GetOnVolumeChange
write SetOnVolumeChange;
end;
implementation
{ TVolumeMonitor }
constructor TVolumeMonitor.Create;
begin
inherited Create;
FWindowHandle := AllocateHWnd(WndProc);
FRegisteredForEndpointNotifications := FALSE;
FRegisteredForVolumeNotifications := FALSE;
FEndPointCritSect.Initialize();
end;
destructor TVolumeMonitor.Destroy;
begin
if FWindowHandle <> INVALID_HANDLE_VALUE then begin
DeallocateHWnd(FWindowHandle);
FWindowHandle := INVALID_HANDLE_VALUE;
end;
FEndPointCritSect.Free;
inherited Destroy;
end;
// Initialize this object. Call after constructor.
function TVolumeMonitor.Initialize: HRESULT;
var
hr : HRESULT;
begin
hr := CoCreateInstance(CLASS_IMMDeviceEnumerator,
nil,
CLSCTX_INPROC_SERVER,
IID_IMMDeviceEnumerator,
FDeviceEnumerator);
if SUCCEEDED(hr) then begin
hr := FDeviceEnumerator.RegisterEndpointNotificationCallback(Self);
if SUCCEEDED(hr) then
hr := AttachToDefaultEndpoint();
end;
Result := hr;
end;
function TVolumeMonitor.AttachToDefaultEndpoint: HRESULT;
var
hr : HRESULT;
begin
FEndPointCritSect.Enter();
// Get the default music & movies playback device
hr := FDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eMultimedia, FAudioEndpoint);
if SUCCEEDED(hr) then begin
// Get the volume control for it
hr := FAudioEndpoint.Activate(IAudioEndpointVolume, CLSCTX_INPROC_SERVER, nil, FAudioEndpointVolume);
if SUCCEEDED(hr) then begin
// Register for callbacks
hr := FAudioEndpointVolume.RegisterControlChangeNotify(self);
FRegisteredForVolumeNotifications := SUCCEEDED(hr);
end;
end;
FEndPointCritSect.Leave();
Result := hr;
end;
// Stop monitoring the device and release all associated references
procedure TVolumeMonitor.DetachFromEndpoint();
begin
FEndPointCritSect.Enter();
if FAudioEndpointVolume <> nil then begin
// be sure to unregister...
if FRegisteredForVolumeNotifications then begin
FAudioEndpointVolume.UnregisterControlChangeNotify(Self);
FRegisteredForVolumeNotifications := FALSE;
end;
FAudioEndpointVolume := nil
end;
if FAudioEndpoint <> nil then
FAudioEndpoint := nil;
FEndPointCritSect.Leave();
end;
// Call when the app is done with this object before calling release.
// This detaches from the endpoint and releases all audio service references.
procedure TVolumeMonitor.Dispose;
begin
DetachFromEndpoint();
if FRegisteredForEndpointNotifications then begin
FDeviceEnumerator.UnregisterEndpointNotificationCallback(Self);
FRegisteredForEndpointNotifications := FALSE;
end;
end;
function TVolumeMonitor.GetLevelInfo(var Info: TVOLUME_INFO): HRESULT;
var
hr : HRESULT;
begin
hr := E_FAIL;
FEndPointCritSect.Enter();
if FAudioEndpointVolume <> nil then begin
hr := FAudioEndpointVolume.GetMute(Info.bMuted);
if SUCCEEDED(hr) then
hr := FAudioEndpointVolume.GetVolumeStepInfo(Info.nStep, Info.cSteps);
end;
FEndPointCritSect.Leave();
Result := hr;
end;
function TVolumeMonitor.GetOnVolumeChange: TNotifyEvent;
begin
Result := FOnVolumeChange;
end;
// Callback for Windows API
function TVolumeMonitor.OnNotify(
pNotify: PAUDIO_VOLUME_NOTIFICATION_DATA): HRESULT;
begin
if FWindowHandle <> INVALID_HANDLE_VALUE then
PostMessage(FWindowHandle, WM_VOLUMECHANGE, 0, 0);
Result := S_OK;
end;
procedure TVolumeMonitor.SetOnVolumeChange(const Value: TNotifyEvent);
begin
FOnVolumeChange := Value;
end;
procedure TVolumeMonitor.WMVolumeChange(var Msg: TMessage);
begin
if Assigned(FOnVolumeChange) then
FOnVolumeChange(Self);
end;
procedure TVolumeMonitor.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_VOLUMECHANGE : WMVolumeChange(Msg);
else
Winapi.Windows.DefWindowProc(FWindowHandle, Msg.Msg, Msg.WParam, Msg.LParam);
end;
end;
Finally, tho interact with Windows API, we need a few declarations for structure and interfaces that Windows make use.
unit Ovb.MMDevApi;
interface
uses
WinApi.Windows,
WinApi.ActiveX;
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}';
// Data-flow direction
eRender = $00000000;
eCapture = $00000001;
eAll = $00000002;
// Role constant
eConsole = $00000000;
eMultimedia = $00000001;
eCommunications = $00000002;
type
TAUDIO_VOLUME_NOTIFICATION_DATA = record
guidEventContext : TGUID;
Muted : BOOL;
fMasterVolume : Single;
nChannels : UINT;
afChannelVolumes : array [1..1] of Single;
end;
PAUDIO_VOLUME_NOTIFICATION_DATA = ^TAUDIO_VOLUME_NOTIFICATION_DATA;
TVOLUME_INFO = record
nStep : UINT;
cSteps : UINT;
bMuted : BOOL;
end;
PVOLUME_INFO = ^TVOLUME_INFO;
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: BOOL): HRESULT; stdcall;
function GetVolumeStepInfo(out pnStep: UINT; out pnStepCount: UINT): 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;
function UnregisterEndpointNotificationCallback(pClient: IMMNotificationClient): HRESULT; stdcall;
end;
implementation
end.
Upvotes: 2
Reputation: 108948
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.
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.
Upvotes: 4