mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-27 10:40:34 +00:00
0aed64426b
The calculation for the frame count in the non-aligned case resulted in a one too low buffer frame count. This resulted in: 1) exclusive mode not working as the frame count has to match exactly there. 2) Buffer underruns in shared mode as the current write() code doesn't handle catching up to low buffer levels (fixed in the next commit) To fix just use the wasapi API to get the buffer size which will always be correct. https://bugzilla.gnome.org/show_bug.cgi?id=796354
999 lines
30 KiB
C
999 lines
30 KiB
C
/*
|
|
* Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>
|
|
* Copyright (C) 2018 Centricular Ltd.
|
|
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include "gstwasapiutil.h"
|
|
#include "gstwasapidevice.h"
|
|
|
|
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug);
|
|
#define GST_CAT_DEFAULT gst_wasapi_debug
|
|
|
|
/* This was only added to MinGW in ~2015 and our Cerbero toolchain is too old */
|
|
#if defined(_MSC_VER)
|
|
#include <functiondiscoverykeys_devpkey.h>
|
|
#elif !defined(PKEY_Device_FriendlyName)
|
|
#include <initguid.h>
|
|
#include <propkey.h>
|
|
DEFINE_PROPERTYKEY (PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80,
|
|
0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
|
|
DEFINE_PROPERTYKEY (PKEY_AudioEngine_DeviceFormat, 0xf19f064d, 0x82c, 0x4e27,
|
|
0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, 0);
|
|
#endif
|
|
|
|
/* __uuidof is only available in C++, so we hard-code the GUID values for all
|
|
* these. This is ok because these are ABI. */
|
|
const CLSID CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,
|
|
{0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e}
|
|
};
|
|
|
|
const IID IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,
|
|
{0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6}
|
|
};
|
|
|
|
const IID IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,
|
|
{0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5}
|
|
};
|
|
|
|
const IID IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32,
|
|
{0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2}
|
|
};
|
|
|
|
const IID IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4,
|
|
{0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42}
|
|
};
|
|
|
|
const IID IID_IAudioClock = { 0xcd63314f, 0x3fba, 0x4a1b,
|
|
{0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7}
|
|
};
|
|
|
|
const IID IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0,
|
|
{0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17}
|
|
};
|
|
|
|
const IID IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483,
|
|
{0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2}
|
|
};
|
|
|
|
/* *INDENT-OFF* */
|
|
static struct
|
|
{
|
|
guint64 wasapi_pos;
|
|
GstAudioChannelPosition gst_pos;
|
|
} wasapi_to_gst_pos[] = {
|
|
{SPEAKER_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT},
|
|
{SPEAKER_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT},
|
|
{SPEAKER_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER},
|
|
{SPEAKER_LOW_FREQUENCY, GST_AUDIO_CHANNEL_POSITION_LFE1},
|
|
{SPEAKER_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_REAR_LEFT},
|
|
{SPEAKER_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT},
|
|
{SPEAKER_FRONT_LEFT_OF_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER},
|
|
{SPEAKER_FRONT_RIGHT_OF_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER},
|
|
{SPEAKER_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_REAR_CENTER},
|
|
/* Enum values diverge from this point onwards */
|
|
{SPEAKER_SIDE_LEFT, GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT},
|
|
{SPEAKER_SIDE_RIGHT, GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT},
|
|
{SPEAKER_TOP_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_CENTER},
|
|
{SPEAKER_TOP_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT},
|
|
{SPEAKER_TOP_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER},
|
|
{SPEAKER_TOP_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT},
|
|
{SPEAKER_TOP_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT},
|
|
{SPEAKER_TOP_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER},
|
|
{SPEAKER_TOP_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT}
|
|
};
|
|
/* *INDENT-ON* */
|
|
|
|
static int windows_major_version = 0;
|
|
|
|
static struct
|
|
{
|
|
HMODULE dll;
|
|
gboolean tried_loading;
|
|
|
|
HANDLE (WINAPI * AvSetMmThreadCharacteristics) (LPCSTR, LPDWORD);
|
|
BOOL (WINAPI * AvRevertMmThreadCharacteristics) (HANDLE);
|
|
} gst_wasapi_avrt_tbl = {
|
|
0};
|
|
|
|
gboolean
|
|
gst_wasapi_util_have_audioclient3 (void)
|
|
{
|
|
if (windows_major_version > 0)
|
|
return windows_major_version == 10;
|
|
|
|
if (g_getenv ("GST_WASAPI_DISABLE_AUDIOCLIENT3") != NULL) {
|
|
windows_major_version = 6;
|
|
return FALSE;
|
|
}
|
|
|
|
/* https://msdn.microsoft.com/en-us/library/windows/desktop/ms724834(v=vs.85).aspx */
|
|
windows_major_version = 6;
|
|
if (g_win32_check_windows_version (10, 0, 0, G_WIN32_OS_ANY))
|
|
windows_major_version = 10;
|
|
|
|
return windows_major_version == 10;
|
|
}
|
|
|
|
GType
|
|
gst_wasapi_device_role_get_type (void)
|
|
{
|
|
static const GEnumValue values[] = {
|
|
{GST_WASAPI_DEVICE_ROLE_CONSOLE,
|
|
"Games, system notifications, voice commands", "console"},
|
|
{GST_WASAPI_DEVICE_ROLE_MULTIMEDIA, "Music, movies, recorded media",
|
|
"multimedia"},
|
|
{GST_WASAPI_DEVICE_ROLE_COMMS, "Voice communications", "comms"},
|
|
{0, NULL, NULL}
|
|
};
|
|
static volatile GType id = 0;
|
|
|
|
if (g_once_init_enter ((gsize *) & id)) {
|
|
GType _id;
|
|
|
|
_id = g_enum_register_static ("GstWasapiDeviceRole", values);
|
|
|
|
g_once_init_leave ((gsize *) & id, _id);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
gint
|
|
gst_wasapi_device_role_to_erole (gint role)
|
|
{
|
|
switch (role) {
|
|
case GST_WASAPI_DEVICE_ROLE_CONSOLE:
|
|
return eConsole;
|
|
case GST_WASAPI_DEVICE_ROLE_MULTIMEDIA:
|
|
return eMultimedia;
|
|
case GST_WASAPI_DEVICE_ROLE_COMMS:
|
|
return eCommunications;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
gint
|
|
gst_wasapi_erole_to_device_role (gint erole)
|
|
{
|
|
switch (erole) {
|
|
case eConsole:
|
|
return GST_WASAPI_DEVICE_ROLE_CONSOLE;
|
|
case eMultimedia:
|
|
return GST_WASAPI_DEVICE_ROLE_MULTIMEDIA;
|
|
case eCommunications:
|
|
return GST_WASAPI_DEVICE_ROLE_COMMS;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
static const gchar *
|
|
hresult_to_string_fallback (HRESULT hr)
|
|
{
|
|
const gchar *s = "unknown error";
|
|
|
|
switch (hr) {
|
|
case AUDCLNT_E_NOT_INITIALIZED:
|
|
s = "AUDCLNT_E_NOT_INITIALIZED";
|
|
break;
|
|
case AUDCLNT_E_ALREADY_INITIALIZED:
|
|
s = "AUDCLNT_E_ALREADY_INITIALIZED";
|
|
break;
|
|
case AUDCLNT_E_WRONG_ENDPOINT_TYPE:
|
|
s = "AUDCLNT_E_WRONG_ENDPOINT_TYPE";
|
|
break;
|
|
case AUDCLNT_E_DEVICE_INVALIDATED:
|
|
s = "AUDCLNT_E_DEVICE_INVALIDATED";
|
|
break;
|
|
case AUDCLNT_E_NOT_STOPPED:
|
|
s = "AUDCLNT_E_NOT_STOPPED";
|
|
break;
|
|
case AUDCLNT_E_BUFFER_TOO_LARGE:
|
|
s = "AUDCLNT_E_BUFFER_TOO_LARGE";
|
|
break;
|
|
case AUDCLNT_E_OUT_OF_ORDER:
|
|
s = "AUDCLNT_E_OUT_OF_ORDER";
|
|
break;
|
|
case AUDCLNT_E_UNSUPPORTED_FORMAT:
|
|
s = "AUDCLNT_E_UNSUPPORTED_FORMAT";
|
|
break;
|
|
case AUDCLNT_E_INVALID_DEVICE_PERIOD:
|
|
s = "AUDCLNT_E_INVALID_DEVICE_PERIOD";
|
|
break;
|
|
case AUDCLNT_E_INVALID_SIZE:
|
|
s = "AUDCLNT_E_INVALID_SIZE";
|
|
break;
|
|
case AUDCLNT_E_DEVICE_IN_USE:
|
|
s = "AUDCLNT_E_DEVICE_IN_USE";
|
|
break;
|
|
case AUDCLNT_E_BUFFER_OPERATION_PENDING:
|
|
s = "AUDCLNT_E_BUFFER_OPERATION_PENDING";
|
|
break;
|
|
case AUDCLNT_E_BUFFER_SIZE_ERROR:
|
|
s = "AUDCLNT_E_BUFFER_SIZE_ERROR";
|
|
break;
|
|
case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED:
|
|
s = "AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED";
|
|
break;
|
|
case AUDCLNT_E_THREAD_NOT_REGISTERED:
|
|
s = "AUDCLNT_E_THREAD_NOT_REGISTERED";
|
|
break;
|
|
case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED:
|
|
s = "AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED";
|
|
break;
|
|
case AUDCLNT_E_ENDPOINT_CREATE_FAILED:
|
|
s = "AUDCLNT_E_ENDPOINT_CREATE_FAILED";
|
|
break;
|
|
case AUDCLNT_E_SERVICE_NOT_RUNNING:
|
|
s = "AUDCLNT_E_SERVICE_NOT_RUNNING";
|
|
break;
|
|
case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED:
|
|
s = "AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED";
|
|
break;
|
|
case AUDCLNT_E_EXCLUSIVE_MODE_ONLY:
|
|
s = "AUDCLNT_E_EXCLUSIVE_MODE_ONLY";
|
|
break;
|
|
case AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL:
|
|
s = "AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL";
|
|
break;
|
|
case AUDCLNT_E_EVENTHANDLE_NOT_SET:
|
|
s = "AUDCLNT_E_EVENTHANDLE_NOT_SET";
|
|
break;
|
|
case AUDCLNT_E_INCORRECT_BUFFER_SIZE:
|
|
s = "AUDCLNT_E_INCORRECT_BUFFER_SIZE";
|
|
break;
|
|
case AUDCLNT_E_CPUUSAGE_EXCEEDED:
|
|
s = "AUDCLNT_E_CPUUSAGE_EXCEEDED";
|
|
break;
|
|
case AUDCLNT_S_BUFFER_EMPTY:
|
|
s = "AUDCLNT_S_BUFFER_EMPTY";
|
|
break;
|
|
case AUDCLNT_S_THREAD_ALREADY_REGISTERED:
|
|
s = "AUDCLNT_S_THREAD_ALREADY_REGISTERED";
|
|
break;
|
|
case AUDCLNT_S_POSITION_STALLED:
|
|
s = "AUDCLNT_S_POSITION_STALLED";
|
|
break;
|
|
case E_POINTER:
|
|
s = "E_POINTER";
|
|
break;
|
|
case E_INVALIDARG:
|
|
s = "E_INVALIDARG";
|
|
break;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
gchar *
|
|
gst_wasapi_util_hresult_to_string (HRESULT hr)
|
|
{
|
|
DWORD flags;
|
|
gchar *ret_text;
|
|
LPTSTR error_text = NULL;
|
|
|
|
flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER
|
|
| FORMAT_MESSAGE_IGNORE_INSERTS;
|
|
FormatMessage (flags, NULL, hr, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
(LPTSTR) & error_text, 0, NULL);
|
|
|
|
/* If we couldn't get the error msg, try the fallback switch statement */
|
|
if (error_text == NULL)
|
|
return g_strdup (hresult_to_string_fallback (hr));
|
|
|
|
#ifdef UNICODE
|
|
/* If UNICODE is defined, LPTSTR is LPWSTR which is UTF-16 */
|
|
ret_text = g_utf16_to_utf8 (error_text, 0, NULL, NULL, NULL);
|
|
#else
|
|
ret_text = g_strdup (error_text);
|
|
#endif
|
|
|
|
LocalFree (error_text);
|
|
return ret_text;
|
|
}
|
|
|
|
static IMMDeviceEnumerator *
|
|
gst_wasapi_util_get_device_enumerator (GstElement * self)
|
|
{
|
|
HRESULT hr;
|
|
IMMDeviceEnumerator *enumerator = NULL;
|
|
|
|
hr = CoCreateInstance (&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
|
|
&IID_IMMDeviceEnumerator, (void **) &enumerator);
|
|
HR_FAILED_RET (hr, CoCreateInstance (MMDeviceEnumerator), NULL);
|
|
|
|
return enumerator;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_devices (GstElement * self, gboolean active,
|
|
GList ** devices)
|
|
{
|
|
gboolean res = FALSE;
|
|
static GstStaticCaps scaps = GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS);
|
|
DWORD dwStateMask = active ? DEVICE_STATE_ACTIVE : DEVICE_STATEMASK_ALL;
|
|
IMMDeviceCollection *device_collection = NULL;
|
|
IMMDeviceEnumerator *enumerator = NULL;
|
|
const gchar *device_class, *element_name;
|
|
guint ii, count;
|
|
HRESULT hr;
|
|
|
|
*devices = NULL;
|
|
|
|
enumerator = gst_wasapi_util_get_device_enumerator (self);
|
|
if (!enumerator)
|
|
return FALSE;
|
|
|
|
hr = IMMDeviceEnumerator_EnumAudioEndpoints (enumerator, eAll, dwStateMask,
|
|
&device_collection);
|
|
HR_FAILED_GOTO (hr, IMMDeviceEnumerator::EnumAudioEndpoints, err);
|
|
|
|
hr = IMMDeviceCollection_GetCount (device_collection, &count);
|
|
HR_FAILED_GOTO (hr, IMMDeviceCollection::GetCount, err);
|
|
|
|
/* Create a GList of GstDevices* to return */
|
|
for (ii = 0; ii < count; ii++) {
|
|
IMMDevice *item = NULL;
|
|
IMMEndpoint *endpoint = NULL;
|
|
IAudioClient *client = NULL;
|
|
IPropertyStore *prop_store = NULL;
|
|
WAVEFORMATEX *format = NULL;
|
|
gchar *description = NULL;
|
|
gchar *strid = NULL;
|
|
EDataFlow dataflow;
|
|
PROPVARIANT var;
|
|
wchar_t *wstrid;
|
|
GstDevice *device;
|
|
GstStructure *props;
|
|
GstCaps *caps;
|
|
|
|
hr = IMMDeviceCollection_Item (device_collection, ii, &item);
|
|
if (hr != S_OK)
|
|
continue;
|
|
|
|
hr = IMMDevice_QueryInterface (item, &IID_IMMEndpoint, (void **) &endpoint);
|
|
if (hr != S_OK)
|
|
goto next;
|
|
|
|
hr = IMMEndpoint_GetDataFlow (endpoint, &dataflow);
|
|
if (hr != S_OK)
|
|
goto next;
|
|
|
|
if (dataflow == eRender) {
|
|
device_class = "Audio/Sink";
|
|
element_name = "wasapisink";
|
|
} else {
|
|
device_class = "Audio/Source";
|
|
element_name = "wasapisrc";
|
|
}
|
|
|
|
PropVariantInit (&var);
|
|
|
|
hr = IMMDevice_GetId (item, &wstrid);
|
|
if (hr != S_OK)
|
|
goto next;
|
|
strid = g_utf16_to_utf8 (wstrid, -1, NULL, NULL, NULL);
|
|
CoTaskMemFree (wstrid);
|
|
|
|
hr = IMMDevice_OpenPropertyStore (item, STGM_READ, &prop_store);
|
|
if (hr != S_OK)
|
|
goto next;
|
|
|
|
/* NOTE: More properties can be added as needed from here:
|
|
* https://msdn.microsoft.com/en-us/library/windows/desktop/dd370794(v=vs.85).aspx */
|
|
hr = IPropertyStore_GetValue (prop_store, &PKEY_Device_FriendlyName, &var);
|
|
if (hr != S_OK)
|
|
goto next;
|
|
description = g_utf16_to_utf8 (var.pwszVal, -1, NULL, NULL, NULL);
|
|
PropVariantClear (&var);
|
|
|
|
/* Get the audio client so we can fetch the mix format for shared mode
|
|
* to get the device format for exclusive mode (or something close to that)
|
|
* fetch PKEY_AudioEngine_DeviceFormat from the property store. */
|
|
hr = IMMDevice_Activate (item, &IID_IAudioClient, CLSCTX_ALL, NULL,
|
|
(void **) &client);
|
|
if (hr != S_OK) {
|
|
gchar *msg = gst_wasapi_util_hresult_to_string (hr);
|
|
GST_ERROR_OBJECT (self, "IMMDevice::Activate (IID_IAudioClient) failed"
|
|
"on %s: %s", strid, msg);
|
|
g_free (msg);
|
|
goto next;
|
|
}
|
|
|
|
hr = IAudioClient_GetMixFormat (client, &format);
|
|
if (hr != S_OK || format == NULL) {
|
|
gchar *msg = gst_wasapi_util_hresult_to_string (hr);
|
|
GST_ERROR_OBJECT (self, "GetMixFormat failed on %s: %s", strid, msg);
|
|
g_free (msg);
|
|
goto next;
|
|
}
|
|
|
|
if (!gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format,
|
|
gst_static_caps_get (&scaps), &caps, NULL))
|
|
goto next;
|
|
|
|
/* Set some useful properties */
|
|
props = gst_structure_new ("wasapi-proplist",
|
|
"device.api", G_TYPE_STRING, "wasapi",
|
|
"device.strid", G_TYPE_STRING, GST_STR_NULL (strid),
|
|
"wasapi.device.description", G_TYPE_STRING, description, NULL);
|
|
|
|
device = g_object_new (GST_TYPE_WASAPI_DEVICE, "device", strid,
|
|
"display-name", description, "caps", caps,
|
|
"device-class", device_class, "properties", props, NULL);
|
|
GST_WASAPI_DEVICE (device)->element = element_name;
|
|
|
|
gst_structure_free (props);
|
|
gst_caps_unref (caps);
|
|
*devices = g_list_prepend (*devices, device);
|
|
|
|
next:
|
|
PropVariantClear (&var);
|
|
if (prop_store)
|
|
IUnknown_Release (prop_store);
|
|
if (endpoint)
|
|
IUnknown_Release (endpoint);
|
|
if (client)
|
|
IUnknown_Release (client);
|
|
if (item)
|
|
IUnknown_Release (item);
|
|
if (description)
|
|
g_free (description);
|
|
if (strid)
|
|
g_free (strid);
|
|
}
|
|
|
|
res = TRUE;
|
|
|
|
err:
|
|
if (enumerator)
|
|
IUnknown_Release (enumerator);
|
|
if (device_collection)
|
|
IUnknown_Release (device_collection);
|
|
return res;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_device_format (GstElement * self,
|
|
gint device_mode, IMMDevice * device, IAudioClient * client,
|
|
WAVEFORMATEX ** ret_format)
|
|
{
|
|
WAVEFORMATEX *format;
|
|
HRESULT hr;
|
|
|
|
*ret_format = NULL;
|
|
|
|
hr = IAudioClient_GetMixFormat (client, &format);
|
|
HR_FAILED_RET (hr, IAudioClient::GetMixFormat, FALSE);
|
|
|
|
/* WASAPI always accepts the format returned by GetMixFormat in shared mode */
|
|
if (device_mode == AUDCLNT_SHAREMODE_SHARED)
|
|
goto out;
|
|
|
|
/* WASAPI may or may not support this format in exclusive mode */
|
|
hr = IAudioClient_IsFormatSupported (client, AUDCLNT_SHAREMODE_EXCLUSIVE,
|
|
format, NULL);
|
|
if (hr == S_OK)
|
|
goto out;
|
|
|
|
CoTaskMemFree (format);
|
|
|
|
/* Open the device property store, and get the format that WASAPI has been
|
|
* using for sending data to the device */
|
|
{
|
|
PROPVARIANT var;
|
|
IPropertyStore *prop_store = NULL;
|
|
|
|
hr = IMMDevice_OpenPropertyStore (device, STGM_READ, &prop_store);
|
|
HR_FAILED_RET (hr, IMMDevice::OpenPropertyStore, FALSE);
|
|
|
|
hr = IPropertyStore_GetValue (prop_store, &PKEY_AudioEngine_DeviceFormat,
|
|
&var);
|
|
if (hr != S_OK) {
|
|
gchar *msg = gst_wasapi_util_hresult_to_string (hr);
|
|
GST_ERROR_OBJECT (self, "GetValue failed: %s", msg);
|
|
g_free (msg);
|
|
IUnknown_Release (prop_store);
|
|
return FALSE;
|
|
}
|
|
|
|
format = malloc (var.blob.cbSize);
|
|
memcpy (format, var.blob.pBlobData, var.blob.cbSize);
|
|
|
|
PropVariantClear (&var);
|
|
IUnknown_Release (prop_store);
|
|
}
|
|
|
|
/* WASAPI may or may not support this format in exclusive mode */
|
|
hr = IAudioClient_IsFormatSupported (client, AUDCLNT_SHAREMODE_EXCLUSIVE,
|
|
format, NULL);
|
|
if (hr == S_OK)
|
|
goto out;
|
|
|
|
GST_ERROR_OBJECT (self, "AudioEngine DeviceFormat not supported");
|
|
free (format);
|
|
return FALSE;
|
|
|
|
out:
|
|
*ret_format = format;
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_device_client (GstElement * self,
|
|
gint data_flow, gint role, const wchar_t * device_strid,
|
|
IMMDevice ** ret_device, IAudioClient ** ret_client)
|
|
{
|
|
gboolean res = FALSE;
|
|
HRESULT hr;
|
|
IMMDeviceEnumerator *enumerator = NULL;
|
|
IMMDevice *device = NULL;
|
|
IAudioClient *client = NULL;
|
|
|
|
if (!(enumerator = gst_wasapi_util_get_device_enumerator (self)))
|
|
goto beach;
|
|
|
|
if (!device_strid) {
|
|
hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint (enumerator, data_flow,
|
|
role, &device);
|
|
HR_FAILED_GOTO (hr, IMMDeviceEnumerator::GetDefaultAudioEndpoint, beach);
|
|
} else {
|
|
hr = IMMDeviceEnumerator_GetDevice (enumerator, device_strid, &device);
|
|
if (hr != S_OK) {
|
|
gchar *msg = gst_wasapi_util_hresult_to_string (hr);
|
|
GST_ERROR_OBJECT (self, "IMMDeviceEnumerator::GetDevice (%S) failed"
|
|
": %s", device_strid, msg);
|
|
g_free (msg);
|
|
goto beach;
|
|
}
|
|
}
|
|
|
|
if (gst_wasapi_util_have_audioclient3 ())
|
|
hr = IMMDevice_Activate (device, &IID_IAudioClient3, CLSCTX_ALL, NULL,
|
|
(void **) &client);
|
|
else
|
|
hr = IMMDevice_Activate (device, &IID_IAudioClient, CLSCTX_ALL, NULL,
|
|
(void **) &client);
|
|
HR_FAILED_GOTO (hr, IMMDevice::Activate (IID_IAudioClient), beach);
|
|
|
|
IUnknown_AddRef (client);
|
|
IUnknown_AddRef (device);
|
|
*ret_client = client;
|
|
*ret_device = device;
|
|
|
|
res = TRUE;
|
|
|
|
beach:
|
|
if (client != NULL)
|
|
IUnknown_Release (client);
|
|
|
|
if (device != NULL)
|
|
IUnknown_Release (device);
|
|
|
|
if (enumerator != NULL)
|
|
IUnknown_Release (enumerator);
|
|
|
|
return res;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_render_client (GstElement * self, IAudioClient * client,
|
|
IAudioRenderClient ** ret_render_client)
|
|
{
|
|
gboolean res = FALSE;
|
|
HRESULT hr;
|
|
IAudioRenderClient *render_client = NULL;
|
|
|
|
hr = IAudioClient_GetService (client, &IID_IAudioRenderClient,
|
|
(void **) &render_client);
|
|
HR_FAILED_GOTO (hr, IAudioClient::GetService, beach);
|
|
|
|
*ret_render_client = render_client;
|
|
res = TRUE;
|
|
|
|
beach:
|
|
return res;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_capture_client (GstElement * self, IAudioClient * client,
|
|
IAudioCaptureClient ** ret_capture_client)
|
|
{
|
|
gboolean res = FALSE;
|
|
HRESULT hr;
|
|
IAudioCaptureClient *capture_client = NULL;
|
|
|
|
hr = IAudioClient_GetService (client, &IID_IAudioCaptureClient,
|
|
(void **) &capture_client);
|
|
HR_FAILED_GOTO (hr, IAudioClient::GetService, beach);
|
|
|
|
*ret_capture_client = capture_client;
|
|
res = TRUE;
|
|
|
|
beach:
|
|
return res;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_get_clock (GstElement * self, IAudioClient * client,
|
|
IAudioClock ** ret_clock)
|
|
{
|
|
gboolean res = FALSE;
|
|
HRESULT hr;
|
|
IAudioClock *clock = NULL;
|
|
|
|
hr = IAudioClient_GetService (client, &IID_IAudioClock, (void **) &clock);
|
|
HR_FAILED_GOTO (hr, IAudioClient::GetService, beach);
|
|
|
|
*ret_clock = clock;
|
|
res = TRUE;
|
|
|
|
beach:
|
|
return res;
|
|
}
|
|
|
|
static const gchar *
|
|
gst_waveformatex_to_audio_format (WAVEFORMATEXTENSIBLE * format)
|
|
{
|
|
const gchar *fmt_str = NULL;
|
|
GstAudioFormat fmt = GST_AUDIO_FORMAT_UNKNOWN;
|
|
|
|
if (format->Format.wFormatTag == WAVE_FORMAT_PCM) {
|
|
fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN,
|
|
format->Format.wBitsPerSample, format->Format.wBitsPerSample);
|
|
} else if (format->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
|
|
if (format->Format.wBitsPerSample == 32)
|
|
fmt = GST_AUDIO_FORMAT_F32LE;
|
|
else if (format->Format.wBitsPerSample == 64)
|
|
fmt = GST_AUDIO_FORMAT_F64LE;
|
|
} else if (format->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
|
if (IsEqualGUID (&format->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM)) {
|
|
fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN,
|
|
format->Format.wBitsPerSample, format->Samples.wValidBitsPerSample);
|
|
} else if (IsEqualGUID (&format->SubFormat,
|
|
&KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
|
|
if (format->Format.wBitsPerSample == 32
|
|
&& format->Samples.wValidBitsPerSample == 32)
|
|
fmt = GST_AUDIO_FORMAT_F32LE;
|
|
else if (format->Format.wBitsPerSample == 64 &&
|
|
format->Samples.wValidBitsPerSample == 64)
|
|
fmt = GST_AUDIO_FORMAT_F64LE;
|
|
}
|
|
}
|
|
|
|
if (fmt != GST_AUDIO_FORMAT_UNKNOWN)
|
|
fmt_str = gst_audio_format_to_string (fmt);
|
|
|
|
return fmt_str;
|
|
}
|
|
|
|
static void
|
|
gst_wasapi_util_channel_position_all_none (guint channels,
|
|
GstAudioChannelPosition * position)
|
|
{
|
|
int ii;
|
|
for (ii = 0; ii < channels; ii++)
|
|
position[ii] = GST_AUDIO_CHANNEL_POSITION_NONE;
|
|
}
|
|
|
|
/* Parse WAVEFORMATEX to get the gstreamer channel mask, and the wasapi channel
|
|
* positions so GstAudioRingbuffer can reorder the audio data to match the
|
|
* gstreamer channel order. */
|
|
static guint64
|
|
gst_wasapi_util_waveformatex_to_channel_mask (WAVEFORMATEXTENSIBLE * format,
|
|
GstAudioChannelPosition ** out_position)
|
|
{
|
|
int ii;
|
|
guint64 mask = 0;
|
|
WORD nChannels = format->Format.nChannels;
|
|
DWORD dwChannelMask = format->dwChannelMask;
|
|
GstAudioChannelPosition *pos = NULL;
|
|
|
|
pos = g_new (GstAudioChannelPosition, nChannels);
|
|
gst_wasapi_util_channel_position_all_none (nChannels, pos);
|
|
|
|
/* Too many channels, have to assume that they are all non-positional */
|
|
if (nChannels > G_N_ELEMENTS (wasapi_to_gst_pos)) {
|
|
GST_INFO ("Got too many (%i) channels, assuming non-positional", nChannels);
|
|
goto out;
|
|
}
|
|
|
|
/* Too many bits in the channel mask, and the bits don't match nChannels */
|
|
if (dwChannelMask >> (G_N_ELEMENTS (wasapi_to_gst_pos) + 1) != 0) {
|
|
GST_WARNING ("Too many bits in channel mask (%lu), assuming "
|
|
"non-positional", dwChannelMask);
|
|
goto out;
|
|
}
|
|
|
|
/* Map WASAPI's channel mask to Gstreamer's channel mask and positions.
|
|
* If the no. of bits in the mask > nChannels, we will ignore the extra. */
|
|
for (ii = 0; ii < nChannels; ii++) {
|
|
if (!(dwChannelMask & wasapi_to_gst_pos[ii].wasapi_pos))
|
|
/* Non-positional or unknown position, warn? */
|
|
continue;
|
|
mask |= G_GUINT64_CONSTANT (1) << wasapi_to_gst_pos[ii].gst_pos;
|
|
pos[ii] = wasapi_to_gst_pos[ii].gst_pos;
|
|
}
|
|
|
|
out:
|
|
if (out_position)
|
|
*out_position = pos;
|
|
return mask;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format,
|
|
GstCaps * template_caps, GstCaps ** out_caps,
|
|
GstAudioChannelPosition ** out_positions)
|
|
{
|
|
int ii;
|
|
const gchar *afmt;
|
|
guint64 channel_mask;
|
|
|
|
*out_caps = NULL;
|
|
|
|
/* TODO: handle SPDIF and other encoded formats */
|
|
|
|
/* 1 or 2 channels <= 16 bits sample size OR
|
|
* 1 or 2 channels > 16 bits sample size or >2 channels */
|
|
if (format->Format.wFormatTag != WAVE_FORMAT_PCM &&
|
|
format->Format.wFormatTag != WAVE_FORMAT_IEEE_FLOAT &&
|
|
format->Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE)
|
|
/* Unhandled format tag */
|
|
return FALSE;
|
|
|
|
/* WASAPI can only tell us one canonical mix format that it will accept. The
|
|
* alternative is calling IsFormatSupported on all combinations of formats.
|
|
* Instead, it's simpler and faster to require conversion inside gstreamer */
|
|
afmt = gst_waveformatex_to_audio_format (format);
|
|
if (afmt == NULL)
|
|
return FALSE;
|
|
|
|
*out_caps = gst_caps_copy (template_caps);
|
|
|
|
/* This will always return something that might be usable */
|
|
channel_mask =
|
|
gst_wasapi_util_waveformatex_to_channel_mask (format, out_positions);
|
|
|
|
for (ii = 0; ii < gst_caps_get_size (*out_caps); ii++) {
|
|
GstStructure *s = gst_caps_get_structure (*out_caps, ii);
|
|
|
|
gst_structure_set (s,
|
|
"format", G_TYPE_STRING, afmt,
|
|
"channels", G_TYPE_INT, format->Format.nChannels,
|
|
"rate", G_TYPE_INT, format->Format.nSamplesPerSec,
|
|
"channel-mask", GST_TYPE_BITMASK, channel_mask, NULL);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
gst_wasapi_util_get_best_buffer_sizes (GstAudioRingBufferSpec * spec,
|
|
gboolean exclusive, REFERENCE_TIME default_period,
|
|
REFERENCE_TIME min_period, REFERENCE_TIME * ret_period,
|
|
REFERENCE_TIME * ret_buffer_duration)
|
|
{
|
|
REFERENCE_TIME use_period, use_buffer;
|
|
|
|
/* Figure out what integral device period to use as the base */
|
|
if (exclusive) {
|
|
/* Exclusive mode can run at multiples of either the minimum period or the
|
|
* default period; these are on the hardware ringbuffer */
|
|
if (spec->latency_time * 10 > default_period)
|
|
use_period = default_period;
|
|
else
|
|
use_period = min_period;
|
|
} else {
|
|
/* Shared mode always runs at the default period, so if we want a larger
|
|
* period (for lower CPU usage), we do it as a multiple of that */
|
|
use_period = default_period;
|
|
}
|
|
|
|
/* Ensure that the period (latency_time) used is an integral multiple of
|
|
* either the default period or the minimum period */
|
|
use_period = use_period * MAX ((spec->latency_time * 10) / use_period, 1);
|
|
|
|
if (exclusive) {
|
|
/* Buffer duration is the same as the period in exclusive mode. The
|
|
* hardware is always writing out one buffer (of size *ret_period), and
|
|
* we're writing to the other one. */
|
|
use_buffer = use_period;
|
|
} else {
|
|
/* Ask WASAPI to create a software ringbuffer of at least this size; it may
|
|
* be larger so the actual buffer time may be different, which is why after
|
|
* initialization we read the buffer duration actually in-use and set
|
|
* segsize/segtotal from that. */
|
|
use_buffer = spec->buffer_time * 10;
|
|
/* Has to be at least twice the period */
|
|
if (use_buffer < 2 * use_period)
|
|
use_buffer = 2 * use_period;
|
|
}
|
|
|
|
*ret_period = use_period;
|
|
*ret_buffer_duration = use_buffer;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_initialize_audioclient (GstElement * self,
|
|
GstAudioRingBufferSpec * spec, IAudioClient * client,
|
|
WAVEFORMATEX * format, guint sharemode, gboolean low_latency,
|
|
gboolean loopback, guint * ret_devicep_frames)
|
|
{
|
|
REFERENCE_TIME default_period, min_period;
|
|
REFERENCE_TIME device_period, device_buffer_duration;
|
|
guint rate, stream_flags;
|
|
guint32 n_frames;
|
|
HRESULT hr;
|
|
|
|
hr = IAudioClient_GetDevicePeriod (client, &default_period, &min_period);
|
|
HR_FAILED_RET (hr, IAudioClient::GetDevicePeriod, FALSE);
|
|
|
|
GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT
|
|
", min period: %" G_GINT64_FORMAT, default_period, min_period);
|
|
|
|
rate = GST_AUDIO_INFO_RATE (&spec->info);
|
|
|
|
if (low_latency) {
|
|
if (sharemode == AUDCLNT_SHAREMODE_SHARED) {
|
|
device_period = default_period;
|
|
device_buffer_duration = 0;
|
|
} else {
|
|
device_period = min_period;
|
|
device_buffer_duration = min_period;
|
|
}
|
|
} else {
|
|
/* Clamp values to integral multiples of an appropriate period */
|
|
gst_wasapi_util_get_best_buffer_sizes (spec,
|
|
sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE, default_period,
|
|
min_period, &device_period, &device_buffer_duration);
|
|
}
|
|
|
|
stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
|
|
if (loopback)
|
|
stream_flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
|
|
|
|
hr = IAudioClient_Initialize (client, sharemode, stream_flags,
|
|
device_buffer_duration,
|
|
/* This must always be 0 in shared mode */
|
|
sharemode == AUDCLNT_SHAREMODE_SHARED ? 0 : device_period, format, NULL);
|
|
|
|
if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED &&
|
|
sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
|
GST_WARNING_OBJECT (self, "initialize failed due to unaligned period %i",
|
|
(int) device_period);
|
|
|
|
/* Calculate a new aligned period. First get the aligned buffer size. */
|
|
hr = IAudioClient_GetBufferSize (client, &n_frames);
|
|
HR_FAILED_RET (hr, IAudioClient::GetBufferSize, FALSE);
|
|
|
|
device_period = (GST_SECOND / 100) * n_frames / rate;
|
|
|
|
GST_WARNING_OBJECT (self, "trying to re-initialize with period %i "
|
|
"(%i frames, %i rate)", (int) device_period, n_frames, rate);
|
|
|
|
hr = IAudioClient_Initialize (client, sharemode, stream_flags,
|
|
device_period, device_period, format, NULL);
|
|
}
|
|
HR_FAILED_RET (hr, IAudioClient::Initialize, FALSE);
|
|
|
|
hr = IAudioClient_GetBufferSize (client, &n_frames);
|
|
HR_FAILED_RET (hr, IAudioClient::GetBufferSize, FALSE);
|
|
|
|
*ret_devicep_frames = n_frames;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gst_wasapi_util_initialize_audioclient3 (GstElement * self,
|
|
GstAudioRingBufferSpec * spec, IAudioClient3 * client,
|
|
WAVEFORMATEX * format, gboolean low_latency, gboolean loopback,
|
|
guint * ret_devicep_frames)
|
|
{
|
|
HRESULT hr;
|
|
gint stream_flags;
|
|
guint devicep_frames;
|
|
guint defaultp_frames, fundp_frames, minp_frames, maxp_frames;
|
|
WAVEFORMATEX *tmpf;
|
|
|
|
hr = IAudioClient3_GetSharedModeEnginePeriod (client, format,
|
|
&defaultp_frames, &fundp_frames, &minp_frames, &maxp_frames);
|
|
HR_FAILED_RET (hr, IAudioClient3::GetSharedModeEnginePeriod, FALSE);
|
|
|
|
GST_INFO_OBJECT (self, "Using IAudioClient3, default period %i frames, "
|
|
"fundamental period %i frames, minimum period %i frames, maximum period "
|
|
"%i frames", defaultp_frames, fundp_frames, minp_frames, maxp_frames);
|
|
|
|
if (low_latency)
|
|
devicep_frames = minp_frames;
|
|
else
|
|
/* Just pick the max period, because lower values can cause glitches
|
|
* https://bugzilla.gnome.org/show_bug.cgi?id=794497 */
|
|
devicep_frames = maxp_frames;
|
|
|
|
stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
|
|
if (loopback)
|
|
stream_flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
|
|
|
|
hr = IAudioClient3_InitializeSharedAudioStream (client, stream_flags,
|
|
devicep_frames, format, NULL);
|
|
HR_FAILED_RET (hr, IAudioClient3::InitializeSharedAudioStream, FALSE);
|
|
|
|
hr = IAudioClient3_GetCurrentSharedModeEnginePeriod (client, &tmpf,
|
|
&devicep_frames);
|
|
CoTaskMemFree (tmpf);
|
|
HR_FAILED_RET (hr, IAudioClient3::GetCurrentSharedModeEnginePeriod, FALSE);
|
|
|
|
*ret_devicep_frames = devicep_frames;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wasapi_util_init_thread_priority (void)
|
|
{
|
|
if (gst_wasapi_avrt_tbl.tried_loading)
|
|
return gst_wasapi_avrt_tbl.dll != NULL;
|
|
|
|
if (!gst_wasapi_avrt_tbl.dll)
|
|
gst_wasapi_avrt_tbl.dll = LoadLibrary (TEXT ("avrt.dll"));
|
|
|
|
if (!gst_wasapi_avrt_tbl.dll) {
|
|
GST_WARNING ("Failed to set thread priority, can't find avrt.dll");
|
|
gst_wasapi_avrt_tbl.tried_loading = TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
gst_wasapi_avrt_tbl.AvSetMmThreadCharacteristics =
|
|
GetProcAddress (gst_wasapi_avrt_tbl.dll, "AvSetMmThreadCharacteristicsA");
|
|
gst_wasapi_avrt_tbl.AvRevertMmThreadCharacteristics =
|
|
GetProcAddress (gst_wasapi_avrt_tbl.dll,
|
|
"AvRevertMmThreadCharacteristics");
|
|
|
|
gst_wasapi_avrt_tbl.tried_loading = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
HANDLE
|
|
gst_wasapi_util_set_thread_characteristics (void)
|
|
{
|
|
DWORD taskIndex = 0;
|
|
|
|
if (!gst_wasapi_util_init_thread_priority ())
|
|
return NULL;
|
|
|
|
return gst_wasapi_avrt_tbl.AvSetMmThreadCharacteristics (TEXT ("Pro Audio"),
|
|
&taskIndex);
|
|
}
|
|
|
|
void
|
|
gst_wasapi_util_revert_thread_characteristics (HANDLE handle)
|
|
{
|
|
if (!gst_wasapi_util_init_thread_priority ())
|
|
return;
|
|
|
|
gst_wasapi_avrt_tbl.AvRevertMmThreadCharacteristics (handle);
|
|
}
|