WASAPI Process Notifications (System Wide)

Share your advanced PureBasic knowledge/code with the community.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

WASAPI Process Notifications (System Wide)

Post by AndyMK »

This code lists processes connected to the Windows Audio Engine.

notifications_default_interface.pbi

Code: Select all

EnableExplicit

Enumeration #PB_Event_FirstCustomValue
  #EventStateInactive
  #EventStateActive
  #EventStateExpired
EndEnumeration

Enumeration
  #AudioSessionStateInactive
  #AudioSessionStateActive
  #AudioSessionStateExpired
EndEnumeration

#AUDCLNT_STREAMFLAGS_CROSSPROCESS = $00010000
#CLSCTX_ALL = #CLSCTX_INPROC_SERVER | #CLSCTX_INPROC_HANDLER | #CLSCTX_LOCAL_SERVER | #CLSCTX_REMOTE_SERVER

Interface IMMDeviceEnumerator Extends IUnknown
  EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices)
  GetDefaultAudioEndpoint(dataFlow, role, ppEndpoint)
  GetDevice(pwstrId, ppDevice)
  RegisterEndpointNotificationCallback(pClient)
  UnregisterEndpointNotificationCallback(pClient)
EndInterface

Interface IMMDevice Extends IUnknown
  Activate(iid, dwClsCtx, pActivationParams, ppInterface)
  OpenPropertyStore(stgmAccess, ppProperties)
  GetId(ppstrId)
  GetState(pdwState)
EndInterface

Interface IAudioSessionManager Extends IUnknown
  GetAudioSessionControl(AudioSessionGuid, StreamFlags, SessionControl)
  GetSimpleAudioVolume(AudioSessionGuid, StreamFlags, AudioVolume)
EndInterface

Interface IAudioSessionManager2 Extends IAudioSessionManager
  GetSessionEnumerator(SessionEnum)
  RegisterSessionNotification(Notification)
  UnregisterSessionNotification(Notification)
  RegisterDuckNotification(sessionID, duckNotification)
  UnregisterDuckNotification(duckNotification)
EndInterface

Interface IAudioSessionNotification Extends IUnknown
  OnSessionCreated(NewSession)
EndInterface

Interface IAudioSessionControl Extends IUnknown
  GetState(pRetVal)
  GetDisplayName(*pRetVal)
  SetDisplayName(Value, EventContext)
  GetIconPath(pRetVal)
  SetIconPath(Value, EventContext)
  GetGroupingParam(pRetVal)
  SetGroupingParam(Override, EventContext)
  RegisterAudioSessionNotification(NewNotifications)
  UnregisterAudioSessionNotification(NewNotifications)
EndInterface

Interface IAudioSessionControl2 Extends IAudioSessionControl
  GetSessionIdentifier(*pRetVal)
  GetSessionInstanceIdentifier(*pRetVal)
  GetProcessId(pRetVal)
  IsSystemSoundsSession()
  SetDuckingPreference(optOut)
EndInterface

Interface IAudioSessionEnumerator Extends IUnknown
  GetCount(count)
  GetSession(sessionCount, session)
EndInterface

Interface IAudioSessionEvents Extends IUnknown
  OnDisplayNameChanged(NewDisplayName, EventContext)
  OnIconPathChanged(NewIconPath, EventContext)
  OnSimpleVolumeChanged(NewVolume, NewMute, EventContext)
  OnChannelVolumeChanged(ChannelCount, NewChannelVolumeArray, ChangedChannel, EventContext)
  OnGroupingParamChanged(NewGroupingParam, EventContext)
  OnStateChanged(NewState)
  OnSessionDisconnected(DisconnectReason)
EndInterface

Structure MyAudioSessionEventsObj
  lpVtbl.i
  RefCount.l
  ProcessID.l
  *SessionID
  pSessionControl.IAudioSessionControl2
EndStructure

Structure MyAudioSessionNotificationObj
  lpVtbl.i
  RefCount.l
EndStructure

Structure mysessions
  *object.MyAudioSessionEventsObj
EndStructure

Declare MyOnSessionCreated(*Object, pSessionControl.IAudioSessionControl2)

Procedure MyAddRef(*Object)
  Protected newCount = PeekL(*Object + SizeOf(Integer)) + 1
  PokeL(*Object + SizeOf(Integer), newCount)
  ProcedureReturn newCount
EndProcedure

