WASAPI Loopback Capture from individual Apps!

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 Loopback Capture from individual Apps!

Post by AndyMK »

This was a hard one for me but thanks to mk-soft and Justin i managed a very basic example. The code below allows you to capture the audio output from an app using the apps process id. This is not "What you hear". You must have Windows 10 Build 20438 or higher to use it. The example is a bit rough but it works.

Enter you process ID here:

Code: Select all

CreateThread(@main(), 27336) ; change process ID to match a process that outputs audio.
Please not, if you are using a browser's process ID, you will have to pick the correct one. I have notification code in this forum that can help you with that. https://www.purebasic.fr/english/viewtopic.php?t=86214

As usual, enjoy :)

Code: Select all

EnableExplicit

;- AUDIOCLIENT_ACTIVATION_TYPE
Enumeration
  #AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT
  #AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK
EndEnumeration

;- PROCESS_LOOPBACK_MODE
Enumeration
  #PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE
  #PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE
EndEnumeration

#CLSCTX_ALL = #CLSCTX_INPROC_SERVER | #CLSCTX_INPROC_HANDLER | #CLSCTX_LOCAL_SERVER | #CLSCTX_REMOTE_SERVER

#AUDCLNT_SHAREMODE_EXCLUSIVE = 1
#AUDCLNT_STREAMFLAGS_EVENTCALLBACK = $00040000
#AUDCLNT_STREAMFLAGS_LOOPBACK = $00020000

Interface IAudioClient Extends IUnknown
  Initialize(ShareMode.l, StreamFlags.l, hnsBufferDuration.q, hnsPeriodicity.q, *pFormat, *pSessionGuid)
  GetBufferSize(*pNumBufferFrames)
  GetStreamLatency(*phnsLatency)
  GetCurrentPadding(*pNumPaddingFrames)
  IsFormatSupported(ShareMode.l, *pFormat, *ppClosestMatch)
  GetMixFormat(*ppDeviceFormat)
  GetDevicePeriod(*phnsDefaultDevicePeriod, *phnsMinimumDevicePeriod)
  Start()
  Stop()
  Reset()
  SetEventHandle(*EventHandle)
  GetService(*riid, *ppv)
EndInterface

Interface IAudioCaptureClient Extends IUnknown
  GetBuffer(*ppData, *pNumFramesToRead, *pdwFlags, *pu64DevicePosition, *pu64QPCPosition)
  ReleaseBuffer(NumFramesRead.l)
  GetNextPacketSize(*pNumFramesInNextPacket)
EndInterface

Interface IActivateAudioInterfaceAsyncOperation Extends IUnknown
  GetActivateResult(activateResult.i, activatedInterface.i)
EndInterface

Structure BLOB Align #PB_Structure_AlignC
  cbSize.l
  pBlobData.i
EndStructure

Structure PROPVARIANT Align #PB_Structure_AlignC
  vt.w
  wReserved1.w
  wReserved2.w
  wReserved3.w
  blob.BLOB
EndStructure

Structure AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS Align #PB_Structure_AlignC
  TargetProcessId.l 
  ProcessLoopbackMode.l 
EndStructure

Structure AUDIOCLIENT_ACTIVATION_PARAMS Align #PB_Structure_AlignC
  ActivationType.l
  ProcessLoopbackParams.AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS
EndStructure

Structure IUnknownVtbl
  QueryInterface.i
  AddRef.i
  Release.i
EndStructure

Structure ActivateCompletionHandlerVtbl Extends IUnknownVtbl
  ActivateCompleted.i
EndStructure

Global.ActivateCompletionHandlerVtbl g_ActivateCompletionHandlerVtbl

Structure ActivateCompletionHandlerObj
  *vt.ActivateCompletionHandlerVtbl
  refCount.i
EndStructure

Procedure.i ACH_Free(*this.ActivateCompletionHandlerObj)
  Debug #PB_Compiler_Procedure
  
  FreeMemory(*this)
EndProcedure

Procedure.l ACH_QueryInterface(*this.ActivateCompletionHandlerObj, *iid.IID, *Obj.Integer)
  *Obj\i = *this
  *this\refCount + 1
  
  ProcedureReturn #S_OK
EndProcedure

Procedure.l ACH_AddRef(*this.ActivateCompletionHandlerObj)
  *this\refCount + 1
  ProcedureReturn #S_OK
EndProcedure

Procedure.l ACH_Release(*this.ActivateCompletionHandlerObj)
  Protected.l refCount
  
  *this\refCount - 1
  refCount = *this\refCount
  
  If *this\refCount = 0
    ACH_Free(*this)
  EndIf
  
  ProcedureReturn refCount
EndProcedure

#WAVE_FORMAT_IEEE_FLOAT = 3

Procedure SetWaveFormatPCM44100Stereo16(*wf.WAVEFORMATEX)
  *wf\wFormatTag       = #WAVE_FORMAT_IEEE_FLOAT
  *wf\nChannels        = 2
  *wf\nSamplesPerSec   = 44100
  *wf\wBitsPerSample   = 32
  *wf\nBlockAlign      = *wf\nChannels * (*wf\wBitsPerSample / 8)
  *wf\nAvgBytesPerSec  = *wf\nSamplesPerSec * *wf\nBlockAlign
  *wf\cbSize           = 0
EndProcedure

