;Bilgus 2019 ; https://www.autoitscript.com/forum/topic/197769-wasapi-audio-capture-w-loopback-udf/ #Region ;**** Directives created by AutoIt3Wrapper_GUI **** #AutoIt3Wrapper_AU3Check_Parameters=-d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 #EndRegion ;**** Directives created by AutoIt3Wrapper_GUI **** #include 'WasApi_capture.au3' #include ;_IsPressed #include Global Const $g_sTMPFILE = @ScriptDir & "\wavecap.raw" Global $g_hUser32 = DllOpen("user32.dll") OnAutoItExitRegister("_Exit") ListAvailableEndpoints() Global $g_sID_Endpoint = _WASAPI_GetDefaultAudioEndpoint($eRender) Global $aEpInfo = _WASAPI_EndpointInfo($g_sID_Endpoint) ConsoleWrite("Capturing From: " & $aEpInfo[$eWASInfo_Name] & @CRLF & "ESC key ends capture" & @CRLF) ListDeviceProperties($g_sID_Endpoint) _Main($g_sID_Endpoint) Func _Exit() DllClose($g_hUser32) EndFunc ;==>_Exit Func Seconds2Time($fSec) ;Eliminates Float Rounding errors Local $iSec, $iH = 0, $iM = 0, $iS = 0, $iMs = 0 Local $aTime = StringRegExp(Round($fSec, 3), "(\d*)(\.?\d*)", 1) If IsArray($aTime) Then $iSec = Int($aTime[0]) $iS = Mod($iSec, 60) $iSec -= $iS $iSec /= 60 $iM = Mod($iSec, 60) $iSec -= $iM $iH = $iSec / 60 $iMs = $aTime[1] * 1000 EndIf Return StringFormat("%02d:%02d:%02d.%03d", $iH, $iM, $iS, $iMs) EndFunc ;==>Seconds2Time Func _Main($sDeviceID) Local Const $iBUFFER_MS = 500 Local $hFile = _WinAPI_CreateFileEx($g_sTMPFILE, $CREATE_ALWAYS, $GENERIC_WRITE) Local $aSC = _WASAPI_Stream_Open($sDeviceID, $iBUFFER_MS) ;Setup stream capture from default render device with a 500Ms buffer Local $pWFX = _WASAPI_Stream_WaveFormat($aSC) Local $nBlockAlign = _WFx_Get($pWFX, "nBlockAlign") Local $iAvgBytesPerSec = _WFx_Get($pWFX, "nAvgBytesPerSec") Local $hEvent = _WASAPI_Stream_CaptureEventHandle($aSC) ConsoleWrite("Buffer Sz: " & _WASAPI_Stream_BufferSizeBytes($aSC) & " Bytes" & @CRLF) Local $iFrameCapThresh = Int(_WASAPI_Stream_BufferSize($aSC) / ($iBUFFER_MS)) ;capture ~1ms of audio data If $iFrameCapThresh < 1 Then $iFrameCapThresh = 1 ConsoleWrite("Capture Every " & $iFrameCapThresh & " audio frame(s) (" & $iFrameCapThresh * $nBlockAlign & " bytes)" & @CRLF) Local $nBytes, $tWave = _WASAPI_RIFF_Header($aSC) _WinAPI_WriteFile($hFile, DllStructGetPtr($tWave), DllStructGetSize($tWave), $nBytes) $tWave = 0 ;No longer needed Local $oICaptureClient = _WASAPI_Stream_StartCapture($aSC) If @error Then ConsoleWrite("Unable to start Capture") Return EndIf Local $iBytesWritten = 0, $iGlitchCount = 0, $iFlags = 0, $iRes = 0 Local $bPaused, $iSilence = 10 * 15 ;15 seconds of silence before quit While $iSilence > 0 And Not _IsPressed("1B", $g_hUser32) $iRes = _WinAPI_WaitForSingleObject($hEvent, 100) If $iRes = 0 Then $iFlags = CapFunc($oICaptureClient, $hFile, $nBlockAlign, $iBytesWritten, $iGlitchCount, $iFrameCapThresh) $bPaused = False If $iFlags Then If BitAND($iFlags, $AUDCLNT_BUFFERFLAGS_SILENT) Then ConsoleWrite("Paused ( waiting for sound play ) - " & $iSilence & @CRLF) $iSilence -= 1 $bPaused = True EndIf If BitAND($iFlags, $AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) Then ConsoleWrite("Time Stamp Error" & @CRLF) EndIf If $iGlitchCount > 0 Then ConsoleWrite("Audio Glitch Detected" & @CRLF) $iGlitchCount = 0 EndIf EndIf ;ConsoleWrite($iBytesWritten & @crlf) Sleep(100) ElseIf $iRes = $WASAPI_EVENT_WAIT_TIMEOUT Then ConsoleWrite("Timeout" & @CRLF) Else ConsoleWrite("Error Waiting for event" & @CRLF) ExitLoop EndIf If $bPaused Then ;Thanks Argumentum ToolTip("[ESC] Exits, Paused ( waiting for sound play ) - " & $iSilence / 10 & "s") Else ToolTip("[ESC] Exits, " & Seconds2Time($iBytesWritten / $iAvgBytesPerSec)) EndIf WEnd ConsoleWrite("Exit or Silence" & @CRLF) ToolTip("") _WASAPI_Stream_StopCapture($aSC, False) _WinAPI_FlushFileBuffers($hFile) ;Finalize the wav file WavSaver($hFile) ;Write the header data to beginning of capture file _WinAPI_CloseHandle($hFile) _WASAPI_Stream_Close($aSC) ; Local $sOutfile = @ScriptDir & "\wavecap" & @YDAY & "_" & @HOUR & @MIN & @SEC & ".wav" If FileMove($g_sTMPFILE, $sOutfile) Then Local $iFileSz = FileGetSize($sOutfile) ConsoleWrite($sOutfile & " - " & $iFileSz & " bytes - " & _ Seconds2Time(($iFileSz - DllStructGetSize(_WASAPI_RIFF_Header(0))) / $iAvgBytesPerSec) & @CRLF) Else MsgBox($MB_ICONERROR, @ScriptName & " Error", "Error renaming tmpfile " & $g_sTMPFILE) EndIf EndFunc ;==>_Main Func CapFunc($oICaptureClient, $hFileOpen, $nBlockAlign, ByRef $iBytesWritten, ByRef $iGlitchCount, $iFrameCapThresh = 1, $bCapture = True) ;Capture function for the stream, saves data directly to open file ;returns flags from the audioclient ;$AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY, ;$AUDCLNT_BUFFERFLAGS_SILENT ;$AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR ; ;If $AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY occurs $iGlitchCount is incremented and packet is skipped ;$iBytesWritten is incremented by number of bytes written ;If $bCapture = False all audio frames are discarded Local $iPacketLength = 0, $iNumFramesAvailable = 0, $iFlags = 0 Local $iFlags_Session = 0 ;Flags for any packet during this capture session Local $pData, $nBytes ;Check if there are any packets to retrieve While (Not $oICaptureClient.GetNextPacketSize($iPacketLength)) And $iPacketLength >= $iFrameCapThresh $iFlags = 0 $iFrameCapThresh = 1 ;get remaining frames ;Get the available data in the shared buffer. $oICaptureClient.GetBuffer($pData, $iNumFramesAvailable, $iFlags, 0, 0) $iFlags_Session = BitOR($iFlags_Session, $iFlags) If $iFlags = $AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY And $iBytesWritten <> 0 Then ;Win 7 Signals Glitch on first capture, Ignore it $iGlitchCount += 1 ElseIf $bCapture And BitAND($iFlags, $AUDCLNT_BUFFERFLAGS_SILENT) = 0 Then _WinAPI_WriteFile($hFileOpen, $pData, $iNumFramesAvailable * $nBlockAlign, $nBytes) $iBytesWritten += $nBytes EndIf $oICaptureClient.ReleaseBuffer($iNumFramesAvailable) ;We are done with the buffer release it back to client WEnd Return $iFlags_Session EndFunc ;==>CapFunc Func WavSaver($hFileOpen) ;Updates FileSize, DATALen, dwSampleLength(if applicable) fields in the wav header Local $tWaveHeader = _WASAPI_RIFF_Header(0) Local $iWaveHeaderLen = DllStructGetSize($tWaveHeader) Local $iFileSz = _WinAPI_SetFilePointer($hFileOpen, 0, 2) Local $nBytes If BitAND($iFileSz, 1) Then ;Padding byte needed Local $tPad = DllStructCreate("byte pad") _WinAPI_SetFilePointer($hFileOpen, 0, 2) ;write padding byte at eof _WinAPI_WriteFile($hFileOpen, DllStructGetPtr($tPad), 1, $nBytes) $iFileSz += $nBytes ConsoleWrite("Adding padding byte" & @CRLF) EndIf DllStructSetData($tWaveHeader, "FileSize", $iFileSz - 8) Local $iDataLen = $iFileSz - $iWaveHeaderLen DllStructSetData($tWaveHeader, "DATALen", $iDataLen) Local $wBitsPerSample = DllStructGetData($tWaveHeader, "wBitsPerSample") DllStructSetData($tWaveHeader, "dwSampleLength", $iDataLen / ($wBitsPerSample / 8)) _WinAPI_SetFilePointer($hFileOpen, 0, 0) ;Rewrite the wavheader at the beginning of the file _WinAPI_WriteFile($hFileOpen, DllStructGetPtr($tWaveHeader), $iWaveHeaderLen, $nBytes) If $nBytes <> $iWaveHeaderLen Then MsgBox($MB_ICONERROR, @ScriptName & " Error", "Error updating wav header") Return $iFileSz EndFunc ;==>WavSaver Func ListAvailableEndpoints() Local $aEP Local $aAvEPs = _WASAPI_EnumAudioEndpoints($eRender) ConsoleWrite("(" & $aAvEPs[0] & ") Available Render Endpoints:" & @CRLF) For $i = 1 To $aAvEPs[0] $aEP = _WASAPI_EndpointInfo($aAvEPs[$i]) ConsoleWrite($aEP[$eWASInfo_Name] & @CRLF) Next ConsoleWrite(@CRLF) $aAvEPs = _WASAPI_EnumAudioEndpoints($eCapture) ConsoleWrite("(" & $aAvEPs[0] & ") Available Capture Endpoints:" & @CRLF) For $i = 1 To $aAvEPs[0] $aEP = _WASAPI_EndpointInfo($aAvEPs[$i]) ConsoleWrite($aEP[$eWASInfo_Name] & @CRLF) Next ConsoleWrite(@CRLF & @CRLF) EndFunc ;==>ListAvailableEndpoints Func ListDeviceProperties($sDeviceID) Local $aInfo = _WASAPI_EndpointInfo($sDeviceID) ConsoleWrite("Device Name: " & $aInfo[$eWASInfo_Name] & @CRLF) ConsoleWrite("Description: " & $aInfo[$eWASinfo_Desc] & @CRLF) ConsoleWrite("Interface: " & $aInfo[$eWASinfo_Interface] & @CRLF) ConsoleWrite("System Name: " & $aInfo[$eWASinfo_SystemName] & @CRLF) ConsoleWrite("DeviceID: " & $aInfo[$eWASinfo_ID] & @CRLF) ConsoleWrite("GUID: " & $aInfo[$eWASinfo_GUID] & @CRLF) ConsoleWrite(@CRLF & @CRLF) EndFunc ;==>ListDeviceProperties