From d28d70de69594dbadddb011cea912d2517e0fb8b Mon Sep 17 00:00:00 2001 From: Toad King Date: Sun, 2 Aug 2020 16:38:51 -0500 Subject: [PATCH] Initial commit --- .gitignore | 144 ++++++++++ README.md | 11 + loopback-capture.sln | 28 ++ mono-to-stereo/cleanup.h | 121 +++++++++ mono-to-stereo/common.h | 14 + mono-to-stereo/guid.cpp | 4 + mono-to-stereo/log.h | 4 + mono-to-stereo/main.cpp | 172 ++++++++++++ mono-to-stereo/mono-to-stereo.cpp | 350 ++++++++++++++++++++++++ mono-to-stereo/mono-to-stereo.filters | 48 ++++ mono-to-stereo/mono-to-stereo.h | 21 ++ mono-to-stereo/mono-to-stereo.vcxproj | 153 +++++++++++ mono-to-stereo/prefs.cpp | 373 ++++++++++++++++++++++++++ mono-to-stereo/prefs.h | 14 + 14 files changed, 1457 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 loopback-capture.sln create mode 100644 mono-to-stereo/cleanup.h create mode 100644 mono-to-stereo/common.h create mode 100644 mono-to-stereo/guid.cpp create mode 100644 mono-to-stereo/log.h create mode 100644 mono-to-stereo/main.cpp create mode 100644 mono-to-stereo/mono-to-stereo.cpp create mode 100644 mono-to-stereo/mono-to-stereo.filters create mode 100644 mono-to-stereo/mono-to-stereo.h create mode 100644 mono-to-stereo/mono-to-stereo.vcxproj create mode 100644 mono-to-stereo/prefs.cpp create mode 100644 mono-to-stereo/prefs.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fc8fe8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool diff --git a/README.md b/README.md new file mode 100644 index 0000000..f858102 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# mono-to-stereo + +Takes a mono input and renders it as if it was an interleaved stereo input. Works on MS2109 capture +devices where the audio input is a 96khz mono stream but in actuality is a 48khz stereo stream with +the left and right channels swapped. In order to support this device better, swapping the left and +right channels is done by default. + +Original code based off of [Matthew van Eerde's loopback-capture](https://github.com/mvaneerde/blog/tree/master/loopback-capture) +project. + +Run `mono-to-stereo.exe -?` for usage instructions. diff --git a/loopback-capture.sln b/loopback-capture.sln new file mode 100644 index 0000000..f9cb0fa --- /dev/null +++ b/loopback-capture.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30309.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mono-to-stereo", "mono-to-stereo\mono-to-stereo.vcxproj", "{4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Debug|Win32.ActiveCfg = Debug|Win32 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Debug|Win32.Build.0 = Debug|Win32 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Debug|x64.ActiveCfg = Debug|x64 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Debug|x64.Build.0 = Debug|x64 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Release|Win32.ActiveCfg = Release|Win32 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Release|Win32.Build.0 = Release|Win32 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Release|x64.ActiveCfg = Release|x64 + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/mono-to-stereo/cleanup.h b/mono-to-stereo/cleanup.h new file mode 100644 index 0000000..0252cf6 --- /dev/null +++ b/mono-to-stereo/cleanup.h @@ -0,0 +1,121 @@ +// cleanup.h + +class AudioClientStopOnExit { +public: + AudioClientStopOnExit(IAudioClient *p) : m_p(p) {} + ~AudioClientStopOnExit() { + HRESULT hr = m_p->Stop(); + if (FAILED(hr)) { + ERR(L"IAudioClient::Stop failed: hr = 0x%08x", hr); + } + } + +private: + IAudioClient *m_p; +}; + +class AvRevertMmThreadCharacteristicsOnExit { +public: + AvRevertMmThreadCharacteristicsOnExit(HANDLE hTask) : m_hTask(hTask) {} + ~AvRevertMmThreadCharacteristicsOnExit() { + if (!AvRevertMmThreadCharacteristics(m_hTask)) { + ERR(L"AvRevertMmThreadCharacteristics failed: last error is %d", GetLastError()); + } + } +private: + HANDLE m_hTask; +}; + +class CancelWaitableTimerOnExit { +public: + CancelWaitableTimerOnExit(HANDLE h) : m_h(h) {} + ~CancelWaitableTimerOnExit() { + if (!CancelWaitableTimer(m_h)) { + ERR(L"CancelWaitableTimer failed: last error is %d", GetLastError()); + } + } +private: + HANDLE m_h; +}; + +class CloseHandleOnExit { +public: + CloseHandleOnExit(HANDLE h) : m_h(h) {} + ~CloseHandleOnExit() { + if (!CloseHandle(m_h)) { + ERR(L"CloseHandle failed: last error is %d", GetLastError()); + } + } + +private: + HANDLE m_h; +}; + +class CoTaskMemFreeOnExit { +public: + CoTaskMemFreeOnExit(PVOID p) : m_p(p) {} + ~CoTaskMemFreeOnExit() { + CoTaskMemFree(m_p); + } + +private: + PVOID m_p; +}; + +class CoUninitializeOnExit { +public: + ~CoUninitializeOnExit() { + CoUninitialize(); + } +}; + +class PropVariantClearOnExit { +public: + PropVariantClearOnExit(PROPVARIANT *p) : m_p(p) {} + ~PropVariantClearOnExit() { + HRESULT hr = PropVariantClear(m_p); + if (FAILED(hr)) { + ERR(L"PropVariantClear failed: hr = 0x%08x", hr); + } + } + +private: + PROPVARIANT *m_p; +}; + +class ReleaseOnExit { +public: + ReleaseOnExit(IUnknown *p) : m_p(p) {} + ~ReleaseOnExit() { + m_p->Release(); + } + +private: + IUnknown *m_p; +}; + +class SetEventOnExit { +public: + SetEventOnExit(HANDLE h) : m_h(h) {} + ~SetEventOnExit() { + if (!SetEvent(m_h)) { + ERR(L"SetEvent failed: last error is %d", GetLastError()); + } + } +private: + HANDLE m_h; +}; + +class WaitForSingleObjectOnExit { +public: + WaitForSingleObjectOnExit(HANDLE h) : m_h(h) {} + ~WaitForSingleObjectOnExit() { + DWORD dwWaitResult = WaitForSingleObject(m_h, INFINITE); + if (WAIT_OBJECT_0 != dwWaitResult) { + ERR(L"WaitForSingleObject returned unexpected result 0x%08x, last error is %d", dwWaitResult, GetLastError()); + } + } + +private: + HANDLE m_h; +}; diff --git a/mono-to-stereo/common.h b/mono-to-stereo/common.h new file mode 100644 index 0000000..d7521a2 --- /dev/null +++ b/mono-to-stereo/common.h @@ -0,0 +1,14 @@ +// common.h + +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "cleanup.h" +#include "prefs.h" +#include "mono-to-stereo.h" diff --git a/mono-to-stereo/guid.cpp b/mono-to-stereo/guid.cpp new file mode 100644 index 0000000..1ac082a --- /dev/null +++ b/mono-to-stereo/guid.cpp @@ -0,0 +1,4 @@ +// guid.cpp + +#include +#include "common.h" diff --git a/mono-to-stereo/log.h b/mono-to-stereo/log.h new file mode 100644 index 0000000..e2cf28c --- /dev/null +++ b/mono-to-stereo/log.h @@ -0,0 +1,4 @@ +// log.h + +#define LOG(format, ...) wprintf(format L"\n", __VA_ARGS__) +#define ERR(format, ...) LOG(L"Error: " format, __VA_ARGS__) diff --git a/mono-to-stereo/main.cpp b/mono-to-stereo/main.cpp new file mode 100644 index 0000000..68d2805 --- /dev/null +++ b/mono-to-stereo/main.cpp @@ -0,0 +1,172 @@ +// main.cpp + +#include "common.h" + +int do_everything(int argc, LPCWSTR argv[]); + +int _cdecl wmain(int argc, LPCWSTR argv[]) { + HRESULT hr = S_OK; + + hr = CoInitialize(NULL); + if (FAILED(hr)) { + ERR(L"CoInitialize failed: hr = 0x%08x", hr); + return -__LINE__; + } + CoUninitializeOnExit cuoe; + + return do_everything(argc, argv); +} + +int do_everything(int argc, LPCWSTR argv[]) { + HRESULT hr = S_OK; + + // parse command line + CPrefs prefs(argc, argv, hr); + if (FAILED(hr)) { + ERR(L"CPrefs::CPrefs constructor failed: hr = 0x%08x", hr); + return -__LINE__; + } + if (S_FALSE == hr) { + // nothing to do + return 0; + } + + // create a "loopback capture has started" event + HANDLE hStartedEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (NULL == hStartedEvent) { + ERR(L"CreateEvent failed: last error is %u", GetLastError()); + return -__LINE__; + } + CloseHandleOnExit closeStartedEvent(hStartedEvent); + + // create a "stop capturing now" event + HANDLE hStopEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (NULL == hStopEvent) { + ERR(L"CreateEvent failed: last error is %u", GetLastError()); + return -__LINE__; + } + CloseHandleOnExit closeStopEvent(hStopEvent); + + // create arguments for loopback capture thread + LoopbackCaptureThreadFunctionArguments threadArgs; + threadArgs.hr = E_UNEXPECTED; // thread will overwrite this + threadArgs.pMMInDevice = prefs.m_pMMInDevice; + threadArgs.pMMOutDevice = prefs.m_pMMOutDevice; + threadArgs.iBufferMs = prefs.m_iBufferMs; + threadArgs.bSwapChannels = prefs.m_bSwapChannels; + threadArgs.hStartedEvent = hStartedEvent; + threadArgs.hStopEvent = hStopEvent; + threadArgs.nFrames = 0; + + HANDLE hThread = CreateThread( + NULL, 0, + LoopbackCaptureThreadFunction, &threadArgs, + 0, NULL + ); + if (NULL == hThread) { + ERR(L"CreateThread failed: last error is %u", GetLastError()); + return -__LINE__; + } + CloseHandleOnExit closeThread(hThread); + + // wait for either capture to start or the thread to end + HANDLE waitArray[2] = { hStartedEvent, hThread }; + DWORD dwWaitResult; + dwWaitResult = WaitForMultipleObjects( + ARRAYSIZE(waitArray), waitArray, + FALSE, INFINITE + ); + + if (WAIT_OBJECT_0 + 1 == dwWaitResult) { + ERR(L"Thread aborted before starting to capture: hr = 0x%08x", threadArgs.hr); + return -__LINE__; + } + + if (WAIT_OBJECT_0 != dwWaitResult) { + ERR(L"Unexpected WaitForMultipleObjects return value %u", dwWaitResult); + return -__LINE__; + } + + // at this point capture is running + // wait for the user to press a key or for capture to error out + { + WaitForSingleObjectOnExit waitForThread(hThread); + SetEventOnExit setStopEvent(hStopEvent); + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + + if (INVALID_HANDLE_VALUE == hStdIn) { + ERR(L"GetStdHandle returned INVALID_HANDLE_VALUE: last error is %u", GetLastError()); + return -__LINE__; + } + + LOG(L"%s", L"Press Enter to quit..."); + + HANDLE rhHandles[2] = { hThread, hStdIn }; + + bool bKeepWaiting = true; + while (bKeepWaiting) { + + dwWaitResult = WaitForMultipleObjects(2, rhHandles, FALSE, INFINITE); + + switch (dwWaitResult) { + + case WAIT_OBJECT_0: // hThread + ERR(L"%s", L"The thread terminated early - something bad happened"); + bKeepWaiting = false; + break; + + case WAIT_OBJECT_0 + 1: // hStdIn + // see if any of them was an Enter key-up event + INPUT_RECORD rInput[128]; + DWORD nEvents; + if (!ReadConsoleInput(hStdIn, rInput, ARRAYSIZE(rInput), &nEvents)) { + ERR(L"ReadConsoleInput failed: last error is %u", GetLastError()); + bKeepWaiting = false; + } + else { + for (DWORD i = 0; i < nEvents; i++) { + if ( + KEY_EVENT == rInput[i].EventType && + VK_RETURN == rInput[i].Event.KeyEvent.wVirtualKeyCode && + !rInput[i].Event.KeyEvent.bKeyDown + ) { + LOG(L"%s", L"Stopping capture..."); + bKeepWaiting = false; + break; + } + } + // if none of them were Enter key-up events, + // continue waiting + } + break; + + default: + ERR(L"WaitForMultipleObjects returned unexpected value 0x%08x", dwWaitResult); + bKeepWaiting = false; + break; + } // switch + } // while + } // naked scope + + // at this point the thread is definitely finished + + DWORD exitCode; + if (!GetExitCodeThread(hThread, &exitCode)) { + ERR(L"GetExitCodeThread failed: last error is %u", GetLastError()); + return -__LINE__; + } + + if (0 != exitCode) { + ERR(L"Capture thread exit code is %u; expected 0", exitCode); + return -__LINE__; + } + + if (S_OK != threadArgs.hr) { + ERR(L"Thread HRESULT is 0x%08x", threadArgs.hr); + return -__LINE__; + } + + // let prefs' destructor call mmioClose + + return 0; +} diff --git a/mono-to-stereo/mono-to-stereo.cpp b/mono-to-stereo/mono-to-stereo.cpp new file mode 100644 index 0000000..3a78bac --- /dev/null +++ b/mono-to-stereo/mono-to-stereo.cpp @@ -0,0 +1,350 @@ +// mono-to-stereo.cpp + +#include "common.h" + +HRESULT LoopbackCapture( + IMMDevice *pMMInDevice, + IMMDevice *pMMOutDevice, + int iBufferMs, + bool bSwapChannels, + HANDLE hStartedEvent, + HANDLE hStopEvent, + PUINT32 pnFrames +); + +DWORD WINAPI LoopbackCaptureThreadFunction(LPVOID pContext) { + LoopbackCaptureThreadFunctionArguments *pArgs = + (LoopbackCaptureThreadFunctionArguments*)pContext; + + pArgs->hr = CoInitialize(NULL); + if (FAILED(pArgs->hr)) { + ERR(L"CoInitialize failed: hr = 0x%08x", pArgs->hr); + return 0; + } + CoUninitializeOnExit cuoe; + + pArgs->hr = LoopbackCapture( + pArgs->pMMInDevice, + pArgs->pMMOutDevice, + pArgs->iBufferMs, + pArgs->bSwapChannels, + pArgs->hStartedEvent, + pArgs->hStopEvent, + &pArgs->nFrames + ); + + return 0; +} + +void swapMemcpy(void *_dst, void *_src, size_t size, size_t chunkSize) { + size_t blockSize = chunkSize * 2; + + if (size % blockSize != 0) { + ERR("bad swapMemcpy size (size %zu, chunkSize %zu)", size, chunkSize); + return; + } + + BYTE* dst = (BYTE*)_dst; + BYTE* src = (BYTE*)_src; + + for (size_t i = 0; i < size / blockSize; i++) { + memcpy(dst + i * blockSize, src + i * blockSize + chunkSize, chunkSize); + memcpy(dst + i * blockSize + chunkSize, src + i * blockSize, chunkSize); + } +} + +HRESULT LoopbackCapture( + IMMDevice *pMMInDevice, + IMMDevice *pMMOutDevice, + int iBufferMs, + bool bSwapChannels, + HANDLE hStartedEvent, + HANDLE hStopEvent, + PUINT32 pnFrames +) { + HRESULT hr; + + // activate an IAudioClient + IAudioClient *pAudioClient; + hr = pMMInDevice->Activate( + __uuidof(IAudioClient), + CLSCTX_ALL, NULL, + (void**)&pAudioClient + ); + if (FAILED(hr)) { + ERR(L"IMMDevice::Activate(IAudioClient) failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseAudioClient(pAudioClient); + + // get the default device periodicity + REFERENCE_TIME hnsDefaultDevicePeriod; + hr = pAudioClient->GetDevicePeriod(&hnsDefaultDevicePeriod, NULL); + if (FAILED(hr)) { + ERR(L"IAudioClient::GetDevicePeriod failed: hr = 0x%08x", hr); + return hr; + } + + // get the default device format + WAVEFORMATEX *pwfx; + hr = pAudioClient->GetMixFormat(&pwfx); + if (FAILED(hr)) { + ERR(L"IAudioClient::GetMixFormat failed: hr = 0x%08x", hr); + return hr; + } + CoTaskMemFreeOnExit freeMixFormat(pwfx); + + if (pwfx->nChannels != 1) { + ERR(L"device doesn't have 1 channel, has %d", pwfx->nChannels); + return hr; + } + + pwfx->nBlockAlign = pwfx->nChannels * pwfx->wBitsPerSample / 8; + + // create a periodic waitable timer + HANDLE hWakeUp = CreateWaitableTimer(NULL, FALSE, NULL); + if (NULL == hWakeUp) { + DWORD dwErr = GetLastError(); + ERR(L"CreateWaitableTimer failed: last error = %u", dwErr); + return HRESULT_FROM_WIN32(dwErr); + } + CloseHandleOnExit closeWakeUp(hWakeUp); + + UINT32 nBlockAlign = pwfx->nBlockAlign; + *pnFrames = 0; + + // call IAudioClient::Initialize + // note that AUDCLNT_STREAMFLAGS_LOOPBACK and AUDCLNT_STREAMFLAGS_EVENTCALLBACK + // do not work together... + // the "data ready" event never gets set + // so we're going to do a timer-driven loop + hr = pAudioClient->Initialize( + AUDCLNT_SHAREMODE_SHARED, + 0, + 0, 0, pwfx, 0 + ); + if (FAILED(hr)) { + ERR(L"IAudioClient::Initialize failed: hr = 0x%08x", hr); + return hr; + } + + // activate an IAudioCaptureClient + IAudioCaptureClient *pAudioCaptureClient; + hr = pAudioClient->GetService( + __uuidof(IAudioCaptureClient), + (void**)&pAudioCaptureClient + ); + if (FAILED(hr)) { + ERR(L"IAudioClient::GetService(IAudioCaptureClient) failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseAudioCaptureClient(pAudioCaptureClient); + + // register with MMCSS + DWORD nTaskIndex = 0; + HANDLE hTask = AvSetMmThreadCharacteristics(L"Audio", &nTaskIndex); + if (NULL == hTask) { + DWORD dwErr = GetLastError(); + ERR(L"AvSetMmThreadCharacteristics failed: last error = %u", dwErr); + return HRESULT_FROM_WIN32(dwErr); + } + AvRevertMmThreadCharacteristicsOnExit unregisterMmcss(hTask); + + // set the waitable timer + LARGE_INTEGER liFirstFire; + liFirstFire.QuadPart = -hnsDefaultDevicePeriod / 2; // negative means relative time + LONG lTimeBetweenFires = (LONG)hnsDefaultDevicePeriod / 2 / (10 * 1000); // convert to milliseconds + BOOL bOK = SetWaitableTimer( + hWakeUp, + &liFirstFire, + lTimeBetweenFires, + NULL, NULL, FALSE + ); + if (!bOK) { + DWORD dwErr = GetLastError(); + ERR(L"SetWaitableTimer failed: last error = %u", dwErr); + return HRESULT_FROM_WIN32(dwErr); + } + CancelWaitableTimerOnExit cancelWakeUp(hWakeUp); + + // call IAudioClient::Start + hr = pAudioClient->Start(); + if (FAILED(hr)) { + ERR(L"IAudioClient::Start failed: hr = 0x%08x", hr); + return hr; + } + AudioClientStopOnExit stopAudioClient(pAudioClient); + + // update format for stereo conversion + pwfx->nChannels *= 2; + pwfx->nSamplesPerSec /= 2; + pwfx->nBlockAlign *= 2; + + // set up output device + IAudioClient *pAudioOutClient; + hr = pMMOutDevice->Activate( + __uuidof(IAudioClient), CLSCTX_ALL, + NULL, (void**)&pAudioOutClient); + if (FAILED(hr)) { + ERR(L"IMMDevice::Activate(IAudioClient) failed (output): hr = 0x%08x", hr); + return hr; + } + + hr = pAudioOutClient->Initialize( + AUDCLNT_SHAREMODE_SHARED, + 0, + iBufferMs * 10000, + iBufferMs * 10000, + pwfx, + NULL); + if (FAILED(hr)) { + ERR(L"IAudioClient::Initialize failed (output): hr = 0x%08x", hr); + return hr; + } + + IAudioRenderClient *pRenderClient; + hr = pAudioOutClient->GetService( + __uuidof(IAudioRenderClient), + (void**)&pRenderClient); + + // Get the actual size of the allocated buffer. + UINT32 clientBufferFrameCount; + hr = pAudioOutClient->GetBufferSize(&clientBufferFrameCount); + if (FAILED(hr)) { + ERR(L"IAudioClient::GetBufferSize failed (output): hr = 0x%08x", hr); + return hr; + } + + // Grab the entire buffer for the initial fill operation. + BYTE *tmp; + hr = pRenderClient->GetBuffer(clientBufferFrameCount / 2, &tmp); + if (FAILED(hr)) { + ERR(L"IAudioClient::GetBuffer failed (output): hr = 0x%08x", hr); + return hr; + } + + hr = pRenderClient->ReleaseBuffer(clientBufferFrameCount / 2, AUDCLNT_BUFFERFLAGS_SILENT); + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::ReleaseBuffer failed (output): hr = 0x%08x", hr); + return hr; + } + + hr = pAudioOutClient->Start(); + if (FAILED(hr)) { + ERR(L"IAudioClient::Start failed (output): hr = 0x%08x", hr); + return hr; + } + + SetEvent(hStartedEvent); + + // loopback capture loop + HANDLE waitArray[2] = { hStopEvent, hWakeUp }; + DWORD dwWaitResult; + + bool bDone = false; + bool bFirstPacket = true; + for (UINT32 nPasses = 0; !bDone; nPasses++) { + // drain data while it is available + UINT32 nNextPacketSize; + for ( + hr = pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize); + SUCCEEDED(hr) && nNextPacketSize > 0; + hr = pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize) + ) { + // get the captured data + BYTE *pData; + BYTE *pOutData; + UINT32 nNumFramesToRead; + DWORD dwFlags; + + hr = pAudioCaptureClient->GetBuffer( + &pData, + &nNumFramesToRead, + &dwFlags, + NULL, + NULL + ); + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::GetBuffer failed on pass %u after %u frames: hr = 0x%08x", nPasses, *pnFrames, hr); + return hr; + } + + if (AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY == dwFlags) { + LOG(L"Probably spurious glitch reported on pass %u after %u frames", nPasses, *pnFrames); + } + else if (0 != dwFlags) { + LOG(L"IAudioCaptureClient::GetBuffer set flags to 0x%08x on pass %u after %u frames", dwFlags, nPasses, *pnFrames); + return E_UNEXPECTED; + } + + nNumFramesToRead &= ~1; + + if (0 == nNumFramesToRead) { + ERR(L"IAudioCaptureClient::GetBuffer said to read 0 frames on pass %u after %u frames", nPasses, *pnFrames); + return E_UNEXPECTED; + } + + LONG lBytesToWrite = nNumFramesToRead * nBlockAlign; + + for (;;) { + hr = pRenderClient->GetBuffer(nNumFramesToRead / 2, &pOutData); + if (hr == AUDCLNT_E_BUFFER_TOO_LARGE) { + ERR(L"%s", L"buffer overflow!"); + Sleep(1); + continue; + } + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::GetBuffer failed (output) on pass %u after %u frames: hr = 0x%08x", nPasses, *pnFrames, hr); + return hr; + } + break; + } + + if (bSwapChannels) { + swapMemcpy(pOutData, pData, lBytesToWrite, nBlockAlign); + } + else { + memcpy(pOutData, pData, lBytesToWrite); + } + + hr = pRenderClient->ReleaseBuffer(nNumFramesToRead / 2, 0); + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::ReleaseBuffer failed (output) on pass %u after %u frames: hr = 0x%08x", nPasses, *pnFrames, hr); + return hr; + } + + hr = pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead); + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::ReleaseBuffer failed on pass %u after %u frames: hr = 0x%08x", nPasses, *pnFrames, hr); + return hr; + } + + *pnFrames += nNumFramesToRead; + + bFirstPacket = false; + } + + if (FAILED(hr)) { + ERR(L"IAudioCaptureClient::GetNextPacketSize failed on pass %u after %u frames: hr = 0x%08x", nPasses, *pnFrames, hr); + return hr; + } + + dwWaitResult = WaitForMultipleObjects( + ARRAYSIZE(waitArray), waitArray, + FALSE, INFINITE + ); + + if (WAIT_OBJECT_0 == dwWaitResult) { + LOG(L"Received stop event after %u passes and %u frames", nPasses, *pnFrames); + bDone = true; + continue; // exits loop + } + + if (WAIT_OBJECT_0 + 1 != dwWaitResult) { + ERR(L"Unexpected WaitForMultipleObjects return value %u on pass %u after %u frames", dwWaitResult, nPasses, *pnFrames); + return E_UNEXPECTED; + } + } // capture loop + + return hr; +} diff --git a/mono-to-stereo/mono-to-stereo.filters b/mono-to-stereo/mono-to-stereo.filters new file mode 100644 index 0000000..989d531 --- /dev/null +++ b/mono-to-stereo/mono-to-stereo.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/mono-to-stereo/mono-to-stereo.h b/mono-to-stereo/mono-to-stereo.h new file mode 100644 index 0000000..3c3ac64 --- /dev/null +++ b/mono-to-stereo/mono-to-stereo.h @@ -0,0 +1,21 @@ +// loopback-capture.h + +// call CreateThread on this function +// feed it the address of a LoopbackCaptureThreadFunctionArguments +// it will capture via loopback from the IMMDevice +// and dump output to the HMMIO +// until the stop event is set +// any failures will be propagated back via hr + +struct LoopbackCaptureThreadFunctionArguments { + IMMDevice *pMMInDevice; + IMMDevice *pMMOutDevice; + int iBufferMs; + bool bSwapChannels; + HANDLE hStartedEvent; + HANDLE hStopEvent; + UINT32 nFrames; + HRESULT hr; +}; + +DWORD WINAPI LoopbackCaptureThreadFunction(LPVOID pContext); diff --git a/mono-to-stereo/mono-to-stereo.vcxproj b/mono-to-stereo/mono-to-stereo.vcxproj new file mode 100644 index 0000000..4acfb40 --- /dev/null +++ b/mono-to-stereo/mono-to-stereo.vcxproj @@ -0,0 +1,153 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {4463F7EB-16DC-4C5E-A9CB-9B4E5A18E2E9} + mono-to-stereo + 10.0 + mono-to-stereo + + + + Application + true + v142 + MultiByte + + + Application + true + v142 + MultiByte + + + Application + false + v142 + true + MultiByte + + + Application + false + v142 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + Level4 + Disabled + true + true + UNICODE;_UNICODE;%(PreprocessorDefinitions) + + + true + Console + avrt.lib;ole32.lib;winmm.lib + + + + + Level4 + Disabled + true + true + UNICODE;_UNICODE;%(PreprocessorDefinitions) + + + true + Console + avrt.lib;ole32.lib;winmm.lib + + + + + Level4 + MaxSpeed + true + true + true + true + UNICODE;_UNICODE;%(PreprocessorDefinitions) + MultiThreaded + + + true + true + true + Console + avrt.lib;ole32.lib;winmm.lib + + + + + Level4 + MaxSpeed + true + true + true + true + UNICODE;_UNICODE;%(PreprocessorDefinitions) + MultiThreaded + + + true + true + true + Console + avrt.lib;ole32.lib;winmm.lib + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mono-to-stereo/prefs.cpp b/mono-to-stereo/prefs.cpp new file mode 100644 index 0000000..9dbf2c0 --- /dev/null +++ b/mono-to-stereo/prefs.cpp @@ -0,0 +1,373 @@ +// prefs.cpp + +#include "common.h" + +#define DEFAULT_BUFFER_MS 64 + +void usage(LPCWSTR exe); +HRESULT get_default_device(IMMDevice **ppMMDevice); +HRESULT list_devices(); +HRESULT list_devices_with_direction(EDataFlow direction, const wchar_t *direction_label); +HRESULT get_specific_device(LPCWSTR szLongName, EDataFlow direction, IMMDevice **ppMMDevice); + +void usage(LPCWSTR exe) { + LOG( + L"%ls -?\n" + L"%ls --list-devices\n" + L"%ls [--in-device \"Device long name\"] [--out-device \"Device long name\"] [--buffer-size 128] [--no-swap-channels]\n" + L"\n" + L" -? prints this message.\n" + L" --list-devices displays the long names of all active capture and render devices.\n" + L" --in-device captures from the specified device to capture (\"Digital Audio Interface (USB Digital Audio)\" if omitted)\n" + L" --out-device device to stream stereo audio to (default if omitted)\n" + L" --buffer-size set the size of the audio buffer, in milliseconds (default to %dms)\n" + L" --no-swap-channels do not swap the L/R channels", + exe, exe, exe, DEFAULT_BUFFER_MS + ); +} + +CPrefs::CPrefs(int argc, LPCWSTR argv[], HRESULT &hr) + : m_pMMInDevice(NULL) + , m_pMMOutDevice(NULL) + , m_iBufferMs(DEFAULT_BUFFER_MS) + , m_bSwapChannels(true) +{ + switch (argc) { + case 2: + if (0 == _wcsicmp(argv[1], L"-?") || 0 == _wcsicmp(argv[1], L"/?")) { + // print usage but don't actually capture + hr = S_FALSE; + usage(argv[0]); + return; + } + else if (0 == _wcsicmp(argv[1], L"--list-devices")) { + // list the devices but don't actually capture + hr = list_devices(); + + // don't actually play + if (S_OK == hr) { + hr = S_FALSE; + return; + } + } + // intentional fallthrough + + default: + // loop through arguments and parse them + for (int i = 1; i < argc; i++) { + + // --in-device + if (0 == _wcsicmp(argv[i], L"--in-device")) { + if (NULL != m_pMMInDevice) { + ERR(L"%s", L"Only one --device switch is allowed"); + hr = E_INVALIDARG; + return; + } + + if (i++ == argc) { + ERR(L"%s", L"--device switch requires an argument"); + hr = E_INVALIDARG; + return; + } + + hr = get_specific_device(argv[i], eCapture, &m_pMMInDevice); + if (FAILED(hr)) { + return; + } + + continue; + } + + // --out-device + if (0 == _wcsicmp(argv[i], L"--out-device")) { + if (NULL != m_pMMOutDevice) { + ERR(L"%s", L"Only one --device switch is allowed"); + hr = E_INVALIDARG; + return; + } + + if (i++ == argc) { + ERR(L"%s", L"--device switch requires an argument"); + hr = E_INVALIDARG; + return; + } + + hr = get_specific_device(argv[i], eRender, &m_pMMOutDevice); + if (FAILED(hr)) { + return; + } + + continue; + } + + // --buffer-size + if (0 == _wcsicmp(argv[i], L"--buffer-size")) { + if (i++ == argc) { + ERR(L"%s", L"--buffer-size switch requires an argument"); + hr = E_INVALIDARG; + return; + } + + m_iBufferMs = _wtoi(argv[i]); + if (m_iBufferMs <= 0) { + ERR(L"%s", L"invalid buffer size given"); + hr = E_INVALIDARG; + return; + } + + continue; + } + + // --no-swap-channels + if (0 == _wcsicmp(argv[i], L"--no-swap-channels")) { + m_bSwapChannels = false; + continue; + } + + ERR(L"Invalid argument %ls", argv[i]); + hr = E_INVALIDARG; + return; + } + + // open default device if not specified + if (NULL == m_pMMInDevice) { + hr = get_specific_device(L"Digital Audio Interface (USB Digital Audio)", eCapture, &m_pMMInDevice); + if (FAILED(hr)) { + return; + } + } + + // open default device if not specified + if (NULL == m_pMMOutDevice) { + hr = get_default_device(&m_pMMOutDevice); + if (FAILED(hr)) { + return; + } + } + } +} + +CPrefs::~CPrefs() { + if (NULL != m_pMMInDevice) { + m_pMMInDevice->Release(); + } + + if (NULL != m_pMMOutDevice) { + m_pMMOutDevice->Release(); + } +} + +HRESULT get_default_device(IMMDevice **ppMMDevice) { + HRESULT hr = S_OK; + IMMDeviceEnumerator *pMMDeviceEnumerator; + + // activate a device enumerator + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + (void**)&pMMDeviceEnumerator + ); + if (FAILED(hr)) { + ERR(L"CoCreateInstance(IMMDeviceEnumerator) failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDeviceEnumerator(pMMDeviceEnumerator); + + // get the default render endpoint + hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, ppMMDevice); + if (FAILED(hr)) { + ERR(L"IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: hr = 0x%08x", hr); + return hr; + } + + return S_OK; +} + +HRESULT list_devices() { + HRESULT hr; + + hr = list_devices_with_direction(eRender, L"render"); + if (FAILED(hr)) { + return hr; + } + + LOG(""); + + hr = list_devices_with_direction(eCapture, L"capture"); + if (FAILED(hr)) { + return hr; + } + + return hr; +} + +HRESULT list_devices_with_direction(EDataFlow direction, const wchar_t *direction_label) { + HRESULT hr = S_OK; + + // get an enumerator + IMMDeviceEnumerator *pMMDeviceEnumerator; + + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + (void**)&pMMDeviceEnumerator + ); + if (FAILED(hr)) { + ERR(L"CoCreateInstance(IMMDeviceEnumerator) failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDeviceEnumerator(pMMDeviceEnumerator); + + IMMDeviceCollection *pMMDeviceCollection; + + // get all the active render endpoints + hr = pMMDeviceEnumerator->EnumAudioEndpoints( + direction, DEVICE_STATE_ACTIVE, &pMMDeviceCollection + ); + if (FAILED(hr)) { + ERR(L"IMMDeviceEnumerator::EnumAudioEndpoints failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDeviceCollection(pMMDeviceCollection); + + UINT count; + hr = pMMDeviceCollection->GetCount(&count); + if (FAILED(hr)) { + ERR(L"IMMDeviceCollection::GetCount failed: hr = 0x%08x", hr); + return hr; + } + LOG(L"Active %s endpoints found: %u", direction_label, count); + + for (UINT i = 0; i < count; i++) { + IMMDevice *pMMDevice; + + // get the "n"th device + hr = pMMDeviceCollection->Item(i, &pMMDevice); + if (FAILED(hr)) { + ERR(L"IMMDeviceCollection::Item failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDevice(pMMDevice); + + // open the property store on that device + IPropertyStore *pPropertyStore; + hr = pMMDevice->OpenPropertyStore(STGM_READ, &pPropertyStore); + if (FAILED(hr)) { + ERR(L"IMMDevice::OpenPropertyStore failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releasePropertyStore(pPropertyStore); + + // get the long name property + PROPVARIANT pv; PropVariantInit(&pv); + hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &pv); + if (FAILED(hr)) { + ERR(L"IPropertyStore::GetValue failed: hr = 0x%08x", hr); + return hr; + } + PropVariantClearOnExit clearPv(&pv); + + if (VT_LPWSTR != pv.vt) { + ERR(L"PKEY_Device_FriendlyName variant type is %u - expected VT_LPWSTR", pv.vt); + return E_UNEXPECTED; + } + + LOG(L" %ls", pv.pwszVal); + } + + return S_OK; +} + +HRESULT get_specific_device(LPCWSTR szLongName, EDataFlow direction, IMMDevice **ppMMDevice) { + HRESULT hr = S_OK; + + *ppMMDevice = NULL; + + // get an enumerator + IMMDeviceEnumerator *pMMDeviceEnumerator; + + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + (void**)&pMMDeviceEnumerator + ); + if (FAILED(hr)) { + ERR(L"CoCreateInstance(IMMDeviceEnumerator) failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDeviceEnumerator(pMMDeviceEnumerator); + + IMMDeviceCollection *pMMDeviceCollection; + + // get all the active render endpoints + hr = pMMDeviceEnumerator->EnumAudioEndpoints( + direction, DEVICE_STATE_ACTIVE, &pMMDeviceCollection + ); + if (FAILED(hr)) { + ERR(L"IMMDeviceEnumerator::EnumAudioEndpoints failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDeviceCollection(pMMDeviceCollection); + + UINT count; + hr = pMMDeviceCollection->GetCount(&count); + if (FAILED(hr)) { + ERR(L"IMMDeviceCollection::GetCount failed: hr = 0x%08x", hr); + return hr; + } + + for (UINT i = 0; i < count; i++) { + IMMDevice *pMMDevice; + + // get the "n"th device + hr = pMMDeviceCollection->Item(i, &pMMDevice); + if (FAILED(hr)) { + ERR(L"IMMDeviceCollection::Item failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releaseMMDevice(pMMDevice); + + // open the property store on that device + IPropertyStore *pPropertyStore; + hr = pMMDevice->OpenPropertyStore(STGM_READ, &pPropertyStore); + if (FAILED(hr)) { + ERR(L"IMMDevice::OpenPropertyStore failed: hr = 0x%08x", hr); + return hr; + } + ReleaseOnExit releasePropertyStore(pPropertyStore); + + // get the long name property + PROPVARIANT pv; PropVariantInit(&pv); + hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &pv); + if (FAILED(hr)) { + ERR(L"IPropertyStore::GetValue failed: hr = 0x%08x", hr); + return hr; + } + PropVariantClearOnExit clearPv(&pv); + + if (VT_LPWSTR != pv.vt) { + ERR(L"PKEY_Device_FriendlyName variant type is %u - expected VT_LPWSTR", pv.vt); + return E_UNEXPECTED; + } + + // is it a match? + if (0 == _wcsicmp(pv.pwszVal, szLongName)) { + // did we already find it? + if (NULL == *ppMMDevice) { + *ppMMDevice = pMMDevice; + pMMDevice->AddRef(); + } + else { + ERR(L"Found (at least) two devices named %ls", szLongName); + return E_UNEXPECTED; + } + } + } + + if (NULL == *ppMMDevice) { + ERR(L"Could not find a device named %ls", szLongName); + return HRESULT_FROM_WIN32(ERROR_NOT_FOUND); + } + + return S_OK; +} diff --git a/mono-to-stereo/prefs.h b/mono-to-stereo/prefs.h new file mode 100644 index 0000000..70035e5 --- /dev/null +++ b/mono-to-stereo/prefs.h @@ -0,0 +1,14 @@ +// prefs.h + +class CPrefs { +public: + IMMDevice *m_pMMInDevice; + IMMDevice *m_pMMOutDevice; + int m_iBufferMs; + bool m_bSwapChannels; + + // set hr to S_FALSE to abort but return success + CPrefs(int argc, LPCWSTR argv[], HRESULT &hr); + ~CPrefs(); + +};