What a coincidence, I'm also writing my personal radio stream client using bass.dll, which is a great library BTW. However, I still wanted to integrate with Windows' mixer, so modifying volume from external applications (like EarTrumpet or Windows Mixer itself) would reflect in my application's volume slider too automatically.
As comments mentioned, there are some disadvantages with Windows XP, since IAudioSession was introduced from Vista onwards, and Windows 7 and newer included improvementes like IAudioSessionManager2, IAudioVolumeDuckNotification, etc).
Still for your own application, IAudioSessionManager will work well, specially using IAudioSimpleVolume. Gladly, that's supported in Vista+.
You can get a variety of MMDeviceAPI implementations at GitHub and many other forums, but here I will strip the necessary interfaces to achieve what we need.
You will need IAudioSessionEvents, IAudioSessionControl, ISimpleAudioVolume, IAudioSessionManager, IMMDevice, IMMDeviceCollection, IMMNotificationClient, and IMMDeviceEnumerator interfaces.
To respond to external audio events, we need to implement our interface extending IAudioSessionsEvents as follows:
TAudioEvent = class(TInterfacedPersistent, IAudioSessionEvents)
private
// IAudioSessionEvents
function OnDisplayNameChanged(NewDisplayName: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
function OnIconPathChanged(NewIconPath: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
// THIS WILL UPDATE "AUTOMATICALLY" ON VOLUME CHANGES
function OnSimpleVolumeChanged(NewVolume : Single;
NewMute : BOOL;
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: TAudioSessionState): HRESULT; stdcall;
function OnSessionDisconnected(
DisconnectReason: TAudioSessionDisconnectReason): HRESULT; stdcall;
public
// constructor Create(AppWindow: HWND); <-- left for improvements
// destructor Destroy; override;
end;
...
function TAudioEvent.OnSimpleVolumeChanged(NewVolume: Single; NewMute: BOOL;
EventContext: PGUID): HRESULT;
begin
// min 0, max 10 for slider, NewVolume is 0 to 1 in Single type
Form1.Slider1.Value := Round(NewVolume*10);
if NewMute then
Form1.MuteButton.Caption := 'Muted'
else
Form1.MuteButton.Caption := ' ';
end;
That's the event handler, it will update volume and mute status as well, any volume changes from within the application or from Windows mixer.
NOTICE, it needs improvements to handle session changes, e.g. when you switch physical audio device, reassign to another audio device, etc.
Now, use a global variables to handle the audio sessions and related queries.
var devicen: IMMDeviceEnumerator;
device: IMMDevice;
audiosession: IAudioSessionManager;
control: IAudioSessionControl;
audio: ISimpleAudioVolume;
audioevent: TAudioEvent;
volume: Single;
Let's initialize after loading Bass.dll.
//HRESULT, I guess you have to do CoInitialize(nil), but Delphi does it by default, at least, that's what they say.
// Get the enumerator for the audio endpoint devices
// on this system.
hr := CoCreateInstance(CLASS_IMMDeviceEnumerator, nil, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator, devicen);
// Get the audio endpoint device with the specified data-flow
// direction (eRender or eCapture) and device role.
devicen.GetDefaultAudioEndpoint(eRender, eMultimedia, device);
// Get the session manager for the endpoint device.
device.Activate(IID_IAudioSessionManager, CLSCTX_INPROC_SERVER, nil, audiosession);
// Get the control interface for the process-specific audio
// session with session GUID = GUID_NULL. This is the session
// that an audio stream for a DirectSound, DirectShow, waveOut,
// or PlaySound application stream belongs to by default.
// GUID_NULL or nil will assign to the default session
// (i.e.) our application (bass created, if done previously) session
audiosession.GetAudioSessionControl(nil, 0, control);
audioevent := TAudioEvent.Create;
// register our audio session events
control.RegisterAudioSessionNotification(audioevent);
// to get/set its volume we will use GetSimpleAudioVolume (NIL, too
audiosession.GetSimpleAudioVolume(nil, 0, audio);
// retrieve its value
audio.GetMasterVolume(volume);
// show it in our slider
Slider1.Value := Round(volume*10);
Hint: this might even be included in our previous custom interface, for cleaner code.
To modify our application volume:
procedure TForm1.Slider1Change(Sender: TObject);
var
vol: Single;
begin
if BASS_ChannelIsActive(chan) = BASS_ACTIVE_PLAYING then
begin
vol := Slider1.Value / 10;
audio.SetMasterVolume(vol, nil);
end;
end;
Similarly to toggle mute, the event handler will update visually, we just need to toggle it.
procedure TForm1.MuteButtonClick(Sender: TObject);
var
ismute: LongBool;
begin
audio.GetMute(ismute);
if isMute then
audio.SetMute(0, nil)
else
audio.SetMute(1, nil);
end;
And that's it, it is up to you improving it. I know, this was a mess of a code, of course it needs cleaning and handling safe releasing interfaces, etc.
Finally, here are the interfaces required:
const
CLASS_IMMDeviceEnumerator : TGUID = '{BCDE0395-E52F-467C-8E3D-C4579291692E}';
IID_IMMDeviceEnumerator : TGUID = '{A95664D2-9614-4F35-A746-DE8DB63617E6}';
IID_IAudioSessionManager : TGUID = '{BFA971F1-4D5E-40BB-935E-967039BFBEE4}';
const
eRender = 0;
const
eConsole = 0;
eMultimedia = eConsole + 1;
type
TAudioSessionDisconnectReason = (DisconnectReasonDeviceRemoval,
DisconnectReasonServerShutdown, DisconnectReasonFormatChanged,
DisconnectReasonSessionLogoff, DisconnectReasonSessionDisconnected,
DisconnectReasonExclusiveModeOverride);
TAudioSessionState = (AudioSessionStateInactive, AudioSessionStateActive,
AudioSessionStateExpired);
IAudioSessionEvents = interface(IUnknown)
['{24918ACC-64B3-37C1-8CA9-74A66E9957A8}']
function OnDisplayNameChanged(NewDisplayName: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
function OnIconPathChanged(NewIconPath: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
function OnSimpleVolumeChanged(NewVolume : Single;
NewMute : BOOL;
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: TAudioSessionState): HRESULT; stdcall;
function OnSessionDisconnected(
DisconnectReason: TAudioSessionDisconnectReason): HRESULT; stdcall;
end;
IAudioSessionControl = interface(IUnknown)
['{F4B1A599-7266-4319-A8CA-E70ACB11E8CD}']
function GetState(out pRetVal: TAudioSessionState): HRESULT; stdcall;
function GetDisplayName(out pRetVal: LPWSTR): HRESULT; stdcall; // pRetVal must be freed by CoTaskMemFree
function SetDisplayName(Value: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
function GetIconPath(out pRetVal: LPWSTR): HRESULT; stdcall; // pRetVal must be freed by CoTaskMemFree
function SetIconPath(Value: LPCWSTR; EventContext: PGUID): HRESULT; stdcall;
function GetGroupingParam(pRetVal: PGUID): HRESULT; stdcall;
function SetGroupingParam(OverrideValue, EventContext: PGUID): HRESULT; stdcall;
function RegisterAudioSessionNotification(
const NewNotifications: IAudioSessionEvents): HRESULT; stdcall;
function UnregisterAudioSessionNotification(
const NewNotifications: IAudioSessionEvents): HRESULT; stdcall;
end;
ISimpleAudioVolume = interface(IUnknown)
['{87CE5498-68D6-44E5-9215-6DA47EF883D8}']
function SetMasterVolume(fLevel: Single; EventContext: PGUID): HRESULT; stdcall;
function GetMasterVolume(out fLevel: Single): HRESULT; stdcall;
// bMute either TRUE = 1 or FALSE = 0 !
function SetMute(bMute: Longint; EventContext: PGUID): HRESULT; stdcall;
function GetMute(out bMute: BOOL): HRESULT; stdcall;
end;
IAudioSessionManager = interface(IUnknown)
['{BFA971F1-4D5E-40BB-935E-967039BFBEE4}']
function GetAudioSessionControl(AudioSessionGuid: PGUID; StreamFlag : UINT;
out SessionControl: IAudioSessionControl): HRESULT; stdcall;
function GetSimpleAudioVolume(AudioSessionGuid: PGUID; StreamFlag: UINT;
out AudioVolume: ISimpleAudioVolume): HRESULT; stdcall;
end;
EDataFlow = TOleEnum;
ERole = TOleEnum;
{$IF CompilerVersion >= 21.0} //Winapi.PropSys
IPropertyStore = Winapi.PropSys.IPropertyStore;
{$EXTERNALSYM IPropertyStore}
{$ELSE}
IPropertyStore = ShlObj.IPropertyStore;
{$ENDIF}
IMMDevice = interface(IUnknown)
['{D666063F-1587-4E43-81F1-B948E807363F}']
function Activate(const iid: TGUID; dwClsCtx: DWORD; pActivationParams: PPropVariant; out ppInterface): HRESULT; stdcall;
function OpenPropertyStore(stgmAccess: DWORD; out ppProperties: IPropertyStore): HRESULT; stdcall;
function GetId(out ppstrId: LPWSTR): HRESULT; stdcall;
function GetState(out pdwState: DWORD): HRESULT; stdcall;
end;
IMMDeviceCollection = interface(IUnknown)
['{0BD7A1BE-7A1A-44DB-8397-CC5392387B5E}']
function GetCount(out pcDevices: UINT): HRESULT; stdcall;
function Item(nDevice: UINT; out ppDevice: IMMDevice): HRESULT; stdcall;
end;
IMMNotificationClient = interface(IUnknown)
['{7991EEC9-7E89-4D85-8390-6C703CEC60C0}']
function OnDeviceStateChanged(pwstrDeviceId: LPCWSTR; dwNewState: DWORD): HRESULT; stdcall;
function OnDeviceAdded(pwstrDeviceId: LPCWSTR): HRESULT; stdcall;
function OnDeviceRemoved(pwstrDeviceId: LPCWSTR): HRESULT; stdcall;
function OnDefaultDeviceChanged(flow: EDataFlow; role: ERole; pwstrDefaultDeviceId: LPCWSTR): HRESULT; stdcall;
function OnPropertyValueChanged(pwstrDeviceId: LPCWSTR; {const} key: PROPERTYKEY): HRESULT; stdcall;
end;
IMMDeviceEnumerator = interface(IUnknown)
['{A95664D2-9614-4F35-A746-DE8DB63617E6}']
function EnumAudioEndpoints(dataFlow: EDataFlow; dwStateMask: DWORD;
out ppDevices: IMMDeviceCollection): HRESULT; stdcall;
function GetDefaultAudioEndpoint(dataFlow: EDataFlow; role: ERole;
out ppEndpoint: IMMDevice): HRESULT; stdcall;
function GetDevice(pwstrId: LPCWSTR; out ppDevice: IMMDevice): HRESULT; stdcall;
function RegisterEndpointNotificationCallback(const pClient: IMMNotificationClient): HRESULT; stdcall;
function UnregisterEndpointNotificationCallback(const pClient: IMMNotificationClient): HRESULT; stdcall;
end;
I guess MfPack and other libraries include MMDeviceAPI.pas which has all of them.
Source: https://learn.microsoft.com/en-us/windows/win32/coreaudio/audio-events-for-legacy-audio-applications
If you want to modify other applications' audio sessions and also find its icon, executable, etc. It is better to use IAudioSessionManager2 (Windows 7+)
here is my implementation https://github.com/vhanla/snotify/blob/596d22f5bd89e58297d15ffff6df2d0f69bd0351/AudioSessionService.pas