mirror of
https://github.com/HIllya51/LunaTranslator.git
synced 2024-12-26 23:24:13 +08:00
loopback
This commit is contained in:
parent
aadc0befd9
commit
ff5e706b78
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "plugins/libs/wil"]
|
||||
path = plugins/libs/wil
|
||||
url = https://github.com/microsoft/wil.git
|
@ -19,7 +19,7 @@ from traceback import print_exc
|
||||
import requests, json, subprocess, time
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
|
||||
import qtawesome, functools, os, base64
|
||||
import gobject, uuid, signal
|
||||
import gobject, uuid, windows, platform
|
||||
from myutils.config import globalconfig, _TR, static_data
|
||||
import myutils.ankiconnect as anki
|
||||
from gui.usefulwidget import (
|
||||
@ -33,6 +33,8 @@ from gui.usefulwidget import (
|
||||
getcolorbutton,
|
||||
tabadd_lazy,
|
||||
)
|
||||
from myutils.subproc import subproc_w, autoproc
|
||||
|
||||
from myutils.wrapper import threader
|
||||
from myutils.ocrutil import imageCut, ocr_run
|
||||
from gui.rangeselect import rangeselct_function
|
||||
@ -63,6 +65,35 @@ class ffmpeg_virtual_audio_capturer:
|
||||
pass
|
||||
|
||||
|
||||
class loopbackrecorder:
|
||||
def __init__(self):
|
||||
os.makedirs("./cache/tts", exist_ok=True)
|
||||
self.file = os.path.abspath(
|
||||
os.path.join("./cache/tts", str(time.time()) + ".wav")
|
||||
)
|
||||
try:
|
||||
if platform.architecture()[0] == "64bit":
|
||||
_6432 = "64"
|
||||
elif platform.architecture()[0] == "32bit":
|
||||
_6432 = "32"
|
||||
self.waitsignal = str(time.time())
|
||||
self.engine = autoproc(
|
||||
subproc_w(
|
||||
'./files/plugins/shareddllproxy{}.exe recordaudio "{}" "{}"'.format(
|
||||
_6432, self.file, self.waitsignal
|
||||
),
|
||||
name="recordaudio",
|
||||
)
|
||||
)
|
||||
except:
|
||||
print_exc()
|
||||
|
||||
def end(self):
|
||||
windows.SetEvent(
|
||||
windows.AutoHandle(windows.CreateEvent(False, False, self.waitsignal))
|
||||
)
|
||||
|
||||
|
||||
class statusbutton(QPushButton):
|
||||
statuschanged1 = pyqtSignal(int)
|
||||
statuschanged2 = pyqtSignal(int)
|
||||
@ -395,7 +426,10 @@ class AnkiWindow(QWidget):
|
||||
|
||||
def startorendrecord(self, target: QLineEdit, idx):
|
||||
if idx == 1:
|
||||
self.recorder = ffmpeg_virtual_audio_capturer()
|
||||
if len(globalconfig["ffmpeg"]) and os.path.exists(globalconfig["ffmpeg"]):
|
||||
self.recorder = ffmpeg_virtual_audio_capturer()
|
||||
else:
|
||||
self.recorder = loopbackrecorder()
|
||||
else:
|
||||
self.recorder.end()
|
||||
target.setText(self.recorder.file)
|
||||
|
@ -7,6 +7,7 @@ endif()
|
||||
include_directories(${CMAKE_CURRENT_LIST_DIR})
|
||||
include_directories(${CMAKE_CURRENT_LIST_DIR}/Detours-4.0.1/include)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_LIST_DIR}/wil/include)
|
||||
|
||||
if(${CMAKE_SIZEOF_VOID_P} EQUAL 4)
|
||||
set(LTLPlatform "Win32")
|
||||
|
1
plugins/libs/wil
Submodule
1
plugins/libs/wil
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 963263543179642aa69addd13697a609c7b838d9
|
@ -12,19 +12,17 @@ generate_product_version(
|
||||
)
|
||||
|
||||
|
||||
add_executable(shareddllproxy shareddllproxy.cpp dllinject.cpp ntleas.cpp aspatch.cpp update.cpp ${versioninfo})
|
||||
add_executable(shareddllproxy applicationloopbackaudio/LoopbackCapture.cpp applicationloopbackaudio/runer.cpp shareddllproxy.cpp dllinject.cpp ntleas.cpp aspatch.cpp update.cpp ${versioninfo})
|
||||
target_precompile_headers(shareddllproxy REUSE_FROM pch)
|
||||
if(${CMAKE_SIZEOF_VOID_P} EQUAL 8)
|
||||
|
||||
target_link_libraries(shareddllproxy ${Detours})
|
||||
target_link_libraries(shareddllproxy Mfplat mfuuid ${Detours})
|
||||
set_target_properties(shareddllproxy PROPERTIES OUTPUT_NAME "shareddllproxy64")
|
||||
else()
|
||||
add_subdirectory(voiceroid2)
|
||||
add_library(x86lib dreye.cpp jbj7.cpp kingsoft.cpp le.cpp neospeech.cpp ../implsapi.cpp LR.cpp)
|
||||
target_precompile_headers(voiceroid2 REUSE_FROM pch)
|
||||
target_precompile_headers(x86lib REUSE_FROM pch)
|
||||
target_link_libraries(shareddllproxy x86lib voiceroid2 ${Detours})
|
||||
target_link_libraries(shareddllproxy Mfplat mfuuid x86lib voiceroid2 ${Detours})
|
||||
set_target_properties(shareddllproxy PROPERTIES OUTPUT_NAME "shareddllproxy32")
|
||||
endif()
|
||||
|
||||
|
58
plugins/shareddllproxy/applicationloopbackaudio/Common.h
Normal file
58
plugins/shareddllproxy/applicationloopbackaudio/Common.h
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <mfidl.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfobjects.h>
|
||||
|
||||
#ifndef METHODASYNCCALLBACK
|
||||
#define METHODASYNCCALLBACK(Parent, AsyncCallback, pfnCallback) \
|
||||
class Callback##AsyncCallback :\
|
||||
public IMFAsyncCallback \
|
||||
{ \
|
||||
public: \
|
||||
Callback##AsyncCallback() : \
|
||||
_parent(((Parent*)((BYTE*)this - offsetof(Parent, m_x##AsyncCallback)))), \
|
||||
_dwQueueID( MFASYNC_CALLBACK_QUEUE_MULTITHREADED ) \
|
||||
{ \
|
||||
} \
|
||||
\
|
||||
STDMETHOD_( ULONG, AddRef )() \
|
||||
{ \
|
||||
return _parent->AddRef(); \
|
||||
} \
|
||||
STDMETHOD_( ULONG, Release )() \
|
||||
{ \
|
||||
return _parent->Release(); \
|
||||
} \
|
||||
STDMETHOD( QueryInterface )( REFIID riid, void **ppvObject ) \
|
||||
{ \
|
||||
if (riid == IID_IMFAsyncCallback || riid == IID_IUnknown) \
|
||||
{ \
|
||||
(*ppvObject) = this; \
|
||||
AddRef(); \
|
||||
return S_OK; \
|
||||
} \
|
||||
*ppvObject = NULL; \
|
||||
return E_NOINTERFACE; \
|
||||
} \
|
||||
STDMETHOD( GetParameters )( \
|
||||
/* [out] */ __RPC__out DWORD *pdwFlags, \
|
||||
/* [out] */ __RPC__out DWORD *pdwQueue) \
|
||||
{ \
|
||||
*pdwFlags = 0; \
|
||||
*pdwQueue = _dwQueueID; \
|
||||
return S_OK; \
|
||||
} \
|
||||
STDMETHOD( Invoke )( /* [out] */ __RPC__out IMFAsyncResult * pResult ) \
|
||||
{ \
|
||||
_parent->pfnCallback( pResult ); \
|
||||
return S_OK; \
|
||||
} \
|
||||
void SetQueueID( DWORD dwQueueID ) { _dwQueueID = dwQueueID; } \
|
||||
\
|
||||
protected: \
|
||||
Parent* _parent; \
|
||||
DWORD _dwQueueID; \
|
||||
\
|
||||
} m_x##AsyncCallback;
|
||||
#endif
|
@ -0,0 +1,425 @@
|
||||
#include <shlobj.h>
|
||||
#include <wchar.h>
|
||||
#include <iostream>
|
||||
#include <audioclientactivationparams.h>
|
||||
|
||||
#include "LoopbackCapture.h"
|
||||
|
||||
#define BITS_PER_BYTE 8
|
||||
|
||||
HRESULT CLoopbackCapture::SetDeviceStateErrorIfFailed(HRESULT hr)
|
||||
{
|
||||
if (FAILED(hr))
|
||||
{
|
||||
m_DeviceState = DeviceState::Error;
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
HRESULT CLoopbackCapture::InitializeLoopbackCapture()
|
||||
{
|
||||
// Create events for sample ready or user stop
|
||||
RETURN_IF_FAILED(m_SampleReadyEvent.create(wil::EventOptions::None));
|
||||
|
||||
// Initialize MF
|
||||
RETURN_IF_FAILED(MFStartup(MF_VERSION, MFSTARTUP_LITE));
|
||||
|
||||
// Register MMCSS work queue
|
||||
DWORD dwTaskID = 0;
|
||||
RETURN_IF_FAILED(MFLockSharedWorkQueue(L"Capture", 0, &dwTaskID, &m_dwQueueID));
|
||||
|
||||
// Set the capture event work queue to use the MMCSS queue
|
||||
m_xSampleReady.SetQueueID(m_dwQueueID);
|
||||
|
||||
// Create the completion event as auto-reset
|
||||
RETURN_IF_FAILED(m_hActivateCompleted.create(wil::EventOptions::None));
|
||||
|
||||
// Create the capture-stopped event as auto-reset
|
||||
RETURN_IF_FAILED(m_hCaptureStopped.create(wil::EventOptions::None));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
CLoopbackCapture::~CLoopbackCapture()
|
||||
{
|
||||
if (m_dwQueueID != 0)
|
||||
{
|
||||
MFUnlockWorkQueue(m_dwQueueID);
|
||||
}
|
||||
}
|
||||
typedef HRESULT (STDAPICALLTYPE *ActivateAudioInterfaceAsync_t)(
|
||||
_In_ LPCWSTR deviceInterfacePath,
|
||||
_In_ REFIID riid,
|
||||
_In_opt_ PROPVARIANT *activationParams,
|
||||
_In_ IActivateAudioInterfaceCompletionHandler *completionHandler,
|
||||
_COM_Outptr_ IActivateAudioInterfaceAsyncOperation **activationOperation
|
||||
);
|
||||
HRESULT CLoopbackCapture::ActivateAudioInterface(DWORD processId, bool includeProcessTree)
|
||||
{
|
||||
return SetDeviceStateErrorIfFailed([&]() -> HRESULT
|
||||
{
|
||||
AUDIOCLIENT_ACTIVATION_PARAMS audioclientActivationParams = {};
|
||||
audioclientActivationParams.ActivationType = AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK;
|
||||
audioclientActivationParams.ProcessLoopbackParams.ProcessLoopbackMode = includeProcessTree ?
|
||||
PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE : PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE;
|
||||
audioclientActivationParams.ProcessLoopbackParams.TargetProcessId = processId;
|
||||
|
||||
PROPVARIANT activateParams = {};
|
||||
activateParams.vt = VT_BLOB;
|
||||
activateParams.blob.cbSize = sizeof(audioclientActivationParams);
|
||||
activateParams.blob.pBlobData = (BYTE*)&audioclientActivationParams;
|
||||
|
||||
wil::com_ptr_nothrow<IActivateAudioInterfaceAsyncOperation> asyncOp;
|
||||
auto pActivateAudioInterfaceAsync=(ActivateAudioInterfaceAsync_t)GetProcAddress(LoadLibrary(L"Mmdevapi.dll"),"ActivateAudioInterfaceAsync");
|
||||
if(pActivateAudioInterfaceAsync==0)return S_FALSE;
|
||||
RETURN_IF_FAILED(pActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient), &activateParams, this, &asyncOp));
|
||||
|
||||
// Wait for activation completion
|
||||
m_hActivateCompleted.wait();
|
||||
|
||||
return m_activateResult;
|
||||
}());
|
||||
}
|
||||
|
||||
//
|
||||
// ActivateCompleted()
|
||||
//
|
||||
// Callback implementation of ActivateAudioInterfaceAsync function. This will be called on MTA thread
|
||||
// when results of the activation are available.
|
||||
//
|
||||
HRESULT CLoopbackCapture::ActivateCompleted(IActivateAudioInterfaceAsyncOperation* operation)
|
||||
{
|
||||
m_activateResult = SetDeviceStateErrorIfFailed([&]()->HRESULT
|
||||
{
|
||||
// Check for a successful activation result
|
||||
HRESULT hrActivateResult = E_UNEXPECTED;
|
||||
wil::com_ptr_nothrow<IUnknown> punkAudioInterface;
|
||||
RETURN_IF_FAILED(operation->GetActivateResult(&hrActivateResult, &punkAudioInterface));
|
||||
RETURN_IF_FAILED(hrActivateResult);
|
||||
|
||||
// Get the pointer for the Audio Client
|
||||
RETURN_IF_FAILED(punkAudioInterface.copy_to(&m_AudioClient));
|
||||
|
||||
// The app can also call m_AudioClient->GetMixFormat instead to get the capture format.
|
||||
// 16 - bit PCM format.
|
||||
m_CaptureFormat.wFormatTag = WAVE_FORMAT_PCM;
|
||||
m_CaptureFormat.nChannels = 2;
|
||||
m_CaptureFormat.nSamplesPerSec = 44100;
|
||||
m_CaptureFormat.wBitsPerSample = 16;
|
||||
m_CaptureFormat.nBlockAlign = m_CaptureFormat.nChannels * m_CaptureFormat.wBitsPerSample / BITS_PER_BYTE;
|
||||
m_CaptureFormat.nAvgBytesPerSec = m_CaptureFormat.nSamplesPerSec * m_CaptureFormat.nBlockAlign;
|
||||
|
||||
// Initialize the AudioClient in Shared Mode with the user specified buffer
|
||||
RETURN_IF_FAILED(m_AudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
200000,
|
||||
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM,
|
||||
&m_CaptureFormat,
|
||||
nullptr));
|
||||
|
||||
// Get the maximum size of the AudioClient Buffer
|
||||
RETURN_IF_FAILED(m_AudioClient->GetBufferSize(&m_BufferFrames));
|
||||
|
||||
// Get the capture client
|
||||
RETURN_IF_FAILED(m_AudioClient->GetService(IID_PPV_ARGS(&m_AudioCaptureClient)));
|
||||
|
||||
// Create Async callback for sample events
|
||||
RETURN_IF_FAILED(MFCreateAsyncResult(nullptr, &m_xSampleReady, nullptr, &m_SampleReadyAsyncResult));
|
||||
|
||||
// Tell the system which event handle it should signal when an audio buffer is ready to be processed by the client
|
||||
RETURN_IF_FAILED(m_AudioClient->SetEventHandle(m_SampleReadyEvent.get()));
|
||||
|
||||
// Creates the WAV file.
|
||||
RETURN_IF_FAILED(CreateWAVFile());
|
||||
|
||||
// Everything is ready.
|
||||
m_DeviceState = DeviceState::Initialized;
|
||||
|
||||
return S_OK;
|
||||
}());
|
||||
|
||||
// Let ActivateAudioInterface know that m_activateResult has the result of the activation attempt.
|
||||
m_hActivateCompleted.SetEvent();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
//
|
||||
// CreateWAVFile()
|
||||
//
|
||||
// Creates a WAV file in music folder
|
||||
//
|
||||
HRESULT CLoopbackCapture::CreateWAVFile()
|
||||
{
|
||||
return SetDeviceStateErrorIfFailed([&]()->HRESULT
|
||||
{
|
||||
m_hFile.reset(CreateFile(m_outputFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
|
||||
RETURN_LAST_ERROR_IF(!m_hFile);
|
||||
|
||||
// Create and write the WAV header
|
||||
|
||||
// 1. RIFF chunk descriptor
|
||||
DWORD header[] = {
|
||||
FCC('RIFF'), // RIFF header
|
||||
0, // Total size of WAV (will be filled in later)
|
||||
FCC('WAVE'), // WAVE FourCC
|
||||
FCC('fmt '), // Start of 'fmt ' chunk
|
||||
sizeof(m_CaptureFormat) // Size of fmt chunk
|
||||
};
|
||||
DWORD dwBytesWritten = 0;
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(m_hFile.get(), header, sizeof(header), &dwBytesWritten, NULL));
|
||||
|
||||
m_cbHeaderSize += dwBytesWritten;
|
||||
|
||||
// 2. The fmt sub-chunk
|
||||
WI_ASSERT(m_CaptureFormat.cbSize == 0);
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(m_hFile.get(), &m_CaptureFormat, sizeof(m_CaptureFormat), &dwBytesWritten, NULL));
|
||||
m_cbHeaderSize += dwBytesWritten;
|
||||
|
||||
// 3. The data sub-chunk
|
||||
DWORD data[] = { FCC('data'), 0 }; // Start of 'data' chunk
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(m_hFile.get(), data, sizeof(data), &dwBytesWritten, NULL));
|
||||
m_cbHeaderSize += dwBytesWritten;
|
||||
|
||||
return S_OK;
|
||||
}());
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// FixWAVHeader()
|
||||
//
|
||||
// The size values were not known when we originally wrote the header, so now go through and fix the values
|
||||
//
|
||||
HRESULT CLoopbackCapture::FixWAVHeader()
|
||||
{
|
||||
// Write the size of the 'data' chunk first
|
||||
DWORD dwPtr = SetFilePointer(m_hFile.get(), m_cbHeaderSize - sizeof(DWORD), NULL, FILE_BEGIN);
|
||||
RETURN_LAST_ERROR_IF(INVALID_SET_FILE_POINTER == dwPtr);
|
||||
|
||||
DWORD dwBytesWritten = 0;
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(m_hFile.get(), &m_cbDataSize, sizeof(DWORD), &dwBytesWritten, NULL));
|
||||
|
||||
// Write the total file size, minus RIFF chunk and size
|
||||
// sizeof(DWORD) == sizeof(FOURCC)
|
||||
RETURN_LAST_ERROR_IF(INVALID_SET_FILE_POINTER == SetFilePointer(m_hFile.get(), sizeof(DWORD), NULL, FILE_BEGIN));
|
||||
|
||||
DWORD cbTotalSize = m_cbDataSize + m_cbHeaderSize - 8;
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(m_hFile.get(), &cbTotalSize, sizeof(DWORD), &dwBytesWritten, NULL));
|
||||
|
||||
RETURN_IF_WIN32_BOOL_FALSE(FlushFileBuffers(m_hFile.get()));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT CLoopbackCapture::StartCaptureAsync(DWORD processId, bool includeProcessTree, PCWSTR outputFileName)
|
||||
{
|
||||
m_outputFileName = outputFileName;
|
||||
auto resetOutputFileName = wil::scope_exit([&] { m_outputFileName = nullptr; });
|
||||
|
||||
RETURN_IF_FAILED(InitializeLoopbackCapture());
|
||||
RETURN_IF_FAILED(ActivateAudioInterface(processId, includeProcessTree));
|
||||
|
||||
// We should be in the initialzied state if this is the first time through getting ready to capture.
|
||||
if (m_DeviceState == DeviceState::Initialized)
|
||||
{
|
||||
m_DeviceState = DeviceState::Starting;
|
||||
return MFPutWorkItem2(MFASYNC_CALLBACK_QUEUE_MULTITHREADED, 0, &m_xStartCapture, nullptr);
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
//
|
||||
// OnStartCapture()
|
||||
//
|
||||
// Callback method to start capture
|
||||
//
|
||||
HRESULT CLoopbackCapture::OnStartCapture(IMFAsyncResult* pResult)
|
||||
{
|
||||
return SetDeviceStateErrorIfFailed([&]()->HRESULT
|
||||
{
|
||||
// Start the capture
|
||||
RETURN_IF_FAILED(m_AudioClient->Start());
|
||||
|
||||
m_DeviceState = DeviceState::Capturing;
|
||||
MFPutWaitingWorkItem(m_SampleReadyEvent.get(), 0, m_SampleReadyAsyncResult.get(), &m_SampleReadyKey);
|
||||
|
||||
return S_OK;
|
||||
}());
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// StopCaptureAsync()
|
||||
//
|
||||
// Stop capture asynchronously via MF Work Item
|
||||
//
|
||||
HRESULT CLoopbackCapture::StopCaptureAsync()
|
||||
{
|
||||
RETURN_HR_IF(E_NOT_VALID_STATE, (m_DeviceState != DeviceState::Capturing) &&
|
||||
(m_DeviceState != DeviceState::Error));
|
||||
|
||||
m_DeviceState = DeviceState::Stopping;
|
||||
|
||||
RETURN_IF_FAILED(MFPutWorkItem2(MFASYNC_CALLBACK_QUEUE_MULTITHREADED, 0, &m_xStopCapture, nullptr));
|
||||
|
||||
// Wait for capture to stop
|
||||
m_hCaptureStopped.wait();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
//
|
||||
// OnStopCapture()
|
||||
//
|
||||
// Callback method to stop capture
|
||||
//
|
||||
HRESULT CLoopbackCapture::OnStopCapture(IMFAsyncResult* pResult)
|
||||
{
|
||||
// Stop capture by cancelling Work Item
|
||||
// Cancel the queued work item (if any)
|
||||
if (0 != m_SampleReadyKey)
|
||||
{
|
||||
MFCancelWorkItem(m_SampleReadyKey);
|
||||
m_SampleReadyKey = 0;
|
||||
}
|
||||
|
||||
m_AudioClient->Stop();
|
||||
m_SampleReadyAsyncResult.reset();
|
||||
|
||||
return FinishCaptureAsync();
|
||||
}
|
||||
|
||||
//
|
||||
// FinishCaptureAsync()
|
||||
//
|
||||
// Finalizes WAV file on a separate thread via MF Work Item
|
||||
//
|
||||
HRESULT CLoopbackCapture::FinishCaptureAsync()
|
||||
{
|
||||
// We should be flushing when this is called
|
||||
return MFPutWorkItem2(MFASYNC_CALLBACK_QUEUE_MULTITHREADED, 0, &m_xFinishCapture, nullptr);
|
||||
}
|
||||
|
||||
//
|
||||
// OnFinishCapture()
|
||||
//
|
||||
// Because of the asynchronous nature of the MF Work Queues and the DataWriter, there could still be
|
||||
// a sample processing. So this will get called to finalize the WAV header.
|
||||
//
|
||||
HRESULT CLoopbackCapture::OnFinishCapture(IMFAsyncResult* pResult)
|
||||
{
|
||||
// FixWAVHeader will set the DeviceStateStopped when all async tasks are complete
|
||||
HRESULT hr = FixWAVHeader();
|
||||
|
||||
m_DeviceState = DeviceState::Stopped;
|
||||
|
||||
m_hCaptureStopped.SetEvent();
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
//
|
||||
// OnSampleReady()
|
||||
//
|
||||
// Callback method when ready to fill sample buffer
|
||||
//
|
||||
HRESULT CLoopbackCapture::OnSampleReady(IMFAsyncResult* pResult)
|
||||
{
|
||||
if (SUCCEEDED(OnAudioSampleRequested()))
|
||||
{
|
||||
// Re-queue work item for next sample
|
||||
if (m_DeviceState == DeviceState::Capturing)
|
||||
{
|
||||
// Re-queue work item for next sample
|
||||
return MFPutWaitingWorkItem(m_SampleReadyEvent.get(), 0, m_SampleReadyAsyncResult.get(), &m_SampleReadyKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_DeviceState = DeviceState::Error;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
//
|
||||
// OnAudioSampleRequested()
|
||||
//
|
||||
// Called when audio device fires m_SampleReadyEvent
|
||||
//
|
||||
HRESULT CLoopbackCapture::OnAudioSampleRequested()
|
||||
{
|
||||
UINT32 FramesAvailable = 0;
|
||||
BYTE* Data = nullptr;
|
||||
DWORD dwCaptureFlags;
|
||||
UINT64 u64DevicePosition = 0;
|
||||
UINT64 u64QPCPosition = 0;
|
||||
DWORD cbBytesToCapture = 0;
|
||||
|
||||
auto lock = m_CritSec.lock();
|
||||
|
||||
// If this flag is set, we have already queued up the async call to finialize the WAV header
|
||||
// So we don't want to grab or write any more data that would possibly give us an invalid size
|
||||
if (m_DeviceState == DeviceState::Stopping)
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// A word on why we have a loop here;
|
||||
// Suppose it has been 10 milliseconds or so since the last time
|
||||
// this routine was invoked, and that we're capturing 48000 samples per second.
|
||||
//
|
||||
// The audio engine can be reasonably expected to have accumulated about that much
|
||||
// audio data - that is, about 480 samples.
|
||||
//
|
||||
// However, the audio engine is free to accumulate this in various ways:
|
||||
// a. as a single packet of 480 samples, OR
|
||||
// b. as a packet of 80 samples plus a packet of 400 samples, OR
|
||||
// c. as 48 packets of 10 samples each.
|
||||
//
|
||||
// In particular, there is no guarantee that this routine will be
|
||||
// run once for each packet.
|
||||
//
|
||||
// So every time this routine runs, we need to read ALL the packets
|
||||
// that are now available;
|
||||
//
|
||||
// We do this by calling IAudioCaptureClient::GetNextPacketSize
|
||||
// over and over again until it indicates there are no more packets remaining.
|
||||
while (SUCCEEDED(m_AudioCaptureClient->GetNextPacketSize(&FramesAvailable)) && FramesAvailable > 0)
|
||||
{
|
||||
cbBytesToCapture = FramesAvailable * m_CaptureFormat.nBlockAlign;
|
||||
|
||||
// WAV files have a 4GB (0xFFFFFFFF) size limit, so likely we have hit that limit when we
|
||||
// overflow here. Time to stop the capture
|
||||
if ((m_cbDataSize + cbBytesToCapture) < m_cbDataSize)
|
||||
{
|
||||
StopCaptureAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
// Get sample buffer
|
||||
RETURN_IF_FAILED(m_AudioCaptureClient->GetBuffer(&Data, &FramesAvailable, &dwCaptureFlags, &u64DevicePosition, &u64QPCPosition));
|
||||
|
||||
|
||||
// Write File
|
||||
if (m_DeviceState != DeviceState::Stopping)
|
||||
{
|
||||
DWORD dwBytesWritten = 0;
|
||||
RETURN_IF_WIN32_BOOL_FALSE(WriteFile(
|
||||
m_hFile.get(),
|
||||
Data,
|
||||
cbBytesToCapture,
|
||||
&dwBytesWritten,
|
||||
NULL));
|
||||
}
|
||||
|
||||
// Release buffer back
|
||||
m_AudioCaptureClient->ReleaseBuffer(FramesAvailable);
|
||||
|
||||
// Increase the size of our 'data' chunk. m_cbDataSize needs to be accurate
|
||||
m_cbDataSize += cbBytesToCapture;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <AudioClient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <initguid.h>
|
||||
#include <guiddef.h>
|
||||
#include <mfapi.h>
|
||||
|
||||
#include <wrl\implements.h>
|
||||
#include <wil\com.h>
|
||||
#include <wil\result.h>
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
using namespace Microsoft::WRL;
|
||||
|
||||
class CLoopbackCapture :
|
||||
public RuntimeClass< RuntimeClassFlags< ClassicCom >, FtmBase, IActivateAudioInterfaceCompletionHandler >
|
||||
{
|
||||
public:
|
||||
//CLoopbackCapture() = default;
|
||||
~CLoopbackCapture();
|
||||
|
||||
HRESULT StartCaptureAsync(DWORD processId, bool includeProcessTree, PCWSTR outputFileName);
|
||||
HRESULT StopCaptureAsync();
|
||||
|
||||
METHODASYNCCALLBACK(CLoopbackCapture, StartCapture, OnStartCapture);
|
||||
METHODASYNCCALLBACK(CLoopbackCapture, StopCapture, OnStopCapture);
|
||||
METHODASYNCCALLBACK(CLoopbackCapture, SampleReady, OnSampleReady);
|
||||
METHODASYNCCALLBACK(CLoopbackCapture, FinishCapture, OnFinishCapture);
|
||||
|
||||
// IActivateAudioInterfaceCompletionHandler
|
||||
STDMETHOD(ActivateCompleted)(IActivateAudioInterfaceAsyncOperation* operation);
|
||||
|
||||
private:
|
||||
// NB: All states >= Initialized will allow some methods
|
||||
// to be called successfully on the Audio Client
|
||||
enum class DeviceState
|
||||
{
|
||||
Uninitialized,
|
||||
Error,
|
||||
Initialized,
|
||||
Starting,
|
||||
Capturing,
|
||||
Stopping,
|
||||
Stopped,
|
||||
};
|
||||
|
||||
HRESULT OnStartCapture(IMFAsyncResult* pResult);
|
||||
HRESULT OnStopCapture(IMFAsyncResult* pResult);
|
||||
HRESULT OnFinishCapture(IMFAsyncResult* pResult);
|
||||
HRESULT OnSampleReady(IMFAsyncResult* pResult);
|
||||
|
||||
HRESULT InitializeLoopbackCapture();
|
||||
HRESULT CreateWAVFile();
|
||||
HRESULT FixWAVHeader();
|
||||
HRESULT OnAudioSampleRequested();
|
||||
|
||||
HRESULT ActivateAudioInterface(DWORD processId, bool includeProcessTree);
|
||||
HRESULT FinishCaptureAsync();
|
||||
|
||||
HRESULT SetDeviceStateErrorIfFailed(HRESULT hr);
|
||||
|
||||
wil::com_ptr_nothrow<IAudioClient> m_AudioClient;
|
||||
WAVEFORMATEX m_CaptureFormat{};
|
||||
UINT32 m_BufferFrames = 0;
|
||||
wil::com_ptr_nothrow<IAudioCaptureClient> m_AudioCaptureClient;
|
||||
wil::com_ptr_nothrow<IMFAsyncResult> m_SampleReadyAsyncResult;
|
||||
|
||||
wil::unique_event_nothrow m_SampleReadyEvent;
|
||||
MFWORKITEM_KEY m_SampleReadyKey = 0;
|
||||
wil::unique_hfile m_hFile;
|
||||
wil::critical_section m_CritSec;
|
||||
DWORD m_dwQueueID = 0;
|
||||
DWORD m_cbHeaderSize = 0;
|
||||
DWORD m_cbDataSize = 0;
|
||||
|
||||
// These two members are used to communicate between the main thread
|
||||
// and the ActivateCompleted callback.
|
||||
PCWSTR m_outputFileName = nullptr;
|
||||
HRESULT m_activateResult = E_UNEXPECTED;
|
||||
|
||||
DeviceState m_DeviceState{ DeviceState::Uninitialized };
|
||||
wil::unique_event_nothrow m_hActivateCompleted;
|
||||
wil::unique_event_nothrow m_hCaptureStopped;
|
||||
};
|
12
plugins/shareddllproxy/applicationloopbackaudio/runer.cpp
Normal file
12
plugins/shareddllproxy/applicationloopbackaudio/runer.cpp
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
#include "LoopbackCapture.h"
|
||||
int recordaudio(int argc, wchar_t *argv[])
|
||||
{
|
||||
CLoopbackCapture loopbackCapture;
|
||||
loopbackCapture.StartCaptureAsync(GetCurrentProcessId(), false, argv[1]);
|
||||
WaitForSingleObject(
|
||||
CreateEventW(&allAccess, FALSE, FALSE, argv[2]),
|
||||
INFINITE);
|
||||
loopbackCapture.StopCaptureAsync();
|
||||
return 0;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#pragma comment(linker, "/subsystem:windows /entry:wmainCRTStartup")
|
||||
int recordaudio(int argc, wchar_t *argv[]);
|
||||
|
||||
int dllinjectwmain(int argc, wchar_t *argv[]);
|
||||
int ntleaswmain(int argc, wchar_t *wargv[]);
|
||||
@ -64,7 +65,8 @@ int wmain(int argc, wchar_t *argv[])
|
||||
return listprocessmodule(argc - 1, argv + 1);
|
||||
if (argv0 == L"update")
|
||||
return updatewmain(argc - 1, argv + 1);
|
||||
|
||||
if (argv0 == L"recordaudio")
|
||||
return recordaudio(argc - 1, argv + 1);
|
||||
#ifndef _WIN64
|
||||
else if (argv0 == L"LR")
|
||||
return LRwmain(argc - 1, argv + 1);
|
||||
|
Loading…
x
Reference in New Issue
Block a user