Skip to content

Add WASAPI loopback implementation #672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions cmake/portaudio.def.in
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ PaUtil_SetDebugPrintFunction @55
@DEF_EXCLUDE_WASAPI_SYMBOLS@PaWasapiWinrt_SetDefaultDeviceId @67
@DEF_EXCLUDE_WASAPI_SYMBOLS@PaWasapiWinrt_PopulateDeviceList @69
@DEF_EXCLUDE_WASAPI_SYMBOLS@PaWasapi_GetIMMDevice @70
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamInputHandleCount @71
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamInputHandle @72
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamOutputHandleCount @73
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamOutputHandle @74
@DEF_EXCLUDE_WASAPI_SYMBOLS@PaWasapi_IsLoopback @71
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamInputHandleCount @72
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamInputHandle @73
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamOutputHandleCount @74
@DEF_EXCLUDE_WMME_SYMBOLS@PaWinMME_GetStreamOutputHandle @75
16 changes: 15 additions & 1 deletion include/pa_win_wasapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ int PaWasapi_GetDeviceMixFormat( void *pFormat, unsigned int formatSize, PaDevic
int/*PaWasapiDeviceRole*/ PaWasapi_GetDeviceRole( PaDeviceIndex device );


/** Get device IMMDevice pointer
/** Get device IMMDevice pointer.

@param device Device index.
@param pAudioClient Pointer to pointer of IMMDevice.
Expand All @@ -447,6 +447,20 @@ int/*PaWasapiDeviceRole*/ PaWasapi_GetDeviceRole( PaDeviceIndex device );
PaError PaWasapi_GetIMMDevice( PaDeviceIndex device, void **pIMMDevice );


/** Get device loopback state:

0 - Not loopback,
1 - Loopback,
negative - PaErrorCode.

@param device Device index.

@return Non-negative value indicating loopback state or, a PaErrorCode (which is always negative)
if PortAudio is not initialized or an error is encountered.
*/
int PaWasapi_IsLoopback( PaDeviceIndex device );


/** Boost thread priority of calling thread (MMCSS).

Use it for Blocking Interface only inside the thread which makes calls to Pa_WriteStream/Pa_ReadStream.
Expand Down
9 changes: 5 additions & 4 deletions msvc/portaudio.def
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ PaWasapi_SetStreamStateHandler @68
PaWasapiWinrt_SetDefaultDeviceId @67
PaWasapiWinrt_PopulateDeviceList @69
PaWasapi_GetIMMDevice @70
PaWinMME_GetStreamInputHandleCount @71
PaWinMME_GetStreamInputHandle @72
PaWinMME_GetStreamOutputHandleCount @73
PaWinMME_GetStreamOutputHandle @74
PaWasapi_IsLoopback @71
PaWinMME_GetStreamInputHandleCount @72
PaWinMME_GetStreamInputHandle @73
PaWinMME_GetStreamOutputHandleCount @74
PaWinMME_GetStreamOutputHandle @75
174 changes: 161 additions & 13 deletions src/hostapi/wasapi/pa_win_wasapi.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Portable Audio I/O Library WASAPI implementation
* Copyright (c) 2006-2010 David Viens
* Copyright (c) 2010-2019 Dmitry Kostjuchenko
* Copyright (c) 2010-2022 Dmitry Kostjuchenko
*
* Based on the Open Source API proposed by Ross Bencina
* Copyright (c) 1999-2019 Ross Bencina, Phil Burk
Expand Down Expand Up @@ -367,6 +367,7 @@ enum { WASAPI_PACKETS_PER_INPUT_BUFFER = 6 };

#define SAFE_CLOSE(h) if ((h) != NULL) { CloseHandle((h)); (h) = NULL; }
#define SAFE_RELEASE(punk) if ((punk) != NULL) { (punk)->lpVtbl->Release((punk)); (punk) = NULL; }
#define SAFE_ADDREF(punk) if ((punk) != NULL) { (punk)->lpVtbl->AddRef((punk)); }

// Mixer function
typedef void (*MixMonoToStereoF) (void *__to, const void *__from, UINT32 count);
Expand Down Expand Up @@ -478,12 +479,15 @@ typedef struct PaWasapiDeviceInfo

// Form-factor
EndpointFormFactor formFactor;

// Loopback state (TRUE if device acts as loopback)
BOOL loopBack;
}
PaWasapiDeviceInfo;

// ------------------------------------------------------------------------------------------
/* PaWasapiHostApiRepresentation - host api datastructure specific to this implementation */
typedef struct
typedef struct PaWasapiHostApiRepresentation
{
PaUtilHostApiRepresentation inheritedHostApiRep;
PaUtilStreamInterface callbackStreamInterface;
Expand Down Expand Up @@ -2071,22 +2075,21 @@ static PaError FillDeviceInfo(PaWasapiHostApiRepresentation *paWasapi, void *pEn
}

// ------------------------------------------------------------------------------------------
static PaDeviceInfo *AllocateDeviceListMemory(PaWasapiHostApiRepresentation *paWasapi)
static PaDeviceInfo *AllocateDeviceListMemory(PaWasapiHostApiRepresentation *paWasapi, UINT32 deviceCount)
{
PaUtilHostApiRepresentation *hostApi = (PaUtilHostApiRepresentation *)paWasapi;
PaDeviceInfo *deviceInfoArray = NULL;

if ((paWasapi->devInfo = (PaWasapiDeviceInfo *)PaUtil_GroupAllocateMemory(paWasapi->allocations,
sizeof(PaWasapiDeviceInfo) * paWasapi->deviceCount)) == NULL)
sizeof(PaWasapiDeviceInfo) * deviceCount)) == NULL)
{
return NULL;
}
memset(paWasapi->devInfo, 0, sizeof(PaWasapiDeviceInfo) * paWasapi->deviceCount);
memset(paWasapi->devInfo, 0, sizeof(PaWasapiDeviceInfo) * deviceCount);

