WASAPI Loopback Capture from individual Apps!
Posted: Wed Feb 12, 2025 11:02 am
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:
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
Enter you process ID here:
Code: Select all
CreateThread(@main(), 27336) ; change process ID to match a process that outputs audio.
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)