mirror of
https://github.com/quantum5/winscap.git
synced 2025-04-24 21:52:03 -04:00
Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ab3805d9c9 | ||
|
6de12d3677 | ||
|
837d713edf | ||
|
df1299f5de | ||
|
af5047a0b7 | ||
|
e64db21dc9 | ||
|
46b021bfe9 | ||
|
a1f735686f |
12
.github/workflows/lint.yml
vendored
Normal file
12
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
name: lint
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: DoozyX/clang-format-lint-action@v0.5
|
||||
with:
|
||||
source: '.'
|
||||
extensions: 'h,c,cpp'
|
||||
clangFormatVersion: 9
|
13
README.md
13
README.md
|
@ -14,6 +14,12 @@ Note that `winscap` uses your audio device in shared mode, so your capture setti
|
|||
must match the Windows output device. By default, this is 16-bit stereo at 48000 Hz.
|
||||
If your capture settings do not match, `winscap` will fail to start.
|
||||
|
||||
## Binaries
|
||||
|
||||
The latest stable binaries are available on [GitHub releases][3] ([winscap.exe][4]).
|
||||
|
||||
Latest bleeding edge binaries are available as artifacts on [GitHub Actions][2].
|
||||
|
||||
## Building
|
||||
|
||||
You need a new enough Visual C++ toolchain. To build, run
|
||||
|
@ -22,7 +28,7 @@ You need a new enough Visual C++ toolchain. To build, run
|
|||
|
||||
## Background
|
||||
|
||||
I created this tool as a lightweight approach to run [Cava][3] on Windows Subsystem
|
||||
I created this tool as a lightweight approach to run [Cava][5] on Windows Subsystem
|
||||
for Linux (WSL) while using sound output from Windows.
|
||||
|
||||
To use with Cava, configure Cava to read from a named pipe inside WSL (we'll use
|
||||
|
@ -40,6 +46,7 @@ Replace `48000` with whatever sampling rate you use with `winscap`.
|
|||
Then, run `winscap` as follows:
|
||||
|
||||
```sh
|
||||
$ mkfifo /tmp/cava.fifo
|
||||
$ /mnt/c/path/to/winscap.exe 2 48000 16 > /tmp/cava.fifo
|
||||
```
|
||||
|
||||
|
@ -47,4 +54,6 @@ Again, replace the arguments as appropriate.
|
|||
|
||||
[1]: https://github.com/quantum5/winscap/workflows/build/badge.svg
|
||||
[2]: https://github.com/quantum5/winscap/actions
|
||||
[3]: https://github.com/karlstav/cava
|
||||
[3]: https://github.com/quantum5/winscap/releases
|
||||
[4]: https://github.com/quantum5/winscap/releases/latest/download/winscap.exe
|
||||
[5]: https://github.com/karlstav/cava
|
||||
|
|
172
winscap.cpp
172
winscap.cpp
|
@ -11,6 +11,8 @@
|
|||
#include <stdlib.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <functiondiscoverykeys_devpkey.h>
|
||||
|
||||
#define REFTIMES_PER_SEC 10000000
|
||||
#define REFTIMES_PER_MILLISEC 10000
|
||||
|
||||
|
@ -31,6 +33,48 @@ _COM_SMARTPTR_TYPEDEF(IMMDeviceEnumerator, __uuidof(IMMDeviceEnumerator));
|
|||
_COM_SMARTPTR_TYPEDEF(IMMDevice, __uuidof(IMMDevice));
|
||||
_COM_SMARTPTR_TYPEDEF(IAudioClient, __uuidof(IAudioClient));
|
||||
_COM_SMARTPTR_TYPEDEF(IAudioCaptureClient, __uuidof(IAudioCaptureClient));
|
||||
_COM_SMARTPTR_TYPEDEF(IPropertyStore, __uuidof(IPropertyStore));
|
||||
|
||||
class DeviceChangeNotification : public IMMNotificationClient {
|
||||
volatile ULONG ref;
|
||||
volatile bool &changed;
|
||||
HANDLE hEvent;
|
||||
|
||||
public:
|
||||
DeviceChangeNotification(volatile bool &changed, HANDLE hEvent)
|
||||
: changed(changed), hEvent(hEvent) {}
|
||||
|
||||
// This is meant to be allocated on stack, so we don't actually free.
|
||||
STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref); }
|
||||
STDMETHODIMP_(ULONG) Release() { return InterlockedDecrement(&ref); }
|
||||
|
||||
STDMETHODIMP QueryInterface(REFIID iid, void **ppv) {
|
||||
if (iid == IID_IUnknown) {
|
||||
AddRef();
|
||||
*ppv = (IUnknown *)this;
|
||||
} else if (iid == __uuidof(IMMNotificationClient)) {
|
||||
AddRef();
|
||||
*ppv = (IMMNotificationClient *)this;
|
||||
} else {
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR) {
|
||||
if (flow == eRender && role == eConsole) {
|
||||
changed = true;
|
||||
SetEvent(hEvent);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP OnDeviceAdded(LPCWSTR) { return S_OK; }
|
||||
STDMETHODIMP OnDeviceRemoved(LPCWSTR) { return S_OK; }
|
||||
STDMETHODIMP OnDeviceStateChanged(LPCWSTR, DWORD) { return S_OK; }
|
||||
STDMETHODIMP OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) { return S_OK; }
|
||||
};
|
||||
|
||||
class EnsureCaptureStop {
|
||||
IAudioClientPtr m_client;
|
||||
|
@ -55,19 +99,20 @@ struct {
|
|||
{AUDCLNT_E_UNSUPPORTED_FORMAT, L"Requested sound format unsupported"},
|
||||
};
|
||||
|
||||
LPCWSTR error_message(const _com_error &err) {
|
||||
for (int i = 0; i < sizeof error_table / sizeof error_table[0]; ++i) {
|
||||
if (error_table[i].hr == err.Error()) {
|
||||
return error_table[i].error;
|
||||
}
|
||||
}
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
#define ensure(hr) ensure_(__FILE__, __LINE__, hr)
|
||||
void ensure_(const char *file, int line, HRESULT hr) {
|
||||
if (FAILED(hr)) {
|
||||
_com_error err(hr);
|
||||
LPCWSTR msg = err.ErrorMessage();
|
||||
|
||||
for (int i = 0; i < sizeof error_table / sizeof error_table[0]; ++i) {
|
||||
if (error_table[i].hr == hr) {
|
||||
msg = error_table[i].error;
|
||||
}
|
||||
}
|
||||
|
||||
fwprintf(stderr, L"Error at %S:%d (0x%08x): %s\n", file, line, hr, msg);
|
||||
fwprintf(stderr, L"Error at %S:%d (0x%08x): %s\n", file, line, hr, error_message(err));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -99,15 +144,6 @@ int main(int argc, char *argv[]) {
|
|||
CCoInitialize comInit;
|
||||
ensure(comInit);
|
||||
|
||||
IMMDeviceEnumeratorPtr pEnumerator;
|
||||
ensure(pEnumerator.CreateInstance(__uuidof(MMDeviceEnumerator)));
|
||||
|
||||
IMMDevicePtr pDevice;
|
||||
ensure(pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice));
|
||||
|
||||
IAudioClientPtr pClient;
|
||||
ensure(pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **)&pClient));
|
||||
|
||||
WAVEFORMATEX wfx;
|
||||
wfx.wFormatTag = WAVE_FORMAT_PCM;
|
||||
wfx.nChannels = (WORD)channels;
|
||||
|
@ -116,44 +152,84 @@ int main(int argc, char *argv[]) {
|
|||
wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8;
|
||||
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
|
||||
|
||||
ensure(pClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK,
|
||||
16 * REFTIMES_PER_MILLISEC, 0, &wfx, nullptr));
|
||||
IMMDeviceEnumeratorPtr pEnumerator;
|
||||
ensure(pEnumerator.CreateInstance(__uuidof(MMDeviceEnumerator)));
|
||||
|
||||
UINT32 bufferFrameCount;
|
||||
ensure(pClient->GetBufferSize(&bufferFrameCount));
|
||||
|
||||
IAudioCaptureClientPtr pCapture;
|
||||
ensure(pClient->GetService(__uuidof(IAudioCaptureClient), (void **)&pCapture));
|
||||
|
||||
DWORD dwDelay = (DWORD)(((double)REFTIMES_PER_SEC * bufferFrameCount / wfx.nSamplesPerSec) /
|
||||
REFTIMES_PER_MILLISEC / 2);
|
||||
|
||||
LPBYTE pSilence = (LPBYTE)malloc(bufferFrameCount * wfx.nBlockAlign);
|
||||
EnsureFree freeSilence(pSilence);
|
||||
ZeroMemory(pSilence, bufferFrameCount * wfx.nBlockAlign);
|
||||
|
||||
ensure(pClient->Start());
|
||||
EnsureCaptureStop autoStop(pClient);
|
||||
volatile bool deviceChanged = false;
|
||||
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
DeviceChangeNotification deviceChangeNotification(deviceChanged, hEvent);
|
||||
pEnumerator->RegisterEndpointNotificationCallback(&deviceChangeNotification);
|
||||
|
||||
for (;;) {
|
||||
Sleep(dwDelay);
|
||||
ResetEvent(hEvent);
|
||||
|
||||
UINT32 packetLength;
|
||||
ensure(pCapture->GetNextPacketSize(&packetLength));
|
||||
IMMDevicePtr pDevice;
|
||||
ensure(pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice));
|
||||
|
||||
while (packetLength) {
|
||||
LPBYTE pData;
|
||||
UINT32 numFramesAvailable;
|
||||
DWORD flags;
|
||||
IAudioClientPtr pClient;
|
||||
ensure(pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **)&pClient));
|
||||
|
||||
ensure(pCapture->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr));
|
||||
HRESULT hrInit = pClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK,
|
||||
16 * REFTIMES_PER_MILLISEC, 0, &wfx, nullptr);
|
||||
if (FAILED(hrInit)) {
|
||||
_com_error err(hrInit);
|
||||
|
||||
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||
pData = pSilence;
|
||||
IPropertyStorePtr pProps;
|
||||
ensure(pDevice->OpenPropertyStore(STGM_READ, &pProps));
|
||||
|
||||
fwrite(pData, wfx.nBlockAlign, numFramesAvailable, stdout);
|
||||
ensure(pCapture->ReleaseBuffer(numFramesAvailable));
|
||||
ensure(pCapture->GetNextPacketSize(&packetLength));
|
||||
PROPVARIANT varName;
|
||||
PropVariantInit(&varName);
|
||||
ensure(pProps->GetValue(PKEY_Device_FriendlyName, &varName));
|
||||
fwprintf(stderr, L"Failed to open: %s: %s\n", varName.pwszVal, error_message(err));
|
||||
PropVariantClear(&varName);
|
||||
WaitForSingleObject(hEvent, INFINITE);
|
||||
continue;
|
||||
}
|
||||
|
||||
UINT32 bufferFrameCount;
|
||||
ensure(pClient->GetBufferSize(&bufferFrameCount));
|
||||
|
||||
IAudioCaptureClientPtr pCapture;
|
||||
ensure(pClient->GetService(__uuidof(IAudioCaptureClient), (void **)&pCapture));
|
||||
|
||||
DWORD dwDelay = (DWORD)(((double)REFTIMES_PER_SEC * bufferFrameCount / wfx.nSamplesPerSec) /
|
||||
REFTIMES_PER_MILLISEC / 2);
|
||||
|
||||
LPBYTE pSilence = (LPBYTE)malloc(bufferFrameCount * wfx.nBlockAlign);
|
||||
EnsureFree freeSilence(pSilence);
|
||||
ZeroMemory(pSilence, bufferFrameCount * wfx.nBlockAlign);
|
||||
|
||||
ensure(pClient->Start());
|
||||
EnsureCaptureStop autoStop(pClient);
|
||||
|
||||
while (!deviceChanged) {
|
||||
Sleep(dwDelay);
|
||||
|
||||
UINT32 packetLength;
|
||||
HRESULT hrNext = pCapture->GetNextPacketSize(&packetLength);
|
||||
if (hrNext == AUDCLNT_E_DEVICE_INVALIDATED) {
|
||||
while (!deviceChanged)
|
||||
;
|
||||
break;
|
||||
} else {
|
||||
ensure(hrNext);
|
||||
}
|
||||
|
||||
while (packetLength) {
|
||||
LPBYTE pData;
|
||||
UINT32 numFramesAvailable;
|
||||
DWORD flags;
|
||||
|
||||
ensure(pCapture->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr));
|
||||
|
||||
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||
pData = pSilence;
|
||||
|
||||
_write(_fileno(stdout), pData, wfx.nBlockAlign * numFramesAvailable);
|
||||
ensure(pCapture->ReleaseBuffer(numFramesAvailable));
|
||||
ensure(pCapture->GetNextPacketSize(&packetLength));
|
||||
}
|
||||
}
|
||||
deviceChanged = false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue