Files

593 lines
21 KiB
C++

/**
* @file tests/unit/platform/windows/test_audio.cpp
* @brief Tests for Windows audio sink selection and endpoint-change handling.
*/
// test includes
#include "../../../tests_common.h"
#ifdef _WIN32
// standard includes
#include <array>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <tuple>
#include <type_traits>
// platform includes
#include <mmdeviceapi.h>
#include <mmreg.h>
#include <propsys.h>
// local includes
#include "src/config.h"
#include "src/platform/common.h"
namespace platf::audio::tests {
std::vector<WAVEFORMATEXTENSIBLE> virtual_sink_waveformats(WORD channel_count);
int initialize_audio_control(const std::function<std::remove_pointer_t<decltype(&CoCreateInstance)>> &create_instance);
std::optional<sink_t> configured_sink_info();
int set_external_sink(const std::string &sink);
bool sink_device_available(const std::string &sink, IMMDeviceEnumerator *device_enum);
bool microphone_available(const std::string &assigned_sink, const std::string &configured_sink, IMMDeviceEnumerator *device_enum);
bool capture_follows_default_device(IMMDeviceEnumerator *device_enum, IMMDevice *capture_device);
capture_e simulate_default_device_change(bool follows_default_device, bool install_callback, bool render_device_changed, int &callback_count);
} // namespace platf::audio::tests
namespace {
class fake_property_store_t final: public IPropertyStore {
public:
explicit fake_property_store_t(std::wstring friendly_name):
friendly_name {std::move(friendly_name)} {
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface
*object = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return 1;
}
ULONG STDMETHODCALLTYPE Release() override {
return 1;
}
HRESULT STDMETHODCALLTYPE GetCount(DWORD *property_count) override {
*property_count = friendly_name.empty() ? 0 : 1;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetAt(DWORD, PROPERTYKEY *) override {
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY, PROPVARIANT *value) override {
if (friendly_name.empty()) {
return E_NOTIMPL;
}
const auto byte_count = (friendly_name.size() + 1) * sizeof(wchar_t);
value->pwszVal = static_cast<wchar_t *>(CoTaskMemAlloc(byte_count));
if (!value->pwszVal) {
return E_OUTOFMEMORY;
}
std::memcpy(value->pwszVal, friendly_name.c_str(), byte_count);
value->vt = VT_LPWSTR;
return S_OK;
}
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE Commit() override {
return E_NOTIMPL;
}
std::wstring friendly_name;
};
class fake_device_t final: public IMMDevice {
public:
fake_device_t(std::wstring id, std::wstring friendly_name):
id {std::move(id)},
properties {std::move(friendly_name)} {
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface
*object = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return 1;
}
ULONG STDMETHODCALLTYPE Release() override {
return 1;
}
HRESULT STDMETHODCALLTYPE Activate(REFIID, DWORD, PROPVARIANT *, void **) override { // NOSONAR(cpp:S5008): required by the Windows COM interface
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD, IPropertyStore **property_store) override {
properties.AddRef();
*property_store = &properties;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *device_id) override {
const auto byte_count = (id.size() + 1) * sizeof(wchar_t);
*device_id = static_cast<wchar_t *>(CoTaskMemAlloc(byte_count));
if (!*device_id) {
return E_OUTOFMEMORY;
}
std::memcpy(*device_id, id.c_str(), byte_count);
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetState(DWORD *device_state) override {
if (FAILED(state_status)) {
return state_status;
}
*device_state = state;
return S_OK;
}
std::wstring id;
fake_property_store_t properties;
HRESULT state_status = S_OK;
DWORD state = DEVICE_STATE_ACTIVE;
};
class fake_device_collection_t final: public IMMDeviceCollection {
public:
explicit fake_device_collection_t(fake_device_t &device):
device {device} {
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface
*object = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return 1;
}
ULONG STDMETHODCALLTYPE Release() override {
return 1;
}
HRESULT STDMETHODCALLTYPE GetCount(UINT *device_count) override {
*device_count = 1;
return S_OK;
}
HRESULT STDMETHODCALLTYPE Item(UINT index, IMMDevice **item) override {
if (index != 0) {
return E_INVALIDARG;
}
device.AddRef();
*item = &device;
return S_OK;
}
fake_device_t &device;
};
class fake_device_enumerator_t final: public IMMDeviceEnumerator {
public:
explicit fake_device_enumerator_t(std::wstring id, std::wstring friendly_name = {}):
device {std::move(id), std::move(friendly_name)},
collection {device} {
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void **object) override { // NOSONAR(cpp:S5008): required by the Windows COM interface
*object = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return 1;
}
ULONG STDMETHODCALLTYPE Release() override {
return 1;
}
HRESULT STDMETHODCALLTYPE EnumAudioEndpoints(EDataFlow, DWORD, IMMDeviceCollection **devices) override {
if (FAILED(enumeration_status)) {
return enumeration_status;
}
collection.AddRef();
*devices = &collection;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetDefaultAudioEndpoint(EDataFlow, ERole, IMMDevice **resolved_device) override {
++get_default_device_calls;
device.AddRef();
*resolved_device = &device;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetDevice(LPCWSTR device_id, IMMDevice **resolved_device) override {
++get_device_calls;
last_requested_id = device_id;
if (FAILED(get_device_status)) {
return get_device_status;
}
if (last_requested_id != device.id) {
return HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
}
device.AddRef();
*resolved_device = &device;
return S_OK;
}
HRESULT STDMETHODCALLTYPE RegisterEndpointNotificationCallback(IMMNotificationClient *) override {
return S_OK;
}
HRESULT STDMETHODCALLTYPE UnregisterEndpointNotificationCallback(IMMNotificationClient *) override {
return S_OK;
}
fake_device_t device;
fake_device_collection_t collection;
HRESULT enumeration_status = S_OK;
HRESULT get_device_status = S_OK;
int get_device_calls = 0;
int get_default_device_calls = 0;
std::wstring last_requested_id;
};
} // namespace
TEST(WindowsAudioTest, PreservesVirtualSpeakerFormatPreferenceAndChannelMasks) {
constexpr DWORD stereo_mask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
constexpr DWORD surround_back_mask = stereo_mask | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
constexpr DWORD surround_side_mask = stereo_mask | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT;
constexpr DWORD surround71_mask = surround_back_mask | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT;
const std::array stereo_samples {
std::tuple {32, 24, WAVE_FORMAT_PCM},
std::tuple {24, 24, WAVE_FORMAT_PCM},
std::tuple {16, 16, WAVE_FORMAT_PCM},
std::tuple {32, 32, WAVE_FORMAT_IEEE_FLOAT},
std::tuple {32, 32, WAVE_FORMAT_PCM},
};
const std::array surround_samples {
stereo_samples[3],
stereo_samples[4],
stereo_samples[0],
stereo_samples[1],
stereo_samples[2],
};
for (WORD channels : {2, 6, 8}) {
const auto formats = platf::audio::tests::virtual_sink_waveformats(channels);
const auto &samples = channels == 2 ? stereo_samples : surround_samples;
ASSERT_EQ(formats.size(), channels == 6 ? 10 : 5);
for (std::size_t i = 0; i < formats.size(); ++i) {
const auto &[storage_bits, valid_bits, subformat] = samples[channels == 6 ? i / 2 : i];
const auto &format = formats[i];
EXPECT_EQ(format.Format.wFormatTag, WAVE_FORMAT_EXTENSIBLE);
EXPECT_EQ(format.Format.nChannels, channels);
EXPECT_EQ(format.Format.nSamplesPerSec, 48000);
EXPECT_EQ(format.Format.wBitsPerSample, storage_bits);
EXPECT_EQ(format.Samples.wValidBitsPerSample, valid_bits);
EXPECT_EQ(format.SubFormat.Data1, subformat);
EXPECT_EQ(format.Format.nBlockAlign, channels * storage_bits / 8);
EXPECT_EQ(format.Format.nAvgBytesPerSec, 48000 * format.Format.nBlockAlign);
EXPECT_EQ(format.dwChannelMask, channels == 2 ? stereo_mask : channels == 8 ? surround71_mask :
i % 2 == 0 ? surround_back_mask :
surround_side_mask);
}
}
EXPECT_TRUE(platf::audio::tests::virtual_sink_waveformats(4).empty());
}
TEST(WindowsAudioTest, AssignedSinkTakesPriorityOverConfiguredSink) {
fake_device_enumerator_t enumerator {L"assigned-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available("assigned-id", "configured-id", &enumerator));
EXPECT_EQ(enumerator.get_device_calls, 1);
EXPECT_EQ(enumerator.last_requested_id, L"assigned-id");
}
TEST(WindowsAudioTest, ConfiguredSinkIsUsedWhenNoSinkWasAssigned) {
fake_device_enumerator_t enumerator {L"configured-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available({}, "configured-id", &enumerator));
EXPECT_EQ(enumerator.get_device_calls, 1);
EXPECT_EQ(enumerator.last_requested_id, L"configured-id");
}
TEST(WindowsAudioTest, DefaultDeviceIsUsedWhenNoSinkWasRequested) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
platf::audio::tests::microphone_available({}, {}, &enumerator);
EXPECT_EQ(enumerator.get_device_calls, 0);
}
/**
* @brief Preserve global audio configuration while exercising capture-only mode.
*/
class ExternalAudioTest: public testing::Test {
protected:
void SetUp() override {
previous = config::audio;
config::audio.external_audio = true;
config::audio.sink = "configured-id";
config::audio.virtual_sink = "ignored-virtual-sink";
policy_requests = 0;
enumerator_requests = 0;
enumerator_status = S_OK;
}
void TearDown() override {
config::audio = previous;
}
/**
* @brief Get the number of policy activation requests.
* @return Requests observed by the test factory.
*/
static int policy_request_count() {
return policy_requests;
}
/**
* @brief Get the number of endpoint enumeration activation requests.
* @return Requests observed by the test factory.
*/
static int enumerator_request_count() {
return enumerator_requests;
}
/**
* @brief Simulate failure to initialize endpoint enumeration.
*/
static void fail_enumerator_initialization() {
enumerator_status = E_FAIL;
}
/**
* @brief Supply endpoint enumeration while simulating an unavailable policy interface.
* @param class_id Requested COM class.
* @param outer Unused aggregation pointer.
* @param context Unused activation context.
* @param interface_id Unused interface identifier.
* @param object Receives the enumerator on success.
* @return Simulated COM activation status.
*/
static HRESULT WINAPI create_capture_interface(REFCLSID class_id, [[maybe_unused]] LPUNKNOWN outer, [[maybe_unused]] DWORD context, [[maybe_unused]] REFIID interface_id, LPVOID *object) {
*object = nullptr;
if (class_id != CLSID_MMDeviceEnumerator) {
++policy_requests;
return REGDB_E_CLASSNOTREG;
}
++enumerator_requests;
if (FAILED(enumerator_status)) {
return enumerator_status;
}
static fake_device_enumerator_t enumerator {L"configured-id"};
enumerator.AddRef();
*object = static_cast<IMMDeviceEnumerator *>(&enumerator);
return S_OK;
}
private:
config::audio_t previous; ///< Configuration restored after each test.
inline static int policy_requests = 0; ///< Requests for the unavailable policy interface.
inline static int enumerator_requests = 0; ///< Requests for endpoint enumeration.
inline static HRESULT enumerator_status = S_OK; ///< Simulated enumeration initialization result.
};
TEST_F(ExternalAudioTest, InitializesWithoutPolicyInterface) {
EXPECT_EQ(platf::audio::tests::initialize_audio_control(&create_capture_interface), 0);
EXPECT_EQ(policy_request_count(), 0);
EXPECT_EQ(enumerator_request_count(), 1);
}
TEST_F(ExternalAudioTest, InitializationRequiresEndpointEnumerator) {
fail_enumerator_initialization();
EXPECT_NE(platf::audio::tests::initialize_audio_control(&create_capture_interface), 0);
EXPECT_EQ(policy_request_count(), 0);
EXPECT_EQ(enumerator_request_count(), 1);
}
TEST_F(ExternalAudioTest, DisabledModeStillRequiresPolicyInterface) {
config::audio.external_audio = false;
EXPECT_NE(platf::audio::tests::initialize_audio_control(&create_capture_interface), 0);
EXPECT_EQ(policy_request_count(), 1);
EXPECT_EQ(enumerator_request_count(), 0);
}
TEST_F(ExternalAudioTest, RequiresExplicitSink) {
config::audio.sink.clear();
EXPECT_EXIT(std::exit(platf::audio::tests::configured_sink_info() ? 1 : 0), testing::ExitedWithCode(0), "");
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available({}, {}, &enumerator));
EXPECT_EQ(enumerator.get_default_device_calls, 0);
EXPECT_EQ(enumerator.get_device_calls, 0);
}
TEST_F(ExternalAudioTest, SkipsDefaultAndVirtualDeviceDiscovery) {
EXPECT_EXIT(
{
const auto sinks = platf::audio::tests::configured_sink_info();
std::exit(sinks && sinks->host == "configured-id" && !sinks->null ? 0 : 1);
},
testing::ExitedWithCode(0),
""
);
}
TEST_F(ExternalAudioTest, SinkChangesDoNotAccessPolicyOrFormatInterfaces) {
EXPECT_EXIT(std::exit(platf::audio::tests::set_external_sink("virtual-Stereoendpoint-id")), testing::ExitedWithCode(0), "");
EXPECT_EXIT(std::exit(platf::audio::tests::set_external_sink("other-endpoint")), testing::ExitedWithCode(0), "");
}
TEST_F(ExternalAudioTest, ConfiguredSinkWinsOverAssignedSink) {
fake_device_enumerator_t enumerator {L"configured-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available("virtual-Stereoother-id", "configured-id", &enumerator));
EXPECT_EQ(enumerator.last_requested_id, L"configured-id");
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST_F(ExternalAudioTest, MissingSinkDoesNotFallBackToDefault) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available({}, "missing-id", &enumerator));
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST_F(ExternalAudioTest, DisconnectedSinkDoesNotFallBackToDefault) {
fake_device_enumerator_t enumerator {L"configured-id"};
enumerator.device.state = DEVICE_STATE_UNPLUGGED;
EXPECT_FALSE(platf::audio::tests::microphone_available({}, "configured-id", &enumerator));
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST_F(ExternalAudioTest, DisabledModePreservesAssignedSinkPriority) {
config::audio.external_audio = false;
fake_device_enumerator_t enumerator {L"assigned-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available("assigned-id", "configured-id", &enumerator));
EXPECT_EQ(enumerator.last_requested_id, L"assigned-id");
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST_F(ExternalAudioTest, DisabledModePreservesDefaultFallback) {
config::audio.external_audio = false;
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_TRUE(platf::audio::tests::capture_follows_default_device(&enumerator, nullptr));
EXPECT_EQ(enumerator.get_default_device_calls, 1);
EXPECT_EQ(enumerator.get_device_calls, 0);
}
TEST_F(ExternalAudioTest, ReactivatedSinkCanBeResolvedAgain) {
fake_device_enumerator_t enumerator {L"configured-id"};
enumerator.device.state = DEVICE_STATE_UNPLUGGED;
EXPECT_FALSE(platf::audio::tests::sink_device_available("configured-id", &enumerator));
enumerator.device.state = DEVICE_STATE_ACTIVE;
EXPECT_TRUE(platf::audio::tests::sink_device_available("configured-id", &enumerator));
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST(WindowsAudioTest, DefaultCaptureSelectsDefaultEndpoint) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_TRUE(platf::audio::tests::capture_follows_default_device(&enumerator, nullptr));
EXPECT_EQ(enumerator.get_default_device_calls, 1);
}
TEST(WindowsAudioTest, PinnedCaptureKeepsExplicitEndpoint) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_FALSE(platf::audio::tests::capture_follows_default_device(&enumerator, &enumerator.device));
EXPECT_EQ(enumerator.get_default_device_calls, 0);
}
TEST(WindowsAudioTest, MicrophoneRejectsUnresolvedSink) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_FALSE(platf::audio::tests::microphone_available("unknown", {}, &enumerator));
EXPECT_EQ(enumerator.get_device_calls, 0);
}
TEST(WindowsAudioTest, ResolvesVirtualSinkDescriptorToActiveEndpoint) {
fake_device_enumerator_t enumerator {L"endpoint-id"};
EXPECT_TRUE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &enumerator));
EXPECT_EQ(enumerator.get_device_calls, 1);
EXPECT_EQ(enumerator.last_requested_id, L"endpoint-id");
}
TEST(WindowsAudioTest, ResolvesDeviceIdentifiersAndFriendlyNames) {
fake_device_enumerator_t id_enumerator {L"endpoint-id"};
EXPECT_TRUE(platf::audio::tests::sink_device_available("endpoint-id", &id_enumerator));
EXPECT_EQ(id_enumerator.get_device_calls, 1);
EXPECT_EQ(id_enumerator.last_requested_id, L"endpoint-id");
fake_device_enumerator_t name_enumerator {L"endpoint-id", L"Friendly Endpoint"};
EXPECT_TRUE(platf::audio::tests::sink_device_available("Friendly Endpoint", &name_enumerator));
EXPECT_EQ(name_enumerator.get_device_calls, 1);
EXPECT_EQ(name_enumerator.last_requested_id, L"endpoint-id");
}
TEST(WindowsAudioTest, RejectsUnknownOrUnenumerableSinks) {
fake_device_enumerator_t unknown_enumerator {L"endpoint-id"};
EXPECT_FALSE(platf::audio::tests::sink_device_available("unknown", &unknown_enumerator));
EXPECT_EQ(unknown_enumerator.get_device_calls, 0);
fake_device_enumerator_t failed_enumerator {L"endpoint-id"};
failed_enumerator.enumeration_status = E_FAIL;
EXPECT_FALSE(platf::audio::tests::sink_device_available("endpoint-id", &failed_enumerator));
EXPECT_EQ(failed_enumerator.get_device_calls, 0);
}
TEST(WindowsAudioTest, RejectsUnavailableResolvedEndpoints) {
fake_device_enumerator_t missing_enumerator {L"endpoint-id"};
missing_enumerator.get_device_status = E_FAIL;
EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &missing_enumerator));
fake_device_enumerator_t state_failure_enumerator {L"endpoint-id"};
state_failure_enumerator.device.state_status = E_FAIL;
EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &state_failure_enumerator));
fake_device_enumerator_t inactive_enumerator {L"endpoint-id"};
inactive_enumerator.device.state = DEVICE_STATE_DISABLED;
EXPECT_FALSE(platf::audio::tests::sink_device_available("virtual-Stereoendpoint-id", &inactive_enumerator));
}
TEST(WindowsAudioTest, DefaultFollowingCaptureReinitializesAfterRenderEndpointChange) {
int callback_count = 0;
EXPECT_EQ(
platf::audio::tests::simulate_default_device_change(true, true, true, callback_count),
platf::capture_e::reinit
);
EXPECT_EQ(callback_count, 1);
}
TEST(WindowsAudioTest, PinnedCaptureContinuesAfterRenderEndpointChange) {
int callback_count = 0;
EXPECT_EQ(
platf::audio::tests::simulate_default_device_change(false, true, true, callback_count),
platf::capture_e::timeout
);
EXPECT_EQ(callback_count, 1);
}
TEST(WindowsAudioTest, CaptureEndpointChangeDoesNotTriggerRenderCallback) {
int callback_count = 0;
EXPECT_EQ(
platf::audio::tests::simulate_default_device_change(false, true, false, callback_count),
platf::capture_e::timeout
);
EXPECT_EQ(callback_count, 0);
}
TEST(WindowsAudioTest, DefaultChangeWithoutCallbackStillReinitializes) {
int callback_count = 0;
EXPECT_EQ(
platf::audio::tests::simulate_default_device_change(true, false, true, callback_count),
platf::capture_e::reinit
);
EXPECT_EQ(callback_count, 0);
}
#endif