Skip to content

Instantly share code, notes, and snippets.

@Liastre
Last active June 26, 2017 15:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Liastre/ff201f37bc62f6dc0b7f5541923565ab to your computer and use it in GitHub Desktop.
Save Liastre/ff201f37bc62f6dc0b7f5541923565ab to your computer and use it in GitHub Desktop.
WASAPI capture shared event based
#pragma once
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
# define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
#endif
#define _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES 1
#include <new>
#include <stdexcept>
#include <windows.h>
#include <strsafe.h>
#include <objbase.h>
#pragma warning(push)
#pragma warning(disable : 4201)
#include <mmdeviceapi.h>
#include <audiopolicy.h>
#pragma warning(pop)
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
#include "DeviceManager.h"
#include <functiondiscoverykeys.h>
LPWSTR GetDeviceName(IMMDeviceCollection *DeviceCollection, UINT DeviceIndex)
{
IMMDevice *device;
LPWSTR deviceId;
HRESULT hr;
hr = DeviceCollection->Item(DeviceIndex, &device);
if (hr != S_OK) throw std::runtime_error("");
hr = device->GetId(&deviceId);
if (hr != S_OK) throw std::runtime_error("");
IPropertyStore *propertyStore;
hr = device->OpenPropertyStore(STGM_READ, &propertyStore);
SafeRelease(&device);
if (hr != S_OK) throw std::runtime_error("");
PROPVARIANT friendlyName;
PropVariantInit(&friendlyName);
hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
if (hr != S_OK) throw std::runtime_error("");
SafeRelease(&propertyStore);
wchar_t deviceName[128];
hr = StringCbPrintf(deviceName, sizeof(deviceName), L"%s (%s)", friendlyName.vt != VT_LPWSTR ? L"Unknown" : friendlyName.pwszVal, deviceId);
if (hr != S_OK) throw std::runtime_error("");
PropVariantClear(&friendlyName);
CoTaskMemFree(deviceId);
wchar_t *returnValue = _wcsdup(deviceName);
if (returnValue == NULL)
{
printf("Unable to allocate buffer for return\n");
return NULL;
}
return returnValue;
}
// Based on the input switches, pick the specified device to use.
bool PickDevice(IMMDevice **DeviceToUse, ERole *DefaultDeviceRole)
{
HRESULT hr;
bool retValue = true;
bool UseConsoleDevice;
bool UseCommunicationsDevice;
bool UseMultimediaDevice;
IMMDeviceEnumerator *deviceEnumerator = NULL;
IMMDeviceCollection *deviceCollection = NULL;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator));
if (hr != S_OK) throw std::runtime_error("");
IMMDevice *device = NULL;
// The user didn't specify an output device, prompt the user for a device and use that.
hr = deviceEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &deviceCollection);
if (hr != S_OK) throw std::runtime_error("");
printf("Select an output device:\n");
printf(" 0: Default Console Device\n");
printf(" 1: Default Communications Device\n");
printf(" 2: Default Multimedia Device\n");
UINT deviceCount;
hr = deviceCollection->GetCount(&deviceCount);
if (hr != S_OK) throw std::runtime_error("");
for (UINT i = 0; i < deviceCount; i += 1)
{
LPWSTR deviceName;
deviceName = GetDeviceName(deviceCollection, i);
if (deviceName == NULL) throw std::runtime_error("");
printf(" %d: %S\n", i + 3, deviceName);
free(deviceName);
}
long deviceIndex;
scanf("%d", &deviceIndex);
switch (deviceIndex)
{
case 0:
UseConsoleDevice = 1;
break;
case 1:
UseCommunicationsDevice = 1;
break;
case 2:
UseMultimediaDevice = 1;
break;
default:
hr = deviceCollection->Item(deviceIndex - 3, &device);
if (hr != S_OK) throw std::runtime_error("");
break;
}
if (device == NULL) throw std::runtime_error("");
*DeviceToUse = device;
retValue = true;
SafeRelease(&deviceCollection);
SafeRelease(&deviceEnumerator);
return retValue;
}
#pragma once
#include "config.h"
LPWSTR GetDeviceName(IMMDeviceCollection *DeviceCollection, UINT DeviceIndex);
bool PickDevice(IMMDevice **DeviceToUse, ERole *DefaultDeviceRole);
#include "config.h"
#include <thread>
#include "WASAPICapture.h"
// A simple WASAPI Capture client.
CWASAPICapture::CWASAPICapture(IMMDevice *Endpoint, ERole EndpointRole) :
_RefCount(1),
_Endpoint(Endpoint),
_AudioClient(NULL),
_CaptureClient(NULL),
_CaptureThread(NULL),
_MixFormat(NULL),
_AudioSamplesReadyEvent(NULL),
_CurrentCaptureIndex(0),
_EndpointRole(EndpointRole),
_AudioSessionControl(NULL),
_DeviceEnumerator(NULL)
{
_Endpoint->AddRef(); // Since we're holding a copy of the endpoint, take a reference to it. It'll be released in Shutdown();
}
// Empty destructor - everything should be released in the Shutdown() call.
CWASAPICapture::~CWASAPICapture(void)
{
}
// Initialize WASAPI in event driven mode, associate the audio client with our samples ready event handle, retrieve
// a capture client for the transport, create the capture thread and start the audio engine.
bool CWASAPICapture::InitializeAudioEngine()
{
HRESULT hr = _AudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, _EngineLatencyInMS*10000, 0, _MixFormat, NULL);
if (hr != S_OK) throw std::runtime_error("Unable to initialize audio client. \n");
hr = _AudioClient->GetBufferSize(&_BufferSize);
if (hr != S_OK) throw std::runtime_error("Unable to get audio client buffer. \n");
hr = _AudioClient->SetEventHandle(_AudioSamplesReadyEvent);
if (hr != S_OK) throw std::runtime_error("Unable to set ready event. \n");
hr = _AudioClient->GetService(IID_PPV_ARGS(&_CaptureClient));
if (hr != S_OK) throw std::runtime_error("Unable to get new capture client. \n");
return true;
}
// Retrieve the format we'll use to capture samples.
// We use the Mix format since we're capturing in shared mode.
bool CWASAPICapture::LoadFormat()
{
HRESULT hr = _AudioClient->GetMixFormat(&_MixFormat);
if (hr != S_OK) throw std::runtime_error("Unable to get mix format on audio client.\n");
_FrameSize = (_MixFormat->wBitsPerSample / 8) * _MixFormat->nChannels;
return true;
}
// Initialize the capturer.
bool CWASAPICapture::Initialize(UINT32 EngineLatency)
{
_AudioSamplesReadyEvent = CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
if (_AudioSamplesReadyEvent == NULL)
{
printf("Unable to create samples ready event: %d.\n", GetLastError());
return false;
}
// Now activate an IAudioClient object on our preferred endpoint and retrieve the mix format for that endpoint.
HRESULT hr = _Endpoint->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, reinterpret_cast<void **>(&_AudioClient));
if (hr != S_OK) throw std::runtime_error("Unable to activate audio client.\n");
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&_DeviceEnumerator));
if (hr != S_OK) throw std::runtime_error("Unable to instantiate device enumerator.\n");
// Load the MixFormat. This may differ depending on the shared mode used
if (!LoadFormat()) throw std::runtime_error("Failed to load the mix format.\n");
// Remember our configured latency in case we'll need it for a stream switch later.
_EngineLatencyInMS = EngineLatency;
if (!InitializeAudioEngine()) throw std::runtime_error("");
return true;
}
// Shut down the capture code and free all the resources.
void CWASAPICapture::Shutdown()
{
if (_CaptureThread)
{
WaitForSingleObject(_CaptureThread, INFINITE);
CloseHandle(_CaptureThread);
_CaptureThread = NULL;
}
if (_AudioSamplesReadyEvent)
{
CloseHandle(_AudioSamplesReadyEvent);
_AudioSamplesReadyEvent = NULL;
}
SafeRelease(&_Endpoint);
SafeRelease(&_AudioClient);
SafeRelease(&_CaptureClient);
if (_MixFormat)
{
CoTaskMemFree(_MixFormat);
_MixFormat = NULL;
}
}
// Start capturing...
bool CWASAPICapture::Start(BYTE *CaptureBuffer, size_t CaptureBufferSize)
{
HRESULT hr;
_CaptureBuffer = CaptureBuffer;
_CaptureBufferSize = CaptureBufferSize;
// Now create the thread which is going to drive the capture.
_CaptureThread = CreateThread(NULL, 0, WASAPICaptureThread, this, 0, NULL);
if (_CaptureThread == NULL) throw std::runtime_error("Unable to create transport thread.\n");
// We're ready to go, start capturing!
hr = _AudioClient->Start();
if (hr != S_OK) throw std::runtime_error("Unable to start capture client.\n");
return true;
}
// Stop the capturer.
void CWASAPICapture::Stop()
{
HRESULT hr;
// Tell the capture thread to shut down, wait for the thread to complete then clean up all the stuff we
// allocated in Start().
hr = _AudioClient->Stop();
if (hr != S_OK) throw std::runtime_error("Unable to stop audio client.\n");
if (_CaptureThread)
{
WaitForSingleObject(_CaptureThread, INFINITE);
CloseHandle(_CaptureThread);
_CaptureThread = NULL;
}
}
// Capture thread - processes samples from the audio engine
DWORD CWASAPICapture::WASAPICaptureThread(LPVOID Context)
{
CWASAPICapture *capturer = static_cast<CWASAPICapture *>(Context);
return capturer->DoCaptureThread();
}
DWORD CWASAPICapture::DoCaptureThread()
{
BYTE *pData;
UINT32 framesAvailable = 1;
DWORD flags, state = 0;
bool stillPlaying = true;
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (hr != S_OK) throw std::runtime_error("Unable to initialize COM in render thread. \n");
// Each loop fills about half of the shared buffer.
//Sleep(_EngineLatencyInMS/2);
state = WaitForSingleObject(_AudioSamplesReadyEvent, INFINITE);
if (state != 0) throw std::runtime_error("");
while (framesAvailable != 0) {
// get the available data in the shared buffer.
hr = _CaptureClient->GetBuffer(&pData, &framesAvailable, &flags, NULL, NULL);
if (hr != S_OK) throw std::runtime_error("");
UINT32 framesToCopy = min(framesAvailable, static_cast<UINT32>((_CaptureBufferSize - _CurrentCaptureIndex) / _FrameSize));
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
ZeroMemory(&_CaptureBuffer[_CurrentCaptureIndex], framesToCopy*_FrameSize);
} else {
CopyMemory(&_CaptureBuffer[_CurrentCaptureIndex], pData, framesToCopy*_FrameSize);
}
_CurrentCaptureIndex += framesToCopy*_FrameSize;
// release data
hr = _CaptureClient->ReleaseBuffer(framesAvailable);
if (hr != S_OK) throw std::runtime_error("");
// start next capture
state = WaitForSingleObject(_AudioSamplesReadyEvent, INFINITE);
if (state != 0) throw std::runtime_error("");
hr = _CaptureClient->GetNextPacketSize(&framesAvailable);
if (hr != S_OK) throw std::runtime_error("");
}
// TODO: my break point, check _CurrentCaptureIndex
if (_CurrentCaptureIndex != _CaptureBufferSize) throw std::runtime_error("Number of read frames not equal to requested");
CoUninitialize();
return 0;
}
ULONG CWASAPICapture::AddRef()
{
return InterlockedIncrement(&_RefCount);
}
ULONG CWASAPICapture::Release()
{
ULONG returnValue = InterlockedDecrement(&_RefCount);
if (returnValue == 0)
{
delete this;
}
return returnValue;
}
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
//
#pragma once
#include <MMDeviceAPI.h>
#include <AudioClient.h>
#include <AudioPolicy.h>
// WASAPI Capture class.
class CWASAPICapture : public IAudioSessionEvents, IMMNotificationClient
{
public:
// Public interface to CWASAPICapture.
CWASAPICapture(IMMDevice *Endpoint, ERole EndpointRole);
bool Initialize(UINT32 EngineLatency);
void Shutdown();
bool Start(BYTE *CaptureBuffer, size_t BufferSize);
void Stop();
WORD ChannelCount() { return _MixFormat->nChannels; }
UINT32 SamplesPerSecond() { return _MixFormat->nSamplesPerSec; }
UINT32 BytesPerSample() { return _MixFormat->wBitsPerSample / 8; }
size_t FrameSize() { return _FrameSize; }
WAVEFORMATEX *MixFormat() { return _MixFormat; }
UINT64 BytesCaptured() { return _CurrentCaptureIndex; }
STDMETHOD_(ULONG, AddRef)();
STDMETHOD_(ULONG, Release)();
private:
~CWASAPICapture(void); // Destructor is private to prevent accidental deletion.
LONG _RefCount;
// Core Audio Capture member variables.
IMMDevice * _Endpoint;
IAudioClient * _AudioClient;
IAudioCaptureClient *_CaptureClient;
HANDLE _CaptureThread;
HANDLE _AudioSamplesReadyEvent;
WAVEFORMATEX * _MixFormat;
size_t _FrameSize;
UINT32 _BufferSize;
// Capture buffer management.
BYTE *_CaptureBuffer;
size_t _CaptureBufferSize;
UINT64 _CurrentCaptureIndex;
static DWORD __stdcall WASAPICaptureThread(LPVOID Context);
DWORD CWASAPICapture::DoCaptureThread();
// Stream switch related members and methods.
bool _EnableStreamSwitch;
ERole _EndpointRole;
IAudioSessionControl * _AudioSessionControl;
IMMDeviceEnumerator * _DeviceEnumerator;
LONG _EngineLatencyInMS;
// Overrides for pure virtual methods.
STDMETHOD(OnDisplayNameChanged) (LPCWSTR /*NewDisplayName*/, LPCGUID /*EventContext*/) { return S_OK; };
STDMETHOD(OnIconPathChanged) (LPCWSTR /*NewIconPath*/, LPCGUID /*EventContext*/) { return S_OK; };
STDMETHOD(OnSimpleVolumeChanged) (float /*NewSimpleVolume*/, BOOL /*NewMute*/, LPCGUID /*EventContext*/) { return S_OK; };
STDMETHOD(OnChannelVolumeChanged) (DWORD /*ChannelCount*/, float /*NewChannelVolumes*/[], DWORD /*ChangedChannel*/, LPCGUID /*EventContext*/) { return S_OK; };
STDMETHOD(OnGroupingParamChanged) (LPCGUID /*NewGroupingParam*/, LPCGUID /*EventContext*/) { return S_OK; };
STDMETHOD(OnStateChanged) (AudioSessionState /*NewState*/) { return S_OK; };
STDMETHOD(OnSessionDisconnected) (AudioSessionDisconnectReason DisconnectReason) { return S_OK; };
STDMETHOD(OnDeviceStateChanged) (LPCWSTR /*DeviceId*/, DWORD /*NewState*/) { return S_OK; }
STDMETHOD(OnDeviceAdded) (LPCWSTR /*DeviceId*/) { return S_OK; };
STDMETHOD(OnDeviceRemoved) (LPCWSTR /*DeviceId(*/) { return S_OK; };
STDMETHOD(OnDefaultDeviceChanged) (EDataFlow Flow, ERole Role, LPCWSTR NewDefaultDeviceId) { return S_OK; };
STDMETHOD(OnPropertyValueChanged) (LPCWSTR /*DeviceId*/, const PROPERTYKEY /*Key*/) { return S_OK; };
STDMETHOD(QueryInterface)(REFIID iid, void **pvObject) { return S_OK; };
// Utility functions.
bool InitializeAudioEngine();
bool LoadFormat();
};
#include "config.h"
#include "DeviceManager.h"
#include "WASAPICapture.h"
int main()
{
int TargetLatency = 1000;
int TargetDurationInSec = 10;
IMMDevice *device = NULL;
ERole role;
// A GUI application should use COINIT_APARTMENTTHREADED instead of COINIT_MULTITHREADED.
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (hr != S_OK) throw std::runtime_error("");
// Pick the device to capture.
if (!PickDevice(&device, &role)) throw std::runtime_error("");
printf("Capture audio data for %d seconds\n", TargetDurationInSec);
// Instantiate a capturer and capture sounds for TargetDuration seconds
// Configure the capturer to enable stream switching on the specified role if the user specified one of the default devices.
CWASAPICapture *capturer = new (std::nothrow) CWASAPICapture(device, role);
if (capturer == NULL)
{
printf("Unable to allocate capturer\n");
return -1;
}
if (capturer->Initialize(TargetLatency))
{
// We've initialized the capturer. Once we've done that, we know some information about the
// mix format and we can allocate the buffer that we're going to capture.
//
// The buffer is going to contain "TargetDuration" seconds worth of PCM data. That means
// we're going to have TargetDuration*samples/second frames multiplied by the frame size.
size_t captureBufferSize = capturer->SamplesPerSecond() * TargetDurationInSec * capturer->FrameSize();
BYTE *captureBuffer = new (std::nothrow) BYTE[captureBufferSize];
if (captureBuffer == NULL)
{
printf("Unable to allocate capture buffer\n");
return -1;
}
if (capturer->Start(captureBuffer, captureBufferSize))
{
do
{
printf(".");
Sleep(1000);
} while (--TargetDurationInSec);
printf("\n");
capturer->Stop();
// Now shut down the capturer and release it we're done.
capturer->Shutdown();
SafeRelease(&capturer);
}
delete []captureBuffer;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment