diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..193102c4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "plugins/libs/wil"] + path = plugins/libs/wil + url = https://github.com/microsoft/wil.git diff --git a/LunaTranslator/LunaTranslator/gui/showword.py b/LunaTranslator/LunaTranslator/gui/showword.py index 2e0c0a00..976083e5 100644 --- a/LunaTranslator/LunaTranslator/gui/showword.py +++ b/LunaTranslator/LunaTranslator/gui/showword.py @@ -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) diff --git a/plugins/libs/libs.cmake b/plugins/libs/libs.cmake index 10c2e950..14974665 100644 --- a/plugins/libs/libs.cmake +++ b/plugins/libs/libs.cmake @@ -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") diff --git a/plugins/libs/wil b/plugins/libs/wil new file mode 160000 index 00000000..96326354 --- /dev/null +++ b/plugins/libs/wil @@ -0,0 +1 @@ +Subproject commit 963263543179642aa69addd13697a609c7b838d9 diff --git a/plugins/shareddllproxy/CMakeLists.txt b/plugins/shareddllproxy/CMakeLists.txt index 3b4be73b..c0ced5ba 100644 --- a/plugins/shareddllproxy/CMakeLists.txt +++ b/plugins/shareddllproxy/CMakeLists.txt @@ -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() - - \ No newline at end of file diff --git a/plugins/shareddllproxy/applicationloopbackaudio/Common.h b/plugins/shareddllproxy/applicationloopbackaudio/Common.h new file mode 100644 index 00000000..a2214497 --- /dev/null +++ b/plugins/shareddllproxy/applicationloopbackaudio/Common.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#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 diff --git a/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.cpp b/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.cpp new file mode 100644 index 00000000..79484b12 --- /dev/null +++ b/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.cpp @@ -0,0 +1,425 @@ +#include +#include +#include +#include + +#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 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 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; +} diff --git a/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.h b/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.h new file mode 100644 index 00000000..ec1aabef --- /dev/null +++ b/plugins/shareddllproxy/applicationloopbackaudio/LoopbackCapture.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#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 m_AudioClient; + WAVEFORMATEX m_CaptureFormat{}; + UINT32 m_BufferFrames = 0; + wil::com_ptr_nothrow m_AudioCaptureClient; + wil::com_ptr_nothrow 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; +}; diff --git a/plugins/shareddllproxy/applicationloopbackaudio/runer.cpp b/plugins/shareddllproxy/applicationloopbackaudio/runer.cpp new file mode 100644 index 00000000..5fbdc6b0 --- /dev/null +++ b/plugins/shareddllproxy/applicationloopbackaudio/runer.cpp @@ -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; +} \ No newline at end of file diff --git a/plugins/shareddllproxy/shareddllproxy.cpp b/plugins/shareddllproxy/shareddllproxy.cpp index 58fb1d87..65bbadb1 100644 --- a/plugins/shareddllproxy/shareddllproxy.cpp +++ b/plugins/shareddllproxy/shareddllproxy.cpp @@ -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);