Procedure MyQueryInterface(*Object, riid.i, ppvObject.i)
  PokeI(ppvObject, *Object)
  MyAddRef(*Object)
  ProcedureReturn #S_OK
EndProcedure

Procedure MyRelease(*Object)
  Protected newCount = PeekL(*Object + SizeOf(Integer)) - 1
  PokeL(*Object + SizeOf(Integer), newCount)
  If newCount = 0
    FreeStructure(*Object)
    *Object = #Null
    ProcedureReturn newCount
  Else
    ProcedureReturn newCount
  EndIf
EndProcedure

Procedure CleanUp(*Object.MyAudioSessionEventsObj)
  CoTaskMemFree_(*Object\SessionID)
  FreeMemory(*Object\lpVtbl)
  *Object\pSessionControl\Release()
  MyRelease(*Object)
EndProcedure

Procedure MySessionEvents_OnSessionDisconnected(*Object.MyAudioSessionEventsObj, DisconnectReason)
  Debug "Session Disconnected: PID=" + Str(*Object\ProcessID) + ", Reason=" + Str(DisconnectReason)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnDisplayNameChanged(*Object.MyAudioSessionEventsObj, NewDisplayName, EventContext)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnIconPathChanged(*Object.MyAudioSessionEventsObj, NewIconPath, EventContext)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnSimpleVolumeChanged(*Object.MyAudioSessionEventsObj, NewVolume, NewMute, EventContext)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnChannelVolumeChanged(*Object.MyAudioSessionEventsObj, ChannelCount, NewChannelVolumeArray, ChangedChannel, EventContext)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnGroupingParamChanged(*Object.MyAudioSessionEventsObj, NewGroupingParam, EventContext)
  ProcedureReturn #S_OK
EndProcedure

