Delphi - 声音变化通知(静音/取消静音)
Delphi - Sound change notification (mute / unmute)
如何获得系统音频更改的通知?
或者回调函数的使用方法
function RegisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function UnregisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function RegisterEndpointNotificationCallback(pClient: IMMNotificationClient): Hresult; stdcall;
首先声明:我不是音频 API 方面的专家。不过,我可以使用 documentation.
让它工作
首先,我们需要掌握一个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
方法来订阅音量通知,包括静音和取消静音。
我们必须为这些通知提供一个收件人,并且该收件人必须实现 IAudioEndpointVolumeCallback
接口,该接口指定收件人对象实际接收通知的方式。
在单窗体 GUI 应用程序中,如我为此答案编写的演示应用程序,使用主窗体是有意义的。因此,我们必须让表单实现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
结构。
我不想在这个方法中接触 GUI 或冒引发异常的风险,所以为了安全起见,我只让这个方法 post 向表单发送包含所需数据的消息。
完整代码:
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 := 7777;
end
else
begin
ArcGauge.ShowCaption := True;
ArcGauge.FgBrush.Color := clHighlight;
end;
ArcGauge.Position := LVolume;
end;
end.
接口单元:
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.
我有一些代码给你,3 个源代码文件:一个带有 class 处理音量控制通知的单元,一个与 Windows API 接口的单元和一个简单的演示程序。该演示实际上是您需要详细查看的全部内容。其余的可以认为是晦涩的支持例程:-)
让我们看看演示程序。它是一个简单的 VCL 表单,上面只有一个 TMemo。它注册音量控制通知并在备忘录中显示一条简单消息(您可能想要一个不错的 UI)。
代码真的很简单:创建一个指向TVolumeControl的接口,为OnVolumeChange分配一个事件处理程序并调用Initialize方法。当事件触发时,调用 GetLevelInfo 获取信息并显示它。当表单被销毁时,调用Dispose方法停止接收通知。
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;
功夫不负有心人是我命名的一个单元Ovb.VolumeMonitor。此单元与 Windows API 交互以在默认音频设备上更改音量时请求通知。
请注意,这不是一个组件,而是一个 class,您可以通过一个界面使用这个 class。请参阅上面的演示应用程序。
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;
最后,为了与 Windows API 交互,我们需要一些 Windows 使用的结构和接口声明。
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 = [=12=]000000;
eCapture = [=12=]000001;
eAll = [=12=]000002;
// Role constant
eConsole = [=12=]000000;
eMultimedia = [=12=]000001;
eCommunications = [=12=]000002;
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.
如何获得系统音频更改的通知?
或者回调函数的使用方法
function RegisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function UnregisterControlChangeNotify(AudioEndPtVol: IAudioEndpointVolumeCallback): Integer; stdcall;
function RegisterEndpointNotificationCallback(pClient: IMMNotificationClient): Hresult; stdcall;
首先声明:我不是音频 API 方面的专家。不过,我可以使用 documentation.
让它工作首先,我们需要掌握一个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
方法来订阅音量通知,包括静音和取消静音。
我们必须为这些通知提供一个收件人,并且该收件人必须实现 IAudioEndpointVolumeCallback
接口,该接口指定收件人对象实际接收通知的方式。
在单窗体 GUI 应用程序中,如我为此答案编写的演示应用程序,使用主窗体是有意义的。因此,我们必须让表单实现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
结构。
我不想在这个方法中接触 GUI 或冒引发异常的风险,所以为了安全起见,我只让这个方法 post 向表单发送包含所需数据的消息。
完整代码:
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 := 7777;
end
else
begin
ArcGauge.ShowCaption := True;
ArcGauge.FgBrush.Color := clHighlight;
end;
ArcGauge.Position := LVolume;
end;
end.
接口单元:
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.
我有一些代码给你,3 个源代码文件:一个带有 class 处理音量控制通知的单元,一个与 Windows API 接口的单元和一个简单的演示程序。该演示实际上是您需要详细查看的全部内容。其余的可以认为是晦涩的支持例程:-)
让我们看看演示程序。它是一个简单的 VCL 表单,上面只有一个 TMemo。它注册音量控制通知并在备忘录中显示一条简单消息(您可能想要一个不错的 UI)。
代码真的很简单:创建一个指向TVolumeControl的接口,为OnVolumeChange分配一个事件处理程序并调用Initialize方法。当事件触发时,调用 GetLevelInfo 获取信息并显示它。当表单被销毁时,调用Dispose方法停止接收通知。
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;
功夫不负有心人是我命名的一个单元Ovb.VolumeMonitor。此单元与 Windows API 交互以在默认音频设备上更改音量时请求通知。
请注意,这不是一个组件,而是一个 class,您可以通过一个界面使用这个 class。请参阅上面的演示应用程序。
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;
最后,为了与 Windows API 交互,我们需要一些 Windows 使用的结构和接口声明。
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 = [=12=]000000;
eCapture = [=12=]000001;
eAll = [=12=]000002;
// Role constant
eConsole = [=12=]000000;
eMultimedia = [=12=]000001;
eCommunications = [=12=]000002;
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.