Procedure.l ACH_ActivateCompleted(*this.ActivateCompletionHandlerObj, activateOperation.IActivateAudioInterfaceAsyncOperation)
  Protected.l result
  Protected.IAudioClient activatedInterface
  Protected.IAudioCaptureClient captureClient
  Protected streamFlags.l = #AUDCLNT_STREAMFLAGS_EVENTCALLBACK | #AUDCLNT_STREAMFLAGS_LOOPBACK
  Protected waveFmt, hr, bufferFrames
  
  activateOperation\GetActivateResult(@result, @activatedInterface)
  Debug #PB_Compiler_Procedure
  
  If result = #S_OK
    SetWaveFormatPCM44100Stereo16(@waveFmt)
    
    hr = activatedInterface\Initialize(0, streamFlags, 50000, 0, @waveFmt, #Null)
    If hr <> #S_OK
      Debug "Initialize() failed hr=" + Hex(hr)
    EndIf
    
    Protected hEvent.i = CreateEvent_(#Null, 0, 0, #Null)
    If hEvent = 0
      Debug "CreateEvent_ failed"
    EndIf
    
    hr = activatedInterface\SetEventHandle(hEvent)
    If hr <> #S_OK
      Debug "SetEventHandle() failed hr=" + Hex(hr)
      CloseHandle_(hEvent)
    EndIf
    
    hr = activatedInterface\GetService(?IID_IAudioCaptureClient, @captureClient)
    
    If hr <> #S_OK
      Debug "GetService(IAudioRenderClient) failed hr=" + Hex(hr)
      CloseHandle_(hEvent)
    EndIf  
    
    hr = activatedInterface\Start()
    If hr <> #S_OK
      Debug "IAudioClient\Start() failed hr=" + Hex(hr)
      CloseHandle_(hEvent)
    EndIf
    
    Protected numPadding.l, numFramesAvailable.l, flags.l, pNumFramesInNextPacket.l, *Data
    
    Repeat
      Protected waitRes = WaitForSingleObject_(hEvent, 500)
      If waitRes = #WAIT_OBJECT_0
        hr = captureClient\GetBuffer(@*data, @numFramesAvailable, @flags, #Null, #Null)
        ;Debug numFramesAvailable
        Debug PeekF(*Data)
        
        captureClient\ReleaseBuffer(numFramesAvailable)
      EndIf
    ForEver
  EndIf 
  
  ProcedureReturn #S_OK
EndProcedure

g_ActivateCompletionHandlerVtbl\QueryInterface = @ACH_QueryInterface()
g_ActivateCompletionHandlerVtbl\AddRef = @ACH_AddRef()
g_ActivateCompletionHandlerVtbl\Release = @ACH_Release()
g_ActivateCompletionHandlerVtbl\ActivateCompleted = @ACH_ActivateCompleted()

Procedure.i ACH_New()
  Protected.ActivateCompletionHandlerObj *this
  
  *this = AllocateMemory(SizeOf(ActivateCompletionHandlerObj))
  *this\vt = @g_ActivateCompletionHandlerVtbl
  *this\refCount = 1
  
  ProcedureReturn *this
EndProcedure

Global ActivateAudioInterfaceAsync
Prototype.i ActivateAudioInterfaceAsync(deviceInterfacePath.s, *riid.IID, *activationParams.PROPVARIANT, completionHandler.i, activationOperation.i)

If OpenLibrary(0, "Mmdevapi.dll")
  ActivateAudioInterfaceAsync.ActivateAudioInterfaceAsync = GetFunction(0, "ActivateAudioInterfaceAsync")
EndIf

Procedure main(processId)
  Protected.IActivateAudioInterfaceAsyncOperation asyncOp
  Protected.l hr
  Protected.s VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK
  Protected.AUDIOCLIENT_ACTIVATION_PARAMS audioclientActivationParams
  Protected.PROPVARIANT activateParams
  Protected.ActivateCompletionHandlerObj *ach
  Protected.s deviceID
  Protected.i is
  
  hr = CoInitializeEx_(#Null, #COINIT_MULTITHREADED)
  If hr <> #S_OK
    Debug "CoInitialize_ failed, hr=" + Hex(hr)
    End
  EndIf
  
  *ach = ACH_New()
  
  audioclientActivationParams\ProcessLoopbackParams\ProcessLoopbackMode = #PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE
  audioclientActivationParams\ProcessLoopbackParams\TargetProcessId = processId
  audioclientActivationParams\ActivationType = #AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK
  
  activateParams\vt = #VT_BLOB
  activateParams\blob\cbSize = SizeOf(AUDIOCLIENT_ACTIVATION_PARAMS)
  activateParams\blob\pBlobData = @audioclientActivationParams
  
  hr = ActivateAudioInterfaceAsync("VAD\Process_Loopback", ?IID_IAudioClient, @activateParams, *ach, @asyncOp)
  
  ACH_Release(*ach)
  
  If asyncOp
    asyncOp\Release()
  EndIf
  
  If hr <> #S_OK
    Debug "ActivateAudioInterfaceAsync() failed hr=" + Hex(hr)
    End
  EndIf
  
EndProcedure

CreateThread(@main(), 27336) ; change process ID to match a process that outputs audio.

Repeat
  Delay(1000)
  ; add exit code here
ForEver

DataSection
  IID_IAudioClient:
  Data.l $1CB9AD4C
  Data.w $DBFA,$4c32
  Data.b $B1, $78, $C2, $F5, $68, $A7, $03, $B2
EndDataSection

DataSection
  IID_IAudioCaptureClient:
  Data.l $C8ADBD64
  Data.w $E71E,$48A0
  Data.b $A4,$DE,$18,$5C,$39,$5C,$D3,$17
EndDataSection

CloseLibrary(0)
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: WASAPI Loopback Capture from individual Apps!

Post by Quin »

Whoa...I didn't even know WASAPI let you do this, I only knew about doing it system wide. As I feel like I say on an at least weekly basis by now, nice work! :)
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Loopback Capture from individual Apps!

Post by AndyMK »

Quin wrote: Thu Feb 13, 2025 5:58 am Whoa...I didn't even know WASAPI let you do this, I only knew about doing it system wide. As I feel like I say on an at least weekly basis by now, nice work! :)
I am currently using this in a Windows Mixer replacement project. I have a pair of virtual audio devices (input/output) that all windows apps use. Each app streams its output audio to the virtual mixer where it is processed before going to the real audio interface via WASAPI exclusive or ASIO.
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: WASAPI Loopback Capture from individual Apps!

Post by Quin »

AndyMK wrote: Thu Feb 13, 2025 11:13 am I am currently using this in a Windows Mixer replacement project. I have a pair of virtual audio devices (input/output) that all windows apps use. Each app streams its output audio to the virtual mixer where it is processed before going to the real audio interface via WASAPI exclusive or ASIO.
That's really cool! This could make it possible to build software like Virtual Audio Cable/VB Cable in PB :)
User avatar
idle
Always Here
Always Here
Posts: 5835
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: WASAPI Loopback Capture from individual Apps!

Post by idle »

That's great thanks.
User avatar
Zapman
Enthusiast
Enthusiast
Posts: 205
Joined: Tue Jan 07, 2020 7:27 pm

Re: WASAPI Loopback Capture from individual Apps!

Post by Zapman »

If it can help, here is how to get the list of current threads and processes on your PC :

Code: Select all

Structure ThreadList_Structure
  ID.i
  ProcessID.i
  ProcessName$
EndStructure


Procedure.s GetProcessNameFromID(ProcessID)
  Protected snap, Proc32.PROCESSENTRY32, ProcessName.s = "N/A"

  snap = CreateToolhelp32Snapshot_(#TH32CS_SNAPPROCESS, 0)
  If snap <> #INVALID_HANDLE_VALUE
    Proc32\dwSize = SizeOf(PROCESSENTRY32)
    If Process32First_(snap, @Proc32)
      Repeat
        If Proc32\th32ProcessID = ProcessID
          ProcessName = PeekS(@Proc32\szExeFile)
          Break
        EndIf
      Until Process32Next_(snap, @Proc32) = 0
    EndIf
    CloseHandle_(snap)
  EndIf

  ProcedureReturn ProcessName
EndProcedure

Procedure.i GetThreadList(List ThreadList.ThreadList_Structure())
  Protected snap.i, Thread32.THREADENTRY32
  Protected mProcessName$
  
  ClearList(ThreadList())
  snap = CreateToolhelp32Snapshot_(#TH32CS_SNAPTHREAD, 0)
  
  If snap And snap <> #INVALID_HANDLE_VALUE
    Thread32\dwSize = SizeOf(THREADENTRY32)
    While Thread32Next_(snap, @Thread32)
      If Thread32\th32ThreadID
        AddElement(ThreadList())
        ThreadList()\ID = Thread32\th32ThreadID
        ThreadList()\ProcessID = Thread32\th32OwnerProcessID
        
        ; Récupérer le nom du programme associé
        ThreadList()\ProcessName$ = GetProcessNameFromID(ThreadList()\ProcessID)
        If ThreadList()\ProcessName$ <> mProcessName$
          mProcessName$ = ThreadList()\ProcessName$
          Debug ThreadList()\ProcessName$
        EndIf
      EndIf
    Wend
  EndIf
  ProcedureReturn ListSize(ThreadList())
EndProcedure

NewList ThreadList.ThreadList_Structure()

If GetThreadList(ThreadList())
  ForEach ThreadList()
    Debug "Thread ID: " + Str(ThreadList()\ID) + " Process ID: " + Str(ThreadList()\ProcessID) + " Process name: " + ThreadList()\ProcessName$
  Next
EndIf
AndyMK
Enthusiast
Enthusiast
Posts: 582
Joined: Wed Jul 12, 2006 4:38 pm
Location: UK

Re: WASAPI Loopback Capture from individual Apps!

Post by AndyMK »

Zapman wrote: Sun Feb 23, 2025 11:20 am If it can help, here is how to get the list of current threads and processes on your PC
See https://www.purebasic.fr/english/viewto ... 62#p636462
It monitors the audio engine for new audio sessions and returns the ProcessId plus other information. It's a bit buggy but i'm working on an update
Post Reply