/* * Copyright (C) 2008 Ole André Vadla Ravnås * Copyright (C) 2018 Centricular Ltd. * Author: Nirbheek Chauhan * Copyright (C) 2020 Seungha Yang * * 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 #endif #include "gstwasapi2util.h" #include #include #include GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug); #define GST_CAT_DEFAULT gst_wasapi2_debug /* Desktop only defines */ #ifndef KSAUDIO_SPEAKER_MONO #define KSAUDIO_SPEAKER_MONO (SPEAKER_FRONT_CENTER) #endif #ifndef KSAUDIO_SPEAKER_1POINT1 #define KSAUDIO_SPEAKER_1POINT1 (SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY) #endif #ifndef KSAUDIO_SPEAKER_STEREO #define KSAUDIO_SPEAKER_STEREO (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT) #endif #ifndef KSAUDIO_SPEAKER_2POINT1 #define KSAUDIO_SPEAKER_2POINT1 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_LOW_FREQUENCY) #endif #ifndef KSAUDIO_SPEAKER_3POINT0 #define KSAUDIO_SPEAKER_3POINT0 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER) #endif #ifndef KSAUDIO_SPEAKER_3POINT1 #define KSAUDIO_SPEAKER_3POINT1 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \ SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY) #endif #ifndef KSAUDIO_SPEAKER_QUAD #define KSAUDIO_SPEAKER_QUAD (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT) #endif #define KSAUDIO_SPEAKER_SURROUND (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \ SPEAKER_FRONT_CENTER | SPEAKER_BACK_CENTER) #ifndef KSAUDIO_SPEAKER_5POINT0 #define KSAUDIO_SPEAKER_5POINT0 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | \ SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT) #endif #define KSAUDIO_SPEAKER_5POINT1 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \ SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | \ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT) #ifndef KSAUDIO_SPEAKER_7POINT0 #define KSAUDIO_SPEAKER_7POINT0 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | \ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | \ SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT) #endif #ifndef KSAUDIO_SPEAKER_7POINT1 #define KSAUDIO_SPEAKER_7POINT1 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \ SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | \ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | \ SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_FRONT_RIGHT_OF_CENTER) #endif /* *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} }; static DWORD default_ch_masks[] = { 0, KSAUDIO_SPEAKER_MONO, /* 2ch */ KSAUDIO_SPEAKER_STEREO, /* 2.1ch */ /* KSAUDIO_SPEAKER_3POINT0 ? */ KSAUDIO_SPEAKER_2POINT1, /* 4ch */ /* KSAUDIO_SPEAKER_3POINT1 or KSAUDIO_SPEAKER_SURROUND ? */ KSAUDIO_SPEAKER_QUAD, /* 5ch */ KSAUDIO_SPEAKER_5POINT0, /* 5.1ch */ KSAUDIO_SPEAKER_5POINT1, /* 7ch */ KSAUDIO_SPEAKER_7POINT0, /* 7.1ch */ KSAUDIO_SPEAKER_7POINT1, }; /* *INDENT-ON* */ 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_wasapi2_util_get_error_message (HRESULT hr) { gchar *error_text = NULL; error_text = g_win32_error_message ((gint) hr); if (!error_text || strlen (error_text) == 0) { g_free (error_text); error_text = g_strdup (hresult_to_string_fallback (hr)); } return error_text; } gboolean _gst_wasapi2_result (HRESULT hr, GstDebugCategory * cat, const gchar * file, const gchar * function, gint line) { #ifndef GST_DISABLE_GST_DEBUG gboolean ret = TRUE; if (FAILED (hr)) { gchar *error_text = NULL; gboolean free_string = TRUE; error_text = g_win32_error_message ((gint) hr); /* g_win32_error_message() seems to be returning empty string for * AUDCLNT_* cases */ if (!error_text || strlen (error_text) == 0) { g_free (error_text); error_text = (gchar *) hresult_to_string_fallback (hr); free_string = FALSE; } gst_debug_log (cat, GST_LEVEL_WARNING, file, function, line, NULL, "WASAPI call failed: 0x%x, %s", (guint) hr, error_text); if (free_string) g_free (error_text); ret = FALSE; } return ret; #else return SUCCEEDED (hr); #endif } static void gst_wasapi_util_channel_position_all_none (guint channels, GstAudioChannelPosition * position) { guint i; for (i = 0; i < channels; i++) position[i] = GST_AUDIO_CHANNEL_POSITION_NONE; } guint64 gst_wasapi2_util_waveformatex_to_channel_mask (WAVEFORMATEX * format, GstAudioChannelPosition ** out_position) { guint i, ch; guint64 mask = 0; GstAudioChannelPosition *pos = NULL; WORD nChannels = 0; DWORD dwChannelMask = 0; nChannels = format->nChannels; if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { WAVEFORMATEXTENSIBLE *extensible = (WAVEFORMATEXTENSIBLE *) format; dwChannelMask = extensible->dwChannelMask; } if (out_position) *out_position = NULL; if (nChannels > 2 && !dwChannelMask) { GST_WARNING ("Unknown channel mask value for %d channel stream", nChannels); if (nChannels >= G_N_ELEMENTS (default_ch_masks)) { GST_ERROR ("To may channels %d", nChannels); return 0; } dwChannelMask = default_ch_masks[nChannels]; } 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 (i = 0, ch = 0; i < G_N_ELEMENTS (wasapi_to_gst_pos) && ch < nChannels; i++) { if (!(dwChannelMask & wasapi_to_gst_pos[i].wasapi_pos)) /* no match, try next */ continue; mask |= G_GUINT64_CONSTANT (1) << wasapi_to_gst_pos[i].gst_pos; pos[ch++] = wasapi_to_gst_pos[i].gst_pos; } /* XXX: Warn if some channel masks couldn't be mapped? */ GST_DEBUG ("Converted WASAPI mask 0x%" G_GINT64_MODIFIER "x -> 0x%" G_GINT64_MODIFIER "x", (guint64) dwChannelMask, (guint64) mask); out: if (out_position) { *out_position = pos; } else { g_free (pos); } return mask; } const gchar * gst_wasapi2_util_waveformatex_to_audio_format (WAVEFORMATEX * format) { const gchar *fmt_str = NULL; GstAudioFormat fmt = GST_AUDIO_FORMAT_UNKNOWN; switch (format->wFormatTag) { case WAVE_FORMAT_PCM: fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN, format->wBitsPerSample, format->wBitsPerSample); break; case WAVE_FORMAT_IEEE_FLOAT: if (format->wBitsPerSample == 32) fmt = GST_AUDIO_FORMAT_F32LE; else if (format->wBitsPerSample == 64) fmt = GST_AUDIO_FORMAT_F64LE; break; case WAVE_FORMAT_EXTENSIBLE: { WAVEFORMATEXTENSIBLE *ex = (WAVEFORMATEXTENSIBLE *) format; if (IsEqualGUID (&ex->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM)) { fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN, format->wBitsPerSample, ex->Samples.wValidBitsPerSample); } else if (IsEqualGUID (&ex->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { if (format->wBitsPerSample == 32 && ex->Samples.wValidBitsPerSample == 32) fmt = GST_AUDIO_FORMAT_F32LE; else if (format->wBitsPerSample == 64 && ex->Samples.wValidBitsPerSample == 64) fmt = GST_AUDIO_FORMAT_F64LE; } break; } default: break; } if (fmt != GST_AUDIO_FORMAT_UNKNOWN) fmt_str = gst_audio_format_to_string (fmt); return fmt_str; } gboolean gst_wasapi2_util_parse_waveformatex (WAVEFORMATEX * format, GstCaps * template_caps, GstCaps ** out_caps, GstAudioChannelPosition ** out_positions) { 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->wFormatTag != WAVE_FORMAT_PCM && format->wFormatTag != WAVE_FORMAT_IEEE_FLOAT && 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_wasapi2_util_waveformatex_to_audio_format (format); if (afmt == NULL) return FALSE; *out_caps = gst_caps_copy (template_caps); channel_mask = gst_wasapi2_util_waveformatex_to_channel_mask (format, out_positions); gst_caps_set_simple (*out_caps, "format", G_TYPE_STRING, afmt, "channels", G_TYPE_INT, format->nChannels, "rate", G_TYPE_INT, format->nSamplesPerSec, NULL); if (channel_mask) { gst_caps_set_simple (*out_caps, "channel-mask", GST_TYPE_BITMASK, channel_mask, NULL); } return TRUE; } gboolean gst_wasapi2_can_automatic_stream_routing (void) { #ifdef GST_WASAPI2_WINAPI_ONLY_APP /* Assume we are on very recent OS */ return TRUE; #else static gboolean ret = FALSE; static gsize version_once = 0; if (g_once_init_enter (&version_once)) { OSVERSIONINFOEXW osverinfo; typedef NTSTATUS (WINAPI fRtlGetVersion) (PRTL_OSVERSIONINFOEXW); fRtlGetVersion *RtlGetVersion = NULL; HMODULE hmodule = NULL; memset (&osverinfo, 0, sizeof (OSVERSIONINFOEXW)); osverinfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFOEXW); hmodule = LoadLibraryW (L"ntdll.dll"); if (hmodule) RtlGetVersion = (fRtlGetVersion *) GetProcAddress (hmodule, "RtlGetVersion"); if (RtlGetVersion) { RtlGetVersion (&osverinfo); /* automatic stream routing requires Windows 10 * Anniversary Update (version 1607, build number 14393.0) */ if (osverinfo.dwMajorVersion > 10 || (osverinfo.dwMajorVersion == 10 && osverinfo.dwBuildNumber >= 14393)) ret = TRUE; } if (hmodule) FreeLibrary (hmodule); g_once_init_leave (&version_once, 1); } GST_INFO ("Automatic stream routing support: %d", ret); return ret; #endif } gboolean gst_wasapi2_can_process_loopback (void) { #ifdef GST_WASAPI2_WINAPI_ONLY_APP /* FIXME: Needs WinRT (Windows.System.Profile) API call * for OS version check */ return FALSE; #else static gboolean ret = FALSE; static gsize version_once = 0; if (g_once_init_enter (&version_once)) { OSVERSIONINFOEXW osverinfo; typedef NTSTATUS (WINAPI fRtlGetVersion) (PRTL_OSVERSIONINFOEXW); fRtlGetVersion *RtlGetVersion = NULL; HMODULE hmodule = NULL; memset (&osverinfo, 0, sizeof (OSVERSIONINFOEXW)); osverinfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFOEXW); hmodule = LoadLibraryW (L"ntdll.dll"); if (hmodule) RtlGetVersion = (fRtlGetVersion *) GetProcAddress (hmodule, "RtlGetVersion"); if (RtlGetVersion) { RtlGetVersion (&osverinfo); /* Process loopback requires Windows 10 build 20348 * https://learn.microsoft.com/en-us/windows/win32/api/audioclientactivationparams/ns-audioclientactivationparams-audioclient_process_loopback_params * * Note: "Windows 10 build 20348" would mean "Windows server 2022" or * "Windows 11", since build number of "Windows 10 version 21H2" is * still 19044.XXX */ /* But other software enables this for build number 19041 or higher... */ if (osverinfo.dwMajorVersion > 10 || (osverinfo.dwMajorVersion == 10 && osverinfo.dwBuildNumber >= 19041)) ret = TRUE; } if (hmodule) FreeLibrary (hmodule); g_once_init_leave (&version_once, 1); } GST_INFO ("Process loopback support: %d", ret); return ret; #endif } WAVEFORMATEX * gst_wasapi2_get_default_mix_format (void) { WAVEFORMATEX *format; /* virtual loopback device might not provide mix format. Create our default * mix format */ format = CoTaskMemAlloc (sizeof (WAVEFORMATEX)); format->wFormatTag = WAVE_FORMAT_PCM; format->nChannels = 2; format->nSamplesPerSec = 44100; format->wBitsPerSample = 16; format->nBlockAlign = format->nChannels * format->wBitsPerSample / 8; format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign; return format; }