if (paWasapi->deviceCount != 0)
if (deviceCount != 0)
{
UINT32 i;
UINT32 deviceCount = paWasapi->deviceCount;
#if defined(PA_WASAPI_MAX_CONST_DEVICE_COUNT) && (PA_WASAPI_MAX_CONST_DEVICE_COUNT > 0)
if (deviceCount < PA_WASAPI_MAX_CONST_DEVICE_COUNT)
deviceCount = PA_WASAPI_MAX_CONST_DEVICE_COUNT;
Expand All @@ -2112,13 +2115,101 @@ static PaDeviceInfo *AllocateDeviceListMemory(PaWasapiHostApiRepresentation *paW
return deviceInfoArray;
}

// ------------------------------------------------------------------------------------------
#ifndef PA_WINRT
static UINT32 GetDeviceListDeviceCount(IMMDeviceCollection *pEndPoints, EDataFlow filterFlow)
{
HRESULT hr;
UINT32 deviceCount;
IMMDevice *device;
IMMEndpoint *endpoint;
EDataFlow flow;
UINT32 ret = 0;

hr = IMMDeviceCollection_GetCount(pEndPoints, &deviceCount);
IF_FAILED_JUMP(hr, error);

for (UINT32 i = 0; i < deviceCount; ++i)
{
hr = IMMDeviceCollection_Item((IMMDeviceCollection *)pEndPoints, i, &device);
IF_FAILED_JUMP(hr, error);

if (SUCCEEDED(hr = IMMDevice_QueryInterface(device, &pa_IID_IMMEndpoint, (void **)&endpoint)))
{
if (SUCCEEDED(hr = IMMEndpoint_GetDataFlow(endpoint, &flow)))
ret += (flow == filterFlow);

SAFE_RELEASE(endpoint);
}

SAFE_RELEASE(device);
}

return ret;

error:

return 0;
}
#else
static UINT32 GetDeviceListDeviceCount(const PaWasapiHostApiRepresentation *paWasapi,
const PaWasapiWinrtDeviceListContext *deviceListContext, EDataFlow filterFlow)
{
UINT32 i, ret = 0;

for (i = 0; i < paWasapi->deviceCount; ++i)
ret += (deviceListContext->devices[i].flow == filterFlow);

return ret;
}
#endif

// ------------------------------------------------------------------------------------------
static BOOL FillLooopbackDeviceInfo(PaWasapiHostApiRepresentation *paWasapi, PaDeviceInfo *loopbackDeviceInfo,
PaWasapiDeviceInfo *loopbackWasapiInfo, const PaDeviceInfo *deviceInfo, const PaWasapiDeviceInfo *wasapiInfo)
{
// Loopback device name identificator
// note: Some projects depend on loopback device detection by device name, do not change!
#define PA_WASAPI_LOOPBACK_NAME_IDENTIFICATOR "[Loopback]"

memcpy(loopbackDeviceInfo, deviceInfo, sizeof(*loopbackDeviceInfo));
memcpy(loopbackWasapiInfo, wasapiInfo, sizeof(*loopbackWasapiInfo));

// Append loopback device name identificator to the device name to provide possibility to find
// loopback device by its name for some external projects
loopbackDeviceInfo->name = (char *)PaUtil_GroupAllocateMemory(paWasapi->allocations, PA_WASAPI_DEVICE_NAME_LEN + 1);
if (loopbackDeviceInfo->name == NULL)
return FALSE;
_snprintf((char *)loopbackDeviceInfo->name, PA_WASAPI_DEVICE_NAME_LEN - 1,
"%s " PA_WASAPI_LOOPBACK_NAME_IDENTIFICATOR, deviceInfo->name);

// Acquire ref to the device as it is already referenced as render (output) device
// to avoid duplicate release in ReleaseWasapiDeviceInfoList()
#ifndef PA_WINRT
SAFE_ADDREF(loopbackWasapiInfo->device);
#endif

// Mark as loopback device
loopbackWasapiInfo->loopBack = TRUE;

// Input is the reverse of Output
loopbackDeviceInfo->maxInputChannels = deviceInfo->maxOutputChannels;
loopbackDeviceInfo->defaultHighInputLatency = deviceInfo->defaultHighOutputLatency;
loopbackDeviceInfo->defaultLowInputLatency = deviceInfo->defaultLowOutputLatency;
loopbackDeviceInfo->maxOutputChannels = 0;
loopbackDeviceInfo->defaultHighOutputLatency = 0;
loopbackDeviceInfo->defaultLowOutputLatency = 0;

return TRUE;
}

// ------------------------------------------------------------------------------------------
static PaError CreateDeviceList(PaWasapiHostApiRepresentation *paWasapi, PaHostApiIndex hostApiIndex)
{
PaUtilHostApiRepresentation *hostApi = (PaUtilHostApiRepresentation *)paWasapi;
PaError result = paNoError;
PaDeviceInfo *deviceInfoArray = NULL;
UINT32 i;
UINT32 i, j, loopbacks;
WCHAR *defaultRenderId = NULL;
WCHAR *defaultCaptureId = NULL;
#ifndef PA_WINRT
Expand Down Expand Up @@ -2184,6 +2275,9 @@ static PaError CreateDeviceList(PaWasapiHostApiRepresentation *paWasapi, PaHostA
// Get device count
hr = IMMDeviceCollection_GetCount(pEndPoints, &paWasapi->deviceCount);
IF_FAILED_INTERNAL_ERROR_JUMP(hr, result, error);

// Get loopback device count (e.g. all renders)
loopbacks = GetDeviceListDeviceCount(pEndPoints, eRender);
#else
WinRT_GetDefaultDeviceId(defaultRender.id, STATIC_ARRAY_SIZE(defaultRender.id) - 1, eRender);
defaultRenderId = defaultRender.id;
Expand Down Expand Up @@ -2232,27 +2326,32 @@ static PaError CreateDeviceList(PaWasapiHostApiRepresentation *paWasapi, PaHostA
paWasapi->deviceCount++;
}
}

// Get loopback device count (e.g. all renders)
loopbacks = GetDeviceListDeviceCount(paWasapi, &deviceListContext, eRender);
#endif

// Allocate memory for the device list
if ((paWasapi->deviceCount != 0) && ((deviceInfoArray = AllocateDeviceListMemory(paWasapi)) == NULL))
if ((paWasapi->deviceCount != 0) &&
((deviceInfoArray = AllocateDeviceListMemory(paWasapi, paWasapi->deviceCount + loopbacks)) == NULL))
{
result = paInsufficientMemory;
goto error;
}

// Fill WASAPI device info
for (i = 0; i < paWasapi->deviceCount; ++i)
for (i = 0, j = 0; i < paWasapi->deviceCount; ++i)
{
PaDeviceInfo *deviceInfo = &deviceInfoArray[i];
PaWasapiDeviceInfo *wasapiInfo = &paWasapi->devInfo[i];

PA_DEBUG(("WASAPI: device idx: %02d\n", i));
PA_DEBUG(("WASAPI: ---------------\n"));

FillBaseDeviceInfo(deviceInfo, hostApiIndex);

if ((result = FillDeviceInfo(paWasapi, pEndPoints, i, defaultRenderId, defaultCaptureId,
deviceInfo, &paWasapi->devInfo[i]
deviceInfo, wasapiInfo
#ifdef PA_WINRT
, &deviceListContext
#endif
Expand All @@ -2265,8 +2364,29 @@ static PaError CreateDeviceList(PaWasapiHostApiRepresentation *paWasapi, PaHostA

hostApi->deviceInfos[i] = deviceInfo;
++hostApi->info.deviceCount;

// Add loopback device for the render device
if (paWasapi->devInfo[i].flow == eRender)
{
// Add loopback device to the end of the device list
UINT32 loopbackIndex = paWasapi->deviceCount + j++;
PaDeviceInfo *loopbackDeviceInfo = &deviceInfoArray[loopbackIndex];
PaWasapiDeviceInfo *loopbackWasapiInfo = &paWasapi->devInfo[loopbackIndex];

if (!FillLooopbackDeviceInfo(paWasapi, loopbackDeviceInfo, loopbackWasapiInfo, deviceInfo, wasapiInfo))
{
result = paInsufficientMemory;
goto error;
}

hostApi->deviceInfos[loopbackIndex] = loopbackDeviceInfo;
++hostApi->info.deviceCount;
}
}

// Resize device list to accomodate inserted loopback devices
paWasapi->deviceCount += loopbacks;

// Fill the remaining slots with inactive device info
#if defined(PA_WASAPI_MAX_CONST_DEVICE_COUNT) && (PA_WASAPI_MAX_CONST_DEVICE_COUNT > 0)
if ((hostApi->info.deviceCount != 0) && (hostApi->info.deviceCount < PA_WASAPI_MAX_CONST_DEVICE_COUNT))
Expand All @@ -2289,7 +2409,7 @@ static PaError CreateDeviceList(PaWasapiHostApiRepresentation *paWasapi, PaHostA
// Clear any non-fatal errors
result = paNoError;

PRINT(("WASAPI: device list ok - found %d devices\n", paWasapi->deviceCount));
PRINT(("WASAPI: device list ok - found %d devices, %d loopbacks\n", paWasapi->deviceCount, loopbacks));

done:

Expand Down Expand Up @@ -2640,6 +2760,29 @@ PaError PaWasapi_GetIMMDevice( PaDeviceIndex device, void **pIMMDevice )
#endif
}

// ------------------------------------------------------------------------------------------
int PaWasapi_IsLoopback( PaDeviceIndex device )
{
PaError ret;
PaDeviceIndex index;

// Get API
PaWasapiHostApiRepresentation *paWasapi = _GetHostApi(&ret);
if (paWasapi == NULL)
return paNotInitialized;

// Get device index
ret = PaUtil_DeviceIndexToHostApiDeviceIndex(&index, device, &paWasapi->inheritedHostApiRep);
if (ret != paNoError)
return ret;

// Validate index
if ((UINT32)index >= paWasapi->deviceCount)
return paInvalidDevice;

return paWasapi->devInfo[ index ].loopBack;
}

// ------------------------------------------------------------------------------------------
PaError PaWasapi_GetFramesPerHostBuffer( PaStream *pStream, unsigned int *pInput, unsigned int *pOutput )
{
Expand Down Expand Up @@ -3309,7 +3452,7 @@ static HRESULT CreateAudioClient(PaWasapiStream *pStream, PaWasapiSubStream *pSu
if ((params->channelCount == 1) && (pSub->wavex.Format.nChannels == 2))
{
// select mixer
pSub->monoMixer = GetMonoToStereoMixer(&pSub->wavex, (pInfo->flow == eRender ? MIX_DIR__1TO2 : MIX_DIR__2TO1_L));
pSub->monoMixer = GetMonoToStereoMixer(&pSub->wavex, (output ? MIX_DIR__1TO2 : MIX_DIR__2TO1_L));
if (pSub->monoMixer == NULL)
{
(*pa_error) = paInvalidChannelCount;
Expand Down Expand Up @@ -3864,6 +4007,11 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi,
((inputStreamInfo != NULL) && (inputStreamInfo->flags & paWinWasapiAutoConvert)))
stream->in.streamFlags |= (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY);

// Loopback device is a real output device, so we open stream with AUDCLNT_STREAMFLAGS_LOOPBACK
// flag to make it work as input device
if (info->loopBack)
stream->in.streamFlags |= AUDCLNT_STREAMFLAGS_LOOPBACK;

// Fill parameters for Audio Client creation
stream->in.params.device_info = info;
stream->in.params.stream_params = (*inputParameters);
Expand Down