[SOLVED] WASAPI Process Notifications

Just starting out? Need help? Post your questions and find answers here.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

[SOLVED] WASAPI Process Notifications

Post by AndyMK »

I posted some code in the Tricks 'n' Tips section of the forum show WASAPI Process notifications which works fine for detecting the PID of currently audio open sessions and new sessions that get opened after the program has started. I now want to extend it to fire events for each session opened like volume changes, disconnected processes etc. I have add the modifications but i am getting strange results.

1. the events only fire if i change the volume for the purebasic app itself.
2. the volume event fired seems to return all audio sessions open.

Help would be appreciated.

Code: Select all

EnableExplicit

Enumeration
  #AudioSessionStateInactive
  #AudioSessionStateActive
  #AudioSessionStateExpired
EndEnumeration

#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

Global deviceEnumerator.IMMDeviceEnumerator
Global defaultRender.IMMDevice
Global ev_sessionMgr.IAudioSessionManager
Global sessionMgr.IAudioSessionManager2
Global sessionList.IAudioSessionEnumerator
Global MyAudioSessionNotification.IAudioSessionNotification
Global sessionEvents.IAudioSessionEvents
Global pControl.IAudioSessionControl

Structure MyAudioSessionNotificationObj
  lpVtbl.i
  RefCount.l
EndStructure

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

Global *MyAudioSessionNotificationVTable = #Null
Global *MyAudioSessionEventsVTable = #Null

Procedure MyAddRef(*Object)
  ;Debug #PB_Compiler_Procedure
  Protected newCount = PeekL(*Object + SizeOf(Quad)) + 1
  PokeL(*Object + SizeOf(Quad), newCount)
  ProcedureReturn newCount
EndProcedure

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

Procedure MyRelease(*Object)
  ;Debug #PB_Compiler_Procedure
  Protected newCount = PeekL(*Object + SizeOf(Quad)) - 1
  PokeL(*Object + SizeOf(Quad), newCount)
  If newCount = 0
    FreeStructure(*Object)
  EndIf
  ProcedureReturn newCount
EndProcedure

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

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

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

Procedure MySessionEvents_OnSimpleVolumeChanged(*Object.MyAudioSessionEventsObj, NewVolume, NewMute, EventContext)
  Debug #PB_Compiler_Procedure
  Debug PeekS(*Object\SessionID)
  ProcedureReturn #S_OK
EndProcedure

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

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

Procedure MySessionEvents_OnStateChanged(*Object.MyAudioSessionEventsObj, NewState)
  Debug #PB_Compiler_Procedure
  ProcedureReturn #S_OK
EndProcedure

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

Procedure MyOnSessionCreated(*Object, pNewSession.IAudioSessionControl2)
  ;Debug #PB_Compiler_Procedure
  Protected state, *sIdentifier, *dName, PID, hr
    pNewSession\GetProcessId(@PID)
    pNewSession\GetState(@state)
    pNewSession\GetSessionInstanceIdentifier(@*sIdentifier)
    
    If pNewSession And PID > 0
      Debug "New audio session created. PID: " + Str(PID)
      
      sessionEvents = CreateMyAudioSessionEvents(PID, *sIdentifier)
      
      hr = ev_sessionMgr\GetAudioSessionControl(*sIdentifier, 0, @pControl)
      If hr <> #S_OK
        Debug "Error getting Session Control: " + Hex(hr)
      EndIf
      
      If sessionEvents
        hr = pControl\RegisterAudioSessionNotification(sessionEvents)
        If hr <> #S_OK
          Debug "Error registering session events: " + Hex(hr)
          MyRelease(sessionEvents)
        EndIf
      EndIf
    EndIf
    CoTaskMemFree_(*sIdentifier)
    
  ProcedureReturn #S_OK
EndProcedure

Procedure CreateMyAudioSessionNotification()
  ;Debug #PB_Compiler_Procedure
  Protected *pObj.MyAudioSessionNotificationObj = AllocateStructure(MyAudioSessionNotificationObj)
  If *pObj = 0
    ProcedureReturn #Null
  EndIf
  
  ; Allocate and initialize the global vtable if it hasn't been created.
  If *MyAudioSessionNotificationVTable = #Null
    *MyAudioSessionNotificationVTable = AllocateMemory(4 * SizeOf(Quad))
    If *MyAudioSessionNotificationVTable = #Null
      FreeStructure(*pObj)
      ProcedureReturn #Null
    EndIf
    PokeI(*MyAudioSessionNotificationVTable + 0 * SizeOf(Quad), @MyQueryInterface())
    PokeI(*MyAudioSessionNotificationVTable + 1 * SizeOf(Quad), @MyAddRef())
    PokeI(*MyAudioSessionNotificationVTable + 2 * SizeOf(Quad), @MyRelease())
    PokeI(*MyAudioSessionNotificationVTable + 3 * SizeOf(Quad), @MyOnSessionCreated())
  EndIf
  
  *pObj\lpVtbl = *MyAudioSessionNotificationVTable
  *pObj\RefCount = 1
  
  ProcedureReturn *pObj
EndProcedure

Procedure CreateSessionManager()
  ;Debug #PB_Compiler_Procedure
  Protected hr, PID, *sIdentifier
  
  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_IAudioSessionManager2, #CLSCTX_ALL, #Null, @sessionMgr) <> #S_OK
    Debug "Activate IAudioSessionManager2 failed"
  EndIf
  
  If defaultRender\Activate(?IID_IAudioSessionManager, #CLSCTX_ALL, #Null, @ev_sessionMgr) <> #S_OK
    Debug "Activate IAudioSessionManager2 failed"
  EndIf
  
  ev_sessionMgr\AddRef()
  ev_sessionMgr\Release()
  sessionMgr\AddRef()
  sessionMgr\Release()
  deviceEnumerator\Release()
  defaultRender\Release()
  
  ProcedureReturn hr
  
EndProcedure

Procedure ProcessEnumeratedSessions()
  ;Debug #PB_Compiler_Procedure
  Protected count.l, i.l, session.IAudioSessionControl2
  
  sessionList\GetCount(@count)
  
  For i = 0 To count - 1
    sessionList\GetSession(i, @session)
    If session <> #Null
      MyOnSessionCreated(MyAudioSessionNotification, session)
      session\Release()
    EndIf
  Next
EndProcedure

Procedure MTAThread(val)
  Debug #PB_Compiler_Procedure
  Protected hr = CoInitializeEx_(#Null, #COINIT_MULTITHREADED)
  If hr <> #S_OK And hr <> #RPC_E_CHANGED_MODE
    Debug "CoInitializeEx_ in new thread failed, hr=" + Hex(hr)
    ProcedureReturn
  EndIf
  
  If CreateSessionManager() = #S_OK
    If sessionMgr\GetSessionEnumerator(@sessionList) = #S_OK
      ProcessEnumeratedSessions()
            
      MyAudioSessionNotification = CreateMyAudioSessionNotification()
      
      If MyAudioSessionNotification = #Null
        Debug "Failed to create IAudioSessionNotification COM object."
      EndIf
      
      hr = sessionMgr\RegisterSessionNotification(MyAudioSessionNotification)
      If hr <> #S_OK
        Debug "RegisterSessionNotification failed, hr=" + Hex(hr)
      EndIf
      
      sessionList\Release()
      
    Else
      Debug "Failed to create IAudioSessionNotification callback object."
    EndIf
  Else
    Debug "Get Session Enumerator failed"
  EndIf
    
  Repeat
    Delay(1000)
  ForEver
  
  CoUninitialize_()
  End

EndProcedure

CreateThread(@MTAThread(), 0)

Repeat
  Delay(1000)
ForEver

DataSection
  IID_IUnknown:
  Data.l $00000000
  Data.w $0000, $0000
  Data.b $C0, $00, $00, $00, $00, $00, $00, $46
  
  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_IAudioSessionNotification:
  Data.l $641DD20B
  Data.w $4D41, $49CC
  Data.b $AB, $A3, $17, $4B, $94, $77, $BB, $08
  
  IID_IAudioSessionEnumerator:
  Data.l $E2F5BB11
  Data.w $0570, $40CA
  Data.b $AC, $DD, $3A, $A0, $12, $77, $DE, $E8
  
  IID_IAudioSessionControl:
  Data.l $F4B1A599
  Data.w $7266, $4319
  Data.b $A8, $CA, $E7, $0A, $CB, $11, $E8, $CD
  
  IID_IAudioSessionControl2:
  Data.l $BFB7FF88
  Data.w $7239, $4FC9
  Data.b $8D, $A2, $B2, $7C, $48, $D6, $9D, $5D
  
  IID_IAudioSessionEvents:
  Data.l $24918ACC
  Data.w $64B3, $37C1
  Data.b $8C, $A9, $74, $A6, $6E, $99, $57, $A8
EndDataSection
Last edited by AndyMK on Wed Feb 19, 2025 7:07 am, edited 1 time in total.
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Process Notifications

Post by AndyMK »

I also tried doing this but still no luck.

Code: Select all

Procedure.i StringToGUID(GUIDString.s, *GUID.GUID)
  Protected Parts.s, Data4Part.s, i
  
  If Len(GUIDString) <> 36
    ProcedureReturn #False
  EndIf

  *GUID\Data1 = Val("$" + Mid(GUIDString, 1, 8))
  *GUID\Data2 = Val("$" + Mid(GUIDString, 10, 4))
  *GUID\Data3 = Val("$" + Mid(GUIDString, 15, 4))

  Data4Part = Mid(GUIDString, 20, 4) + Mid(GUIDString, 25, 12)

  For i = 0 To 7
    *GUID\Data4[i] = Val("$" + Mid(Data4Part, i * 2 + 1, 2))
  Next

  ProcedureReturn #True
EndProcedure

Procedure MyOnSessionCreated(*Object, pNewSession.IAudioSessionControl2)
  ;Debug #PB_Compiler_Procedure
  Protected state, *sIdentifier, *dName, PID, hr, guidString.s, guid.GUID
    pNewSession\GetProcessId(@PID)
    pNewSession\GetState(@state)
    pNewSession\GetSessionInstanceIdentifier(@*sIdentifier)
    
    guidString = PeekS(*sIdentifier, -1, #PB_Unicode)
    guidString = RemoveString(RemoveString(StringField(StringField(guidString, 5, "."), 1, "|"), "{"), "}")
    StringToGUID(guidString, guid)
    
    If pNewSession And PID > 0
      Debug "New audio session created. PID: " + Str(PID)
      
      sessionEvents = CreateMyAudioSessionEvents(PID, *sIdentifier)
      
      hr = ev_sessionMgr\GetAudioSessionControl(@guid, 0, @pControl)
      If hr <> #S_OK
        Debug "Error getting Session Control: " + Hex(hr)
      EndIf
      
      If sessionEvents
        hr = pControl\RegisterAudioSessionNotification(sessionEvents)
        If hr <> #S_OK
          Debug "Error registering session events: " + Hex(hr)
          MyRelease(sessionEvents)
        EndIf
      EndIf
    EndIf
    CoTaskMemFree_(*sIdentifier)
    
  ProcedureReturn #S_OK
EndProcedure
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: [SOLVED] WASAPI Process Notifications

Post by AndyMK »

Post Reply