Controlling application sound volume in Delphi
Asked Answered
S

2

8

I'm not even sure if this fits into one question, but it is a single problem. I have an internet radio player written in Delphi XE, using the BASS library for audio streaming and playback. The application needs to run under Windows XP, Vista and 7.

Bass makes it easy to control global volume, but has no facility for muting sound, and in general it's a better idea to control volume on per-application basis.

Bass also makes it easy to control the volume of a "channel" (stream), but again there is no muting, and this isn't the proper per-application control, either. (The application volume control in Windows mixer is unaffected.)

I understand that for Vista and above I need ISimpleAudioVolume and/or IAudioEndpointVolume, but cannot find a Delphi implementation of these. So one part of the question is, does it exist as a 3rd party library?

Part two is, what's the proper way to to control volume and toggle mute (system-wide or per application) on XP, where these interfaces are not available?

Selden answered 6/1, 2011 at 14:44 Comment(9)
You don't need a "Delphi implementation". Just follow the official docs at MSDN.Everyday
These interfaces do not appear to be declared in Delphi Xe. And what about XP?Metronome
1) You can declare them yourself. 2) That might be a problem.Everyday
Do you really want to change the volume of your application, or just the volume of the audio you play with a certain library?Consolata
@CodeInChaos: Either, but a properly implemented volume control should work in tandem with the system. The position of volume slider in the application should be reflected in the per-application volume slider in the Windows mixer. (7 has a new mixer that displays a per-app control when an application begins using an audio device). The same for mute. Currently I just set the volume to zero in the app, but this not "really mute", as far as Windows is concerned, the speaker icon does not indicate mute state, etc.Metronome
Hm, why downvote the question? Is it phrased inaccurately? Uniportant? It is certainly not a duplicate - I've searched, and there seems to be no useful answer on stackoverflow yet, at least not for Pascal/Delphi. C++ solutions are not useful. (Moderators: if the question is somehow inappropriate, please just close it.)Metronome
XP only has one volume control; don't make the mistake that software like VLC makes: they fiddler with the main volume control the wrong way, and get out of sync. Let the main volume control to the user, and do a per-app thing only in Windows 7 and up.Analyse
@Jeroen Pluimers: Vista and up. Other than that, couldn't agree more.Albarran
you have my upvote to compensate for the downvotes you got. your question is valid and good.Platt
N
2

Use this simple code to mute the main volume it works on my machine:

procedure TForm1.Button1Click(Sender: TObject);
var
i:Integer;
begin
for i:=0 to 100 do
begin
  keybd_event($AE, MapVirtualKey($AE,0), 0, 0);
  keybd_event($AE, MapVirtualKey($AE,0), KEYEVENTF_KEYUP, 0);
end;
end;
Nonaligned answered 27/5, 2011 at 12:40 Comment(1)
Instead of turning down the volume, why not really mute? Virtual-Key Codes -> VK_VOLUME_MUTE=0xADRozella
H
0

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.

demoapp

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

Hydropathy answered 16/8, 2021 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.