Procedure MySessionEvents_OnStateChanged(*Object.MyAudioSessionEventsObj, NewState)
  Select NewState
    Case #AudioSessionStateInactive
      PostEvent(#EventStateInactive, #PB_Ignore, #PB_Ignore, #PB_Ignore, *Object)
      
    Case #AudioSessionStateActive
      PostEvent(#EventStateActive, #PB_Ignore, #PB_Ignore, #PB_Ignore, *Object)
      
    Case #AudioSessionStateExpired
      PostEvent(#EventStateExpired, #PB_Ignore, #PB_Ignore, #PB_Ignore, *Object)
      
  EndSelect
  
  ProcedureReturn #S_OK
EndProcedure

Procedure CreateMyAudioSessionEvents(ProcessID, *SessionID, pSessionControl)
  Protected *pObj.MyAudioSessionEventsObj = AllocateStructure(MyAudioSessionEventsObj)
  Protected *MyAudioSessionEventsVTable = AllocateMemory(10 * SizeOf(Integer))
  
  If *pObj = 0
    ProcedureReturn #Null
  EndIf
  
  If *MyAudioSessionEventsVTable = #Null
    FreeStructure(*pObj)
    ProcedureReturn #Null
  EndIf
  
  PokeI(*MyAudioSessionEventsVTable + 0 * SizeOf(Integer), @MyQueryInterface())
  PokeI(*MyAudioSessionEventsVTable + 1 * SizeOf(Integer), @MyAddRef())
  PokeI(*MyAudioSessionEventsVTable + 2 * SizeOf(Integer), @MyRelease())
  PokeI(*MyAudioSessionEventsVTable + 3 * SizeOf(Integer), @MySessionEvents_OnDisplayNameChanged())
  PokeI(*MyAudioSessionEventsVTable + 4 * SizeOf(Integer), @MySessionEvents_OnIconPathChanged())
  PokeI(*MyAudioSessionEventsVTable + 5 * SizeOf(Integer), @MySessionEvents_OnSimpleVolumeChanged())
  PokeI(*MyAudioSessionEventsVTable + 6 * SizeOf(Integer), @MySessionEvents_OnChannelVolumeChanged())
  PokeI(*MyAudioSessionEventsVTable + 7 * SizeOf(Integer), @MySessionEvents_OnGroupingParamChanged())
  PokeI(*MyAudioSessionEventsVTable + 8 * SizeOf(Integer), @MySessionEvents_OnStateChanged())
  PokeI(*MyAudioSessionEventsVTable + 9 * SizeOf(Integer), @MySessionEvents_OnSessionDisconnected())
  
  *pObj\lpVtbl = *MyAudioSessionEventsVTable
  *pObj\ProcessID = ProcessID
  *pObj\pSessionControl = pSessionControl
  *pObj\SessionID = *SessionID
  *pObj\RefCount = 1
  
  ProcedureReturn *pObj
EndProcedure

Procedure CreateMyAudioSessionNotification()
  Protected *pObj.MyAudioSessionNotificationObj = AllocateStructure(MyAudioSessionNotificationObj)
  Protected *MyAudioSessionNotificationVTable = AllocateMemory(4 * SizeOf(Integer))
  
  If *pObj = 0
    ProcedureReturn #Null
  EndIf
  
  If *MyAudioSessionNotificationVTable = #Null
    FreeStructure(*pObj)
    ProcedureReturn #Null
  EndIf
  
  PokeI(*MyAudioSessionNotificationVTable + 0 * SizeOf(Integer), @MyQueryInterface())
  PokeI(*MyAudioSessionNotificationVTable + 1 * SizeOf(Integer), @MyAddRef())
  PokeI(*MyAudioSessionNotificationVTable + 2 * SizeOf(Integer), @MyRelease())
  PokeI(*MyAudioSessionNotificationVTable + 3 * SizeOf(Integer), @MyOnSessionCreated())
  
  *pObj\lpVtbl = *MyAudioSessionNotificationVTable
  *pObj\RefCount = 1
  
  ProcedureReturn *pObj
EndProcedure

Procedure MyOnSessionCreated(*Object.MyAudioSessionNotificationObj, pSessionControl.IAudioSessionControl2)
  Protected pSessionId, PID, sessionEvents, fname.s, state
  
  pSessionControl\AddRef()
  pSessionControl\GetProcessId(@PID)
  pSessionControl\GetSessionIdentifier(@pSessionId)
  pSessionControl\GetState(@state)
  
  If pSessionId
    fname = GetFilePart(StringField(StringField(PeekS(pSessionId, -1, #PB_Unicode), 2, "|"), 1, "%"))
  EndIf
  
  sessionEvents = CreateMyAudioSessionEvents(PID, pSessionId, pSessionControl)
  pSessionControl\RegisterAudioSessionNotification(sessionEvents)
  
  ProcedureReturn #S_OK
EndProcedure

Define deviceEnumerator.IMMDeviceEnumerator
Define defaultRender.IMMDevice
Define pSessionManager.IAudioSessionManager2
Define MyAudioSessionNotification.IAudioSessionNotification
Define pSessionList.IAudioSessionEnumerator
Define pSessionControl.IAudioSessionControl2
Define sessionEvents.IAudioSessionEvents
Define hr, cbSessionCount, index, PID, NewState, pSessionId

hr = CoInitializeEx_(#Null, #COINIT_APARTMENTTHREADED)
If hr <> #S_OK
  Debug "CoInitializeEx_ in new thread failed, hr=" + Hex(hr)
EndIf

hr = CoCreateInstance_(?uuidof_MMDeviceEnumerator, #Null, #CLSCTX_INPROC_SERVER, ?uuidof_IMMDeviceEnumerator, @deviceEnumerator)
If hr <> #S_OK
  Debug "CoCreateInstance_(MMDeviceEnumerator) failed, hr=" + Hex(hr)
EndIf

hr = deviceEnumerator\GetDefaultAudioEndpoint(0, 0, @defaultRender)
If hr <> #S_OK
  Debug "GetDefaultAudioEndpoint(eRender) failed, hr=" + Hex(hr)
EndIf

If defaultRender\Activate(?IID_IAudioSessionManager, #CLSCTX_ALL, #Null, @pSessionManager) <> #S_OK
  Debug "Activate IAudioSessionManager2 failed"
EndIf

pSessionManager\QueryInterface(?IID_IAudioSessionManager2, @pSessionManager)
If pSessionManager\GetSessionEnumerator(@pSessionList) <> #S_OK
  Debug "Get Session List Error"
EndIf

MyAudioSessionNotification = CreateMyAudioSessionNotification()

If MyAudioSessionNotification = #Null
  Debug "Failed to create IAudioSessionNotification COM object."
EndIf

hr = pSessionManager\RegisterSessionNotification(MyAudioSessionNotification)
If hr <> #S_OK
  Debug "RegisterSessionNotification failed, hr=" + Hex(hr)
EndIf

If pSessionList\GetCount(@cbSessionCount) <> #S_OK
  Debug "Get Session Count Error"
EndIf

For index = 0 To cbSessionCount - 1
  pSessionList\GetSession(index, @pSessionControl)
  If pSessionControl\QueryInterface(?IID_IAudioSessionControl2, @pSessionControl) = #S_OK
    pSessionControl\GetProcessId(@PID)
    pSessionControl\GetState(@NewState)
    pSessionControl\GetSessionIdentifier(@pSessionId)
    sessionEvents = CreateMyAudioSessionEvents(PID, pSessionId, pSessionControl)
    pSessionControl\RegisterAudioSessionNotification(sessionEvents)
    PostEvent(#EventStateActive, #PB_Ignore, #PB_Ignore, #PB_Ignore, sessionEvents)
  EndIf
Next

DataSection
  uuidof_MMDeviceEnumerator:
  Data.l $BCDE0395
  Data.w $E52F, $467C
  Data.b $8E, $3D, $C4, $57, $92, $91, $69, $2E
  
  uuidof_IMMDeviceEnumerator:
  Data.l $A95664D2
  Data.w $9614, $4F35
  Data.b $A7, $46, $DE, $8D, $B6, $36, $17, $E6
  
  IID_IAudioSessionManager:
  Data.l $BFA971F1
  Data.w $4D5E, $40BB
  Data.b $93, $5E, $96, $70, $39, $BF, $BE, $E4
  
  IID_IAudioSessionManager2:
  Data.l $77AA99A0
  Data.w $1BD6, $484F
  Data.b $8B, $C7, $2C, $65, $4C, $9A, $9B, $6F
  
  IID_IAudioSessionControl2:
  Data.l $bfb7ff88
  Data.w $7239, $4fc9
  Data.b $8f, $a2, $07, $c9, $50, $be, $9c, $6d
EndDataSection
How to use:

Code: Select all

IncludeFile "notifications_default_interface.pbi"

Enumeration #PB_Event_FirstCustomValue
  #EventStateInactive
  #EventStateActive
  #EventStateExpired
EndEnumeration

Enumeration
  #AudioSessionStateInactive
  #AudioSessionStateActive
  #AudioSessionStateExpired
EndEnumeration

Procedure CleanUp(*Object.MyAudioSessionEventsObj)
  CoTaskMemFree_(*Object\SessionID)
  FreeMemory(*Object\lpVtbl)
  *Object\pSessionControl\Release()
  MyRelease(*Object)
EndProcedure

Define Event, fname.s, *Object.MyAudioSessionEventsObj

OpenWindow(0, 0, 0, 0, 0, "", #PB_Window_Invisible)

Repeat
  Event = WaitWindowEvent()
  Select Event
    Case #EventStateActive
      *Object = EventData()
      fname = GetFilePart(StringField(StringField(PeekS(*Object\SessionId, -1, #PB_Unicode), 2, "|"), 1, "%"))
      Debug "Active: " + *Object\ProcessID + " " + fname
      
    Case #EventStateInactive
      *Object = EventData()
      fname = GetFilePart(StringField(StringField(PeekS(*Object\SessionId, -1, #PB_Unicode), 2, "|"), 1, "%"))
      Debug "Inactive: " + *Object\ProcessID + " " + fname
      
    Case #EventStateExpired
      *Object = EventData()
      fname = GetFilePart(StringField(StringField(PeekS(*Object\SessionId, -1, #PB_Unicode), 2, "|"), 1, "%"))
      Debug "Expired: " + *Object\ProcessID + " " + fname
      CleanUp(*Object)
  EndSelect
Until Event = #PB_Event_CloseWindow
19/02/2025 - Added Session Events. I haven't added Unregistering the Events yet.
19/20/2025 - Changed Quad types to Integer to support 32 bit.
20/02/2025 - Implemented UnregisterAudioSessionNotification on Expired events
05/03/2025 - Fixed memory leaks. Added custom events. Initialized COM properly to avoid crashes (thanks Zapman)
Last edited by AndyMK on Mon Jun 16, 2025 2:21 pm, edited 11 times in total.
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: WASAPI Process Notifications (System Wide)

Post by Quin »

Works nicely here, thanks for sharing! You are far braver than me, delving into the Windows Audio APIs with PB ;)
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

Quin wrote: Wed Feb 05, 2025 6:46 pm Works nicely here, thanks for sharing! You are far braver than me, delving into the Windows Audio APIs with PB ;)
Once you get the hang of COM, its not that bad. I'm still at novice at anything other than the stuff built in to PB.
User avatar
idle
Always Here
Always Here
Posts: 5835
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: WASAPI Process Notifications (System Wide)

Post by idle »

Thanks should be very useful
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: WASAPI Process Notifications (System Wide)

Post by Quin »

AndyMK wrote: Wed Feb 05, 2025 8:20 pm Once you get the hang of COM, its not that bad. I'm still at novice at anything other than the stuff built in to PB.
Probably the case, but it still terrifies me compared to something like BCX's COM implementation, where I can just do something like I can in VBS, e.g.:

Code: Select all

 DIM oVoice AS OBJECT
 oVoice = COM("SAPI.SpVoice")
 IF ISOBJECT(oVoice) THEN oVoice.Speak "Hello from BCX"
 UNCOM(oVoice)
Doubtful we'll ever see anything quite like this in PB, but one can dream. There are just so many interface methods and having to wrap all of them terrifies me :D
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

Updated to handle Session Events. You now get events like volume changes, session inactive/expired, etc.
dige
Addict
Addict
Posts: 1391
Joined: Wed Apr 30, 2003 8:15 am
Location: Germany
Contact:

Re: WASAPI Process Notifications (System Wide)

Post by dige »

Unfortunately I can't try it out. Windows 10 PB 6.20 x86 / Threadsfae enabled

Invalid memory access at: pSessionControl\GetProcessId(@PID)

What do I need to do, to execute the code?
"Daddy, I'll run faster, then it is not so far..."
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

dige wrote: Wed Feb 19, 2025 10:46 am Invalid memory access at: pSessionControl\GetProcessId(@PID)
Which line number? That method is called in 2 different places.
fryquez
Enthusiast
Enthusiast
Posts: 391
Joined: Mon Dec 21, 2015 8:12 pm

Re: WASAPI Process Notifications (System Wide)

Post by fryquez »

You use SizeOf(Quad) to build the 2 vtables.
You should use SizeOf(Integer) instead and it will work on 32bit compiler, too.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

fryquez wrote: Wed Feb 19, 2025 12:30 pm You use SizeOf(Quad) to build the 2 vtables.
You should use SizeOf(Integer) instead and it will work on 32bit compiler, too.
Yeah, i completely forgot about that. Updated. I am wondering if dige is using 32bit as he said he is using Windows 10 x86.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

Implemented UnregisterAudioSessionNotification on Expired events
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: WASAPI Process Notifications (System Wide)

Post by Zapman »

Same error as dige here: Invalid memory error at line 255. Reading error at address 14.

Code: Select all

pSessionControl\GetProcessId(@PID)
ThreadSafe enabled. PureBasic 6.20 Beta 4 (x64) on Windows 11.

But I get the same error with PureBasic 6.12 LTS (x86) on Windows 11.

I can do some more tests if you want to.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

I am re writing it because i should not be registering/unregistering event inside a callback according to Microsoft. It is also leaking memory.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: WASAPI Process Notifications (System Wide)

Post by Zapman »

About the memory error that some of us get when tryig to run your code:
Line 254, you do:

Code: Select all

pSessionList\GetSession(index, @pSessionControl)
--> That give you an IID_IAudioSessionControl interface.
But, later, you use it as an IID_IAudioSessionControl2 interface. That's not correct.

You should do :

Code: Select all

    pSessionList\GetSession(index, @pSessionControl)
    pSessionControl\QueryInterface(?IID_IAudioSessionControl2, @pSessionControl)
So, you get an IID_IAudioSessionControl2 interface from the IID_IAudioSessionControl interface

And, at the end of your code (DataSection), you need to add:

Code: Select all

  IID_IAudioSessionControl2:  ; {bfb7ff88-7239-4fc9-8fa2-07c950be9c6d}
  Data.l $bfb7ff88
  Data.w $7239, $4fc9
  Data.b $8f, $a2, $07, $c9, $50, $be, $9c, $6d
With this modiication, I haven't memory error anymore.
And if I launch Audacity, I get:
New audio session created. PID: 18744
Audacity.exe
18744 Inactive
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications (System Wide)

Post by AndyMK »

Code: Select all

Protected pSessionControl.IAudioSessionControl2
pSessionControl is already an IAudioSessionControl2 interface. Is this the wrong way to do it?
Post Reply