You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
548 lines
23 KiB
C++
548 lines
23 KiB
C++
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <csignal>
|
|
#include <cstdlib>
|
|
#include <fcntl.h>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <spawn.h>
|
|
#include <string>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
#include <spa/param/audio/raw.h>
|
|
#include <spa/param/profile.h>
|
|
#include <spa/param/props.h>
|
|
#include <spa/param/route.h>
|
|
#include <spa/pod/builder.h>
|
|
|
|
#include "audio/audiomodel.h"
|
|
#include "audio/backend.h"
|
|
#include "audio/volumemonitor.h"
|
|
#include "base/event_loop.h"
|
|
#include "base/timer.h"
|
|
#include "fakeaudiodevice.h"
|
|
#include "teststream.h"
|
|
|
|
extern char** environ;
|
|
|
|
using namespace cutefish::audio;
|
|
using namespace cutefish::base;
|
|
using namespace std::chrono_literals;
|
|
|
|
// Runs the loop until the condition holds or the time is up.
|
|
static bool waitFor(EventLoop& loop, const std::function<bool()>& condition, std::chrono::milliseconds timeout = 5s)
|
|
{
|
|
if (condition())
|
|
return true;
|
|
Timer poll(&loop);
|
|
poll.onTimeout = [&] {
|
|
if (condition())
|
|
loop.quit();
|
|
};
|
|
Timer limit(&loop);
|
|
limit.setSingleShot(true);
|
|
limit.onTimeout = [&loop] { loop.quit(); };
|
|
poll.start(5ms);
|
|
limit.start(timeout);
|
|
loop.run();
|
|
return condition();
|
|
}
|
|
|
|
static void wait(EventLoop& loop, std::chrono::milliseconds time)
|
|
{
|
|
waitFor(loop, [] { return false; }, time);
|
|
}
|
|
|
|
static bool near(double a, double b)
|
|
{
|
|
return std::abs(a - b) < 0.0001;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// A program run beside the test, its output into a log file.
|
|
class Process
|
|
{
|
|
public:
|
|
~Process() { stop(); }
|
|
|
|
bool start(const std::string& program, const std::vector<std::string>& arguments, const std::string& log)
|
|
{
|
|
posix_spawn_file_actions_t actions;
|
|
posix_spawn_file_actions_init(&actions);
|
|
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, log.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644);
|
|
posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO);
|
|
std::vector<char*> argv{const_cast<char*>(program.c_str())};
|
|
for (const std::string& argument : arguments)
|
|
argv.push_back(const_cast<char*>(argument.c_str()));
|
|
argv.push_back(nullptr);
|
|
const int result = posix_spawn(&m_pid, program.c_str(), &actions, nullptr, argv.data(), environ);
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
if (result != 0)
|
|
m_pid = 0;
|
|
return m_pid != 0;
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (!m_pid)
|
|
return;
|
|
::kill(m_pid, SIGTERM);
|
|
for (int i = 0; i < 300; ++i) {
|
|
if (::waitpid(m_pid, nullptr, WNOHANG) == m_pid) {
|
|
m_pid = 0;
|
|
return;
|
|
}
|
|
::usleep(10000);
|
|
}
|
|
::kill(m_pid, SIGKILL);
|
|
::waitpid(m_pid, nullptr, 0);
|
|
m_pid = 0;
|
|
}
|
|
|
|
static int run(const std::string& program, const std::vector<std::string>& arguments)
|
|
{
|
|
Process process;
|
|
if (!process.start(program, arguments, "/dev/null"))
|
|
return -1;
|
|
int status = 0;
|
|
::waitpid(process.m_pid, &status, 0);
|
|
process.m_pid = 0;
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
|
}
|
|
|
|
private:
|
|
pid_t m_pid = 0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// A private PipeWire and WirePlumber with two sinks and a source, shared by
|
|
// the tests in their order, as the daemon's state carries from one to the next.
|
|
class AudioTest : public testing::Test
|
|
{
|
|
protected:
|
|
static void SetUpTestSuite()
|
|
{
|
|
char directory[] = "/tmp/cutefish-audio-test-XXXXXX";
|
|
runtime = ::mkdtemp(directory);
|
|
// The PipeWire libraries and daemons find each other through these.
|
|
::setenv("XDG_RUNTIME_DIR", runtime.c_str(), 1);
|
|
::setenv("PIPEWIRE_RUNTIME_DIR", runtime.c_str(), 1);
|
|
::setenv("PIPEWIRE_REMOTE", "cutefish-audio-test", 1);
|
|
::setenv("XDG_CONFIG_HOME", (runtime + "/config").c_str(), 1);
|
|
::setenv("XDG_STATE_HOME", (runtime + "/state").c_str(), 1);
|
|
::setenv("XDG_CACHE_HOME", (runtime + "/cache").c_str(), 1);
|
|
::unsetenv("PIPEWIRE_CONFIG_NAME");
|
|
::unsetenv("PIPEWIRE_CONFIG_DIR");
|
|
::unsetenv("WIREPLUMBER_CONFIG_DIR");
|
|
const std::filesystem::path fragments = runtime + "/config/wireplumber/wireplumber.conf.d";
|
|
std::filesystem::create_directories(fragments);
|
|
std::filesystem::copy_file(WIREPLUMBER_TEST_CONFIG, fragments / "50-cutefish-test.conf");
|
|
loop = new EventLoop;
|
|
backend = Backend::shared();
|
|
wait(*loop, 50ms);
|
|
ASSERT_FALSE(backend->isConnected());
|
|
startServers();
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected(); }, 10s));
|
|
ASSERT_TRUE(waitFor(*loop, [] { return find("test-output-a") && find("test-output-b") && find("test-input"); },
|
|
10s));
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->defaultNode(AudioObject::Sink) != nullptr; }, 10s));
|
|
activateDevices();
|
|
}
|
|
|
|
static void TearDownTestSuite()
|
|
{
|
|
drivers.clear();
|
|
backend.reset();
|
|
stopServers();
|
|
delete loop;
|
|
loop = nullptr;
|
|
std::error_code error;
|
|
std::filesystem::remove_all(runtime, error);
|
|
}
|
|
|
|
static void startServers()
|
|
{
|
|
const std::string log = runtime + "/servers.log";
|
|
ASSERT_TRUE(server.start(PIPEWIRE_EXECUTABLE, {"-c", AUDIO_TEST_CONFIG}, log));
|
|
ASSERT_TRUE(waitFor(*loop, [] { return std::filesystem::exists(runtime + "/cutefish-audio-test"); }));
|
|
ASSERT_TRUE(policy.start(WIREPLUMBER_EXECUTABLE, {"--profile=policy"}, log));
|
|
}
|
|
|
|
static void stopServers()
|
|
{
|
|
drivers.clear();
|
|
policy.stop();
|
|
server.stop();
|
|
}
|
|
|
|
static void activateDevices()
|
|
{
|
|
for (const char* name : {"test-output-a", "test-output-b", "test-input"}) {
|
|
ASSERT_TRUE(waitFor(*loop, [name] { return find(name) != nullptr; }, 10s));
|
|
const auto node = find(name);
|
|
auto stream = std::make_unique<TestStream>(backend->core(), std::string("cutefish.internal.test-") + name,
|
|
node->property(PW_KEY_OBJECT_SERIAL),
|
|
node->kind() == AudioObject::Source);
|
|
ASSERT_TRUE(stream->connected);
|
|
TestStream* added = stream.get();
|
|
ASSERT_TRUE(waitFor(*loop, [added] { return added->streaming; }));
|
|
drivers.push_back(std::move(stream));
|
|
}
|
|
}
|
|
|
|
static std::shared_ptr<AudioObject> find(const std::string& name)
|
|
{
|
|
for (const auto& object : backend->objects()) {
|
|
if (object->name() == name)
|
|
return object;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static bool until(const std::function<bool()>& condition) { return waitFor(*loop, condition); }
|
|
|
|
static inline std::string runtime;
|
|
static inline EventLoop* loop = nullptr;
|
|
static inline std::shared_ptr<Backend> backend;
|
|
static inline Process server;
|
|
static inline Process policy;
|
|
static inline std::vector<std::unique_ptr<TestStream>> drivers;
|
|
};
|
|
|
|
TEST_F(AudioTest, VolumeScale)
|
|
{
|
|
EXPECT_EQ(AudioObject::fromLinear(0.125f), 0.5);
|
|
EXPECT_EQ(AudioObject::toLinear(0.5), 0.125f);
|
|
EXPECT_EQ(AudioObject::toLinear(-1.0), 0.0f);
|
|
EXPECT_EQ(AudioObject::toLinear(100.0), 3.375f);
|
|
EXPECT_EQ(AudioObject::toLinear(std::numeric_limits<double>::infinity()), 0.0f);
|
|
EXPECT_EQ(AudioObject::fromLinear(std::numeric_limits<float>::quiet_NaN()), 0.0);
|
|
for (double value : {0.0, 0.01, 0.25, 0.75, 1.0, 1.5})
|
|
EXPECT_LT(std::abs(AudioObject::fromLinear(AudioObject::toLinear(value)) - value), 0.000001);
|
|
}
|
|
|
|
TEST_F(AudioTest, ParseProperties)
|
|
{
|
|
AudioObject object(nullptr, 42, AudioObject::Sink);
|
|
EXPECT_FALSE(object.hasVolume());
|
|
EXPECT_EQ(object.activePortIndex(), -1);
|
|
uint8_t buffer[1024];
|
|
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
const float volumes[] = {0.125f, 1.0f};
|
|
const uint32_t channels[] = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR};
|
|
const auto* param = static_cast<spa_pod*>(spa_pod_builder_add_object(
|
|
&builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, SPA_PROP_channelVolumes,
|
|
SPA_POD_Array(sizeof(float), SPA_TYPE_Float, 2, volumes), SPA_PROP_channelMap,
|
|
SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, 2, channels), SPA_PROP_mute, SPA_POD_Bool(true)));
|
|
object.updateVolume(param);
|
|
EXPECT_TRUE(object.hasVolume());
|
|
EXPECT_TRUE(object.muted());
|
|
EXPECT_EQ(object.volume(), 1.0);
|
|
EXPECT_EQ(object.channelVolumes(), (std::vector<double>{0.5, 1.0}));
|
|
EXPECT_EQ(object.channels(), (std::vector<std::string>{"FL", "FR"}));
|
|
object.setChannelVolume(-1, 0.5);
|
|
object.setChannelVolume(2, 0.5);
|
|
object.updateVolume(nullptr);
|
|
EXPECT_EQ(object.volume(), 1.0);
|
|
|
|
const spa_dict_item items[] = {{"node.name", "test"}, {"node.description", "Test speaker"}};
|
|
const spa_dict properties = SPA_DICT_INIT(items, 2);
|
|
object.updateProperties(&properties);
|
|
EXPECT_EQ(object.description(), "Test speaker");
|
|
EXPECT_EQ(object.displayName(), "Test speaker");
|
|
EXPECT_EQ(object.cardIndex(), AudioObject::InvalidId);
|
|
}
|
|
|
|
TEST_F(AudioTest, ParseProfilesAndRoutes)
|
|
{
|
|
AudioObject card(nullptr, 99, AudioObject::Card);
|
|
uint8_t buffer[1024];
|
|
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
const auto* profile = static_cast<spa_pod*>(spa_pod_builder_add_object(
|
|
&builder, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_EnumProfile, SPA_PARAM_PROFILE_index, SPA_POD_Int(7),
|
|
SPA_PARAM_PROFILE_name, SPA_POD_String("stereo"), SPA_PARAM_PROFILE_description, SPA_POD_String("Stereo"),
|
|
SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes)));
|
|
card.updateProfile(profile, false);
|
|
card.updateProfile(profile, false);
|
|
card.updateProfile(profile, true);
|
|
ASSERT_EQ(card.profiles().size(), 1u);
|
|
EXPECT_EQ(card.activeProfileIndex(), 0);
|
|
EXPECT_EQ(card.profiles().front().availability, AudioObject::Available);
|
|
builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
const int devices[] = {3};
|
|
const int profiles[] = {7};
|
|
const auto* route = static_cast<spa_pod*>(spa_pod_builder_add_object(
|
|
&builder, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_EnumRoute, SPA_PARAM_ROUTE_index, SPA_POD_Int(9),
|
|
SPA_PARAM_ROUTE_device, SPA_POD_Int(3), SPA_PARAM_ROUTE_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
|
|
SPA_PARAM_ROUTE_name, SPA_POD_String("headphones"), SPA_PARAM_ROUTE_devices,
|
|
SPA_POD_Array(sizeof(int), SPA_TYPE_Int, 1, devices), SPA_PARAM_ROUTE_profiles,
|
|
SPA_POD_Array(sizeof(int), SPA_TYPE_Int, 1, profiles), SPA_PARAM_ROUTE_available,
|
|
SPA_POD_Id(SPA_PARAM_AVAILABILITY_no)));
|
|
card.updateRoute(route, false);
|
|
card.updateRoute(route, true);
|
|
ASSERT_EQ(card.routes().size(), 1u);
|
|
EXPECT_EQ(card.activeRoutes().at(3), 9);
|
|
EXPECT_EQ(card.routes().front().availability, AudioObject::Unavailable);
|
|
}
|
|
|
|
TEST_F(AudioTest, DiscoveryAndModels)
|
|
{
|
|
AudioModel sinks(AudioObject::Sink, backend);
|
|
AudioModel sources(AudioObject::Source, backend);
|
|
EXPECT_EQ(sinks.count(), 2);
|
|
EXPECT_EQ(sources.count(), 1);
|
|
EXPECT_EQ(sources.objects().front()->name(), "test-input");
|
|
EXPECT_EQ(sinks.defaultNode(), backend->defaultNode(AudioObject::Sink));
|
|
}
|
|
|
|
TEST_F(AudioTest, VolumeAndMute)
|
|
{
|
|
const auto sink = find("test-output-a");
|
|
ASSERT_TRUE(until([&] { return sink->volumeWritable(); }));
|
|
sink->setVolume(0.42);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.42); }));
|
|
sink->setMuted(true);
|
|
ASSERT_TRUE(until([&] { return sink->muted(); }));
|
|
sink->setMuted(false);
|
|
ASSERT_TRUE(until([&] { return !sink->muted(); }));
|
|
sink->setChannelVolumes({0.25, 0.5});
|
|
ASSERT_TRUE(until([&] { return near(sink->channelVolumes().at(0), 0.25); }));
|
|
sink->setVolume(1.0);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 1.0); }));
|
|
EXPECT_TRUE(near(sink->channelVolumes().at(0), 0.5));
|
|
sink->setVolume(std::numeric_limits<double>::quiet_NaN());
|
|
sink->setChannelVolumes({0.1});
|
|
sink->setChannelVolume(-1, 0.0);
|
|
wait(*loop, 30ms);
|
|
EXPECT_TRUE(near(sink->volume(), 1.0));
|
|
sink->setVolume(-1.0);
|
|
ASSERT_TRUE(until([&] { return sink->volume() == 0.0; }));
|
|
sink->setVolume(5.0);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 1.5); }));
|
|
sink->setVolume(0.5);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.5); }));
|
|
const auto source = find("test-input");
|
|
ASSERT_TRUE(until([&] { return source->volumeWritable(); }));
|
|
source->setMuted(true);
|
|
ASSERT_TRUE(until([&] { return source->muted(); }));
|
|
source->setMuted(false);
|
|
ASSERT_TRUE(until([&] { return !source->muted(); }));
|
|
}
|
|
|
|
TEST_F(AudioTest, ExternalChanges)
|
|
{
|
|
const auto sink = find("test-output-a");
|
|
ASSERT_EQ(Process::run(PW_CLI_EXECUTABLE, {"set-param", std::to_string(sink->index()), "Props",
|
|
"{ channelVolumes: [ 0.064 0.064 ], mute: true }"}),
|
|
0);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.4); }));
|
|
ASSERT_TRUE(until([&] { return sink->muted(); }));
|
|
sink->setMuted(false);
|
|
ASSERT_TRUE(until([&] { return !sink->muted(); }));
|
|
}
|
|
|
|
TEST_F(AudioTest, DefaultDevices)
|
|
{
|
|
const auto first = find("test-output-a");
|
|
const auto second = find("test-output-b");
|
|
second->setDefault(true);
|
|
ASSERT_TRUE(until([&] { return backend->defaultNode(AudioObject::Sink) == second; }));
|
|
EXPECT_TRUE(second->isDefault());
|
|
EXPECT_FALSE(first->isDefault());
|
|
first->setDefault(true);
|
|
ASSERT_TRUE(until([&] { return backend->defaultNode(AudioObject::Sink) == first; }));
|
|
find("test-input")->setDefault(true);
|
|
ASSERT_TRUE(until([&] { return backend->defaultNode(AudioObject::Source) == find("test-input"); }));
|
|
}
|
|
|
|
TEST_F(AudioTest, Hotplug)
|
|
{
|
|
AudioModel sinks(AudioObject::Sink, backend);
|
|
auto* properties = pw_properties_new("factory.name", "support.null-audio-sink", PW_KEY_NODE_NAME, "hotplug-output",
|
|
PW_KEY_MEDIA_CLASS, "Audio/Sink", "audio.position", "[ FL FR ]", nullptr);
|
|
auto* proxy = static_cast<pw_proxy*>(
|
|
pw_core_create_object(backend->core(), "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
|
|
pw_properties_free(properties);
|
|
ASSERT_TRUE(proxy);
|
|
ASSERT_TRUE(until([&] { return sinks.count() == 3; }));
|
|
std::weak_ptr<AudioObject> device = find("hotplug-output");
|
|
ASSERT_FALSE(device.expired());
|
|
device.lock()->setDefault(true);
|
|
ASSERT_TRUE(until([&] { return backend->defaultNode(AudioObject::Sink) == device.lock(); }));
|
|
pw_proxy_destroy(proxy);
|
|
ASSERT_TRUE(until([&] { return sinks.count() == 2; }));
|
|
ASSERT_TRUE(until([&] { return device.expired(); }));
|
|
ASSERT_TRUE(until([&] { return backend->defaultNode(AudioObject::Sink) != nullptr; }));
|
|
}
|
|
|
|
TEST_F(AudioTest, HardwareRoutes)
|
|
{
|
|
FakeAudioDevice device;
|
|
ASSERT_TRUE(device.exportTo(backend->core()));
|
|
ASSERT_TRUE(until([] { return find("test-card") != nullptr; }));
|
|
const auto card = find("test-card");
|
|
ASSERT_TRUE(until([&] { return card->profiles().size() == 2; }));
|
|
ASSERT_TRUE(until([&] { return card->activeProfileIndex() >= 0; }));
|
|
card->setActiveProfileIndex(1);
|
|
ASSERT_TRUE(until([&] { return device.profile == 8; }));
|
|
ASSERT_TRUE(until([&] { return card->activeProfileIndex() == 1; }));
|
|
EXPECT_TRUE(device.saved);
|
|
|
|
const std::string cardId = std::to_string(card->index());
|
|
auto* properties = pw_properties_new("factory.name", "support.null-audio-sink", PW_KEY_NODE_NAME, "test-card-output",
|
|
PW_KEY_MEDIA_CLASS, "Audio/Sink", PW_KEY_DEVICE_ID, cardId.c_str(),
|
|
"card.profile.device", "3", "audio.position", "[ FL FR ]", nullptr);
|
|
auto* proxy = static_cast<pw_proxy*>(
|
|
pw_core_create_object(backend->core(), "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
|
|
pw_properties_free(properties);
|
|
ASSERT_TRUE(proxy);
|
|
ASSERT_TRUE(until([] { return find("test-card-output") != nullptr; }));
|
|
const auto sink = find("test-card-output");
|
|
ASSERT_TRUE(until([&] { return sink->volumeWritable(); }));
|
|
ASSERT_TRUE(until([&] { return sink->ports().size() == 2; }));
|
|
sink->setActivePortIndex(1);
|
|
ASSERT_TRUE(until([&] { return device.route == 10; }));
|
|
ASSERT_TRUE(until([&] { return sink->activePortIndex() == 1; }));
|
|
EXPECT_EQ(sink->displayName(), "Headphones");
|
|
sink->setChannelVolumes({0.5, 0.5});
|
|
ASSERT_TRUE(until([&] { return device.volumes[0] == 0.125f && device.volumes[1] == 0.125f; }));
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.5); }));
|
|
EXPECT_TRUE(device.saved);
|
|
sink->setVolume(0.25);
|
|
ASSERT_TRUE(until([&] { return device.volumes[0] == 0.015625f; }));
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.25); }));
|
|
sink->setMuted(true);
|
|
ASSERT_TRUE(until([&] { return device.muted; }));
|
|
ASSERT_TRUE(until([&] { return sink->muted(); }));
|
|
const int writes = device.writes;
|
|
sink->setActivePortIndex(-1);
|
|
card->setActiveProfileIndex(999);
|
|
wait(*loop, 30ms);
|
|
EXPECT_EQ(device.writes, writes);
|
|
|
|
device.volumes[0] = 0.064f;
|
|
device.volumes[1] = 0.064f;
|
|
device.muted = false;
|
|
device.notify();
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.4); }));
|
|
ASSERT_TRUE(until([&] { return !sink->muted(); }));
|
|
pw_proxy_destroy(proxy);
|
|
ASSERT_TRUE(until([] { return find("test-card-output") == nullptr; }));
|
|
}
|
|
|
|
TEST_F(AudioTest, StreamRouting)
|
|
{
|
|
const auto first = find("test-output-a");
|
|
const auto second = find("test-output-b");
|
|
TestStream playback(backend->core(), "test-playback", first->property(PW_KEY_OBJECT_SERIAL));
|
|
ASSERT_TRUE(playback.connected);
|
|
ASSERT_TRUE(until([&] { return playback.streaming; }));
|
|
ASSERT_TRUE(until([] { return find("test-playback") != nullptr; }));
|
|
const auto stream = find("test-playback");
|
|
EXPECT_EQ(stream->kind(), AudioObject::Playback);
|
|
ASSERT_TRUE(until([&] { return stream->deviceIndex() == first->index(); }));
|
|
ASSERT_TRUE(until([&] { return stream->volumeWritable(); }));
|
|
stream->setVolume(0.25);
|
|
ASSERT_TRUE(until([&] { return near(stream->volume(), 0.25); }));
|
|
stream->setMuted(true);
|
|
ASSERT_TRUE(until([&] { return stream->muted(); }));
|
|
stream->setDeviceIndex(second->index());
|
|
ASSERT_TRUE(until([&] { return stream->deviceIndex() == second->index(); }));
|
|
stream->setDeviceIndex(find("test-input")->index());
|
|
wait(*loop, 30ms);
|
|
EXPECT_EQ(stream->deviceIndex(), second->index());
|
|
first->switchStreams();
|
|
ASSERT_TRUE(until([&] { return stream->deviceIndex() == first->index(); }));
|
|
|
|
TestStream recording(backend->core(), "test-recording", find("test-input")->property(PW_KEY_OBJECT_SERIAL), true);
|
|
ASSERT_TRUE(recording.connected);
|
|
ASSERT_TRUE(until([&] { return recording.streaming; }));
|
|
ASSERT_TRUE(until([] { return find("test-recording") != nullptr; }));
|
|
EXPECT_EQ(find("test-recording")->kind(), AudioObject::Capture);
|
|
ASSERT_TRUE(until([] { return find("test-recording")->deviceIndex() == find("test-input")->index(); }));
|
|
|
|
auto* properties = pw_properties_new("factory.name", "audiotestsrc", PW_KEY_NODE_NAME, "second-input",
|
|
PW_KEY_MEDIA_CLASS, "Audio/Source", "audio.position", "[ FL FR ]", nullptr);
|
|
auto* proxy = static_cast<pw_proxy*>(
|
|
pw_core_create_object(backend->core(), "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
|
|
pw_properties_free(properties);
|
|
ASSERT_TRUE(proxy);
|
|
ASSERT_TRUE(until([] { return find("second-input") != nullptr; }));
|
|
find("second-input")->setDefault(true);
|
|
ASSERT_TRUE(until([] { return backend->defaultNode(AudioObject::Source) == find("second-input"); }));
|
|
find("test-recording")->setDeviceIndex(find("second-input")->index());
|
|
ASSERT_TRUE(until([] { return find("test-recording")->deviceIndex() == find("second-input")->index(); }));
|
|
find("test-recording")->setDeviceIndex(find("test-input")->index());
|
|
ASSERT_TRUE(until([] { return find("test-recording")->deviceIndex() == find("test-input")->index(); }));
|
|
pw_proxy_destroy(proxy);
|
|
ASSERT_TRUE(until([] { return find("second-input") == nullptr; }));
|
|
ASSERT_TRUE(until([] { return backend->defaultNode(AudioObject::Source) == find("test-input"); }));
|
|
}
|
|
|
|
TEST_F(AudioTest, Monitor)
|
|
{
|
|
const auto sink = find("test-output-a");
|
|
sink->setVolume(1.0);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 1.0); }));
|
|
VolumeMonitor monitor(backend);
|
|
monitor.setTarget(sink);
|
|
double peak = 0.0;
|
|
const auto subscription = monitor.volumeChanged.connect([&] { peak = std::max(peak, monitor.volume()); });
|
|
TestStream tone(backend->core(), "test-tone", sink->property(PW_KEY_OBJECT_SERIAL), false, 0.5f);
|
|
ASSERT_TRUE(tone.connected);
|
|
ASSERT_TRUE(waitFor(*loop, [&] { return monitor.isAvailable(); }, 2s));
|
|
ASSERT_TRUE(waitFor(*loop, [&] { return peak > 0.01; }, 2s));
|
|
monitor.setTarget(nullptr);
|
|
EXPECT_EQ(monitor.volume(), 0.0);
|
|
EXPECT_FALSE(monitor.isAvailable());
|
|
}
|
|
|
|
TEST_F(AudioTest, Reconnect)
|
|
{
|
|
AudioModel sinks(AudioObject::Sink, backend);
|
|
std::weak_ptr<AudioObject> previous = find("test-output-a");
|
|
VolumeMonitor monitor(backend);
|
|
monitor.setTarget(previous.lock());
|
|
stopServers();
|
|
ASSERT_TRUE(until([] { return !backend->isConnected(); }));
|
|
EXPECT_EQ(sinks.count(), 0);
|
|
EXPECT_FALSE(backend->defaultNode(AudioObject::Sink));
|
|
ASSERT_TRUE(until([&] { return previous.expired(); }));
|
|
EXPECT_FALSE(monitor.target());
|
|
startServers();
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected(); }, 10s));
|
|
ASSERT_TRUE(waitFor(*loop, [&] { return sinks.count() == 2; }, 10s));
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->defaultNode(AudioObject::Sink) != nullptr; }, 10s));
|
|
activateDevices();
|
|
const auto sink = find("test-output-a");
|
|
ASSERT_TRUE(until([&] { return sink->volumeWritable(); }));
|
|
sink->setVolume(0.3);
|
|
ASSERT_TRUE(until([&] { return near(sink->volume(), 0.3); }));
|
|
}
|
|
|
|
// As a page that shows sound opens and closes: each backend reads the graph
|
|
// and is the last one to go.
|
|
TEST_F(AudioTest, LastBackendComesAndGoes)
|
|
{
|
|
drivers.clear();
|
|
backend.reset();
|
|
for (int i = 0; i < 5; ++i) {
|
|
backend = Backend::shared();
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected() && find("test-output-a"); }, 10s));
|
|
backend.reset();
|
|
}
|
|
backend = Backend::shared();
|
|
ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected() && find("test-output-a"); }, 10s));
|
|
}
|