Torque3D/Engine/lib/openal-soft/alc/backends/pipewire.cpp
marauder2k7 a745fc3757 Initial commit
added libraries:
opus
flac
libsndfile

updated:
libvorbis
libogg
openal

- Everything works as expected for now. Bare in mind libsndfile needed the check for whether or not it could find the xiph libraries removed in order for this to work.
2024-03-21 17:33:47 +00:00

2166 lines
75 KiB
C++

/**
* OpenAL cross platform audio library
* Copyright (C) 2010 by Chris Robinson
* 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
* Or go to http://www.gnu.org/copyleft/lgpl.html
*/
#include "config.h"
#include "pipewire.h"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <cerrno>
#include <chrono>
#include <ctime>
#include <list>
#include <memory>
#include <mutex>
#include <stdint.h>
#include <thread>
#include <type_traits>
#include <utility>
#include "albyte.h"
#include "alc/alconfig.h"
#include "almalloc.h"
#include "alnumeric.h"
#include "aloptional.h"
#include "alspan.h"
#include "alstring.h"
#include "core/devformat.h"
#include "core/device.h"
#include "core/helpers.h"
#include "core/logging.h"
#include "dynload.h"
#include "opthelpers.h"
#include "ringbuffer.h"
/* Ignore warnings caused by PipeWire headers (lots in standard C++ mode). GCC
* doesn't support ignoring -Weverything, so we have the list the individual
* warnings to ignore (and ignoring -Winline doesn't seem to work).
*/
_Pragma("GCC diagnostic push")
_Pragma("GCC diagnostic ignored \"-Wpedantic\"")
_Pragma("GCC diagnostic ignored \"-Wconversion\"")
_Pragma("GCC diagnostic ignored \"-Wfloat-conversion\"")
_Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"")
_Pragma("GCC diagnostic ignored \"-Wunused-parameter\"")
_Pragma("GCC diagnostic ignored \"-Wold-style-cast\"")
_Pragma("GCC diagnostic ignored \"-Wsign-compare\"")
_Pragma("GCC diagnostic ignored \"-Winline\"")
_Pragma("GCC diagnostic ignored \"-Wpragmas\"")
_Pragma("GCC diagnostic ignored \"-Weverything\"")
#include "pipewire/pipewire.h"
#include "pipewire/extensions/metadata.h"
#include "spa/buffer/buffer.h"
#include "spa/param/audio/format-utils.h"
#include "spa/param/audio/raw.h"
#include "spa/param/param.h"
#include "spa/pod/builder.h"
#include "spa/utils/json.h"
namespace {
/* Wrap some nasty macros here too... */
template<typename ...Args>
auto ppw_core_add_listener(pw_core *core, Args&& ...args)
{ return pw_core_add_listener(core, std::forward<Args>(args)...); }
template<typename ...Args>
auto ppw_core_sync(pw_core *core, Args&& ...args)
{ return pw_core_sync(core, std::forward<Args>(args)...); }
template<typename ...Args>
auto ppw_registry_add_listener(pw_registry *reg, Args&& ...args)
{ return pw_registry_add_listener(reg, std::forward<Args>(args)...); }
template<typename ...Args>
auto ppw_node_add_listener(pw_node *node, Args&& ...args)
{ return pw_node_add_listener(node, std::forward<Args>(args)...); }
template<typename ...Args>
auto ppw_node_subscribe_params(pw_node *node, Args&& ...args)
{ return pw_node_subscribe_params(node, std::forward<Args>(args)...); }
template<typename ...Args>
auto ppw_metadata_add_listener(pw_metadata *mdata, Args&& ...args)
{ return pw_metadata_add_listener(mdata, std::forward<Args>(args)...); }
constexpr auto get_pod_type(const spa_pod *pod) noexcept
{ return SPA_POD_TYPE(pod); }
template<typename T>
constexpr auto get_pod_body(const spa_pod *pod, size_t count) noexcept
{ return al::span<T>{static_cast<T*>(SPA_POD_BODY(pod)), count}; }
template<typename T, size_t N>
constexpr auto get_pod_body(const spa_pod *pod) noexcept
{ return al::span<T,N>{static_cast<T*>(SPA_POD_BODY(pod)), N}; }
constexpr auto make_pod_builder(void *data, uint32_t size) noexcept
{ return SPA_POD_BUILDER_INIT(data, size); }
constexpr auto get_array_value_type(const spa_pod *pod) noexcept
{ return SPA_POD_ARRAY_VALUE_TYPE(pod); }
constexpr auto PwIdAny = PW_ID_ANY;
} // namespace
_Pragma("GCC diagnostic pop")
namespace {
/* Added in 0.3.33, but we currently only require 0.3.23. */
#ifndef PW_KEY_NODE_RATE
#define PW_KEY_NODE_RATE "node.rate"
#endif
using std::chrono::seconds;
using std::chrono::milliseconds;
using std::chrono::nanoseconds;
using uint = unsigned int;
constexpr char pwireDevice[] = "PipeWire Output";
constexpr char pwireInput[] = "PipeWire Input";
bool check_version(const char *version)
{
/* There doesn't seem to be a function to get the version as an integer, so
* instead we have to parse the string, which hopefully won't break in the
* future.
*/
int major{0}, minor{0}, revision{0};
int ret{sscanf(version, "%d.%d.%d", &major, &minor, &revision)};
if(ret == 3 && (major > PW_MAJOR || (major == PW_MAJOR && minor > PW_MINOR)
|| (major == PW_MAJOR && minor == PW_MINOR && revision >= PW_MICRO)))
return true;
return false;
}
#ifdef HAVE_DYNLOAD
#define PWIRE_FUNCS(MAGIC) \
MAGIC(pw_context_connect) \
MAGIC(pw_context_destroy) \
MAGIC(pw_context_new) \
MAGIC(pw_core_disconnect) \
MAGIC(pw_get_library_version) \
MAGIC(pw_init) \
MAGIC(pw_properties_free) \
MAGIC(pw_properties_new) \
MAGIC(pw_properties_set) \
MAGIC(pw_properties_setf) \
MAGIC(pw_proxy_add_object_listener) \
MAGIC(pw_proxy_destroy) \
MAGIC(pw_proxy_get_user_data) \
MAGIC(pw_stream_add_listener) \
MAGIC(pw_stream_connect) \
MAGIC(pw_stream_dequeue_buffer) \
MAGIC(pw_stream_destroy) \
MAGIC(pw_stream_get_state) \
MAGIC(pw_stream_new) \
MAGIC(pw_stream_queue_buffer) \
MAGIC(pw_stream_set_active) \
MAGIC(pw_thread_loop_new) \
MAGIC(pw_thread_loop_destroy) \
MAGIC(pw_thread_loop_get_loop) \
MAGIC(pw_thread_loop_start) \
MAGIC(pw_thread_loop_stop) \
MAGIC(pw_thread_loop_lock) \
MAGIC(pw_thread_loop_wait) \
MAGIC(pw_thread_loop_signal) \
MAGIC(pw_thread_loop_unlock)
#if PW_CHECK_VERSION(0,3,50)
#define PWIRE_FUNCS2(MAGIC) \
MAGIC(pw_stream_get_time_n)
#else
#define PWIRE_FUNCS2(MAGIC) \
MAGIC(pw_stream_get_time)
#endif
void *pwire_handle;
#define MAKE_FUNC(f) decltype(f) * p##f;
PWIRE_FUNCS(MAKE_FUNC)
PWIRE_FUNCS2(MAKE_FUNC)
#undef MAKE_FUNC
bool pwire_load()
{
if(pwire_handle)
return true;
static constexpr char pwire_library[] = "libpipewire-0.3.so.0";
std::string missing_funcs;
pwire_handle = LoadLib(pwire_library);
if(!pwire_handle)
{
WARN("Failed to load %s\n", pwire_library);
return false;
}
#define LOAD_FUNC(f) do { \
p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(pwire_handle, #f)); \
if(p##f == nullptr) missing_funcs += "\n" #f; \
} while(0);
PWIRE_FUNCS(LOAD_FUNC)
PWIRE_FUNCS2(LOAD_FUNC)
#undef LOAD_FUNC
if(!missing_funcs.empty())
{
WARN("Missing expected functions:%s\n", missing_funcs.c_str());
CloseLib(pwire_handle);
pwire_handle = nullptr;
return false;
}
return true;
}
#ifndef IN_IDE_PARSER
#define pw_context_connect ppw_context_connect
#define pw_context_destroy ppw_context_destroy
#define pw_context_new ppw_context_new
#define pw_core_disconnect ppw_core_disconnect
#define pw_get_library_version ppw_get_library_version
#define pw_init ppw_init
#define pw_properties_free ppw_properties_free
#define pw_properties_new ppw_properties_new
#define pw_properties_set ppw_properties_set
#define pw_properties_setf ppw_properties_setf
#define pw_proxy_add_object_listener ppw_proxy_add_object_listener
#define pw_proxy_destroy ppw_proxy_destroy
#define pw_proxy_get_user_data ppw_proxy_get_user_data
#define pw_stream_add_listener ppw_stream_add_listener
#define pw_stream_connect ppw_stream_connect
#define pw_stream_dequeue_buffer ppw_stream_dequeue_buffer
#define pw_stream_destroy ppw_stream_destroy
#define pw_stream_get_state ppw_stream_get_state
#define pw_stream_new ppw_stream_new
#define pw_stream_queue_buffer ppw_stream_queue_buffer
#define pw_stream_set_active ppw_stream_set_active
#define pw_thread_loop_destroy ppw_thread_loop_destroy
#define pw_thread_loop_get_loop ppw_thread_loop_get_loop
#define pw_thread_loop_lock ppw_thread_loop_lock
#define pw_thread_loop_new ppw_thread_loop_new
#define pw_thread_loop_signal ppw_thread_loop_signal
#define pw_thread_loop_start ppw_thread_loop_start
#define pw_thread_loop_stop ppw_thread_loop_stop
#define pw_thread_loop_unlock ppw_thread_loop_unlock
#define pw_thread_loop_wait ppw_thread_loop_wait
#if PW_CHECK_VERSION(0,3,50)
#define pw_stream_get_time_n ppw_stream_get_time_n
#else
inline auto pw_stream_get_time_n(pw_stream *stream, pw_time *ptime, size_t /*size*/)
{ return ppw_stream_get_time(stream, ptime); }
#endif
#endif
#else
constexpr bool pwire_load() { return true; }
#endif
/* Helpers for retrieving values from params */
template<uint32_t T> struct PodInfo { };
template<>
struct PodInfo<SPA_TYPE_Int> {
using Type = int32_t;
static auto get_value(const spa_pod *pod, int32_t *val)
{ return spa_pod_get_int(pod, val); }
};
template<>
struct PodInfo<SPA_TYPE_Id> {
using Type = uint32_t;
static auto get_value(const spa_pod *pod, uint32_t *val)
{ return spa_pod_get_id(pod, val); }
};
template<uint32_t T>
using Pod_t = typename PodInfo<T>::Type;
template<uint32_t T>
al::span<const Pod_t<T>> get_array_span(const spa_pod *pod)
{
uint32_t nvals;
if(void *v{spa_pod_get_array(pod, &nvals)})
{
if(get_array_value_type(pod) == T)
return {static_cast<const Pod_t<T>*>(v), nvals};
}
return {};
}
template<uint32_t T>
al::optional<Pod_t<T>> get_value(const spa_pod *value)
{
Pod_t<T> val{};
if(PodInfo<T>::get_value(value, &val) == 0)
return val;
return al::nullopt;
}
/* Internally, PipeWire types "inherit" from each other, but this is hidden
* from the API and the caller is expected to C-style cast to inherited types
* as needed. It's also not made very clear what types a given type can be
* casted to. To make it a bit safer, this as() method allows casting pw_*
* types to known inherited types, generating a compile-time error for
* unexpected/invalid casts.
*/
template<typename To, typename From>
To as(From) noexcept = delete;
/* pw_proxy
* - pw_registry
* - pw_node
* - pw_metadata
*/
template<>
pw_proxy* as(pw_registry *reg) noexcept { return reinterpret_cast<pw_proxy*>(reg); }
template<>
pw_proxy* as(pw_node *node) noexcept { return reinterpret_cast<pw_proxy*>(node); }
template<>
pw_proxy* as(pw_metadata *mdata) noexcept { return reinterpret_cast<pw_proxy*>(mdata); }
struct PwContextDeleter {
void operator()(pw_context *context) const { pw_context_destroy(context); }
};
using PwContextPtr = std::unique_ptr<pw_context,PwContextDeleter>;
struct PwCoreDeleter {
void operator()(pw_core *core) const { pw_core_disconnect(core); }
};
using PwCorePtr = std::unique_ptr<pw_core,PwCoreDeleter>;
struct PwRegistryDeleter {
void operator()(pw_registry *reg) const { pw_proxy_destroy(as<pw_proxy*>(reg)); }
};
using PwRegistryPtr = std::unique_ptr<pw_registry,PwRegistryDeleter>;
struct PwNodeDeleter {
void operator()(pw_node *node) const { pw_proxy_destroy(as<pw_proxy*>(node)); }
};
using PwNodePtr = std::unique_ptr<pw_node,PwNodeDeleter>;
struct PwMetadataDeleter {
void operator()(pw_metadata *mdata) const { pw_proxy_destroy(as<pw_proxy*>(mdata)); }
};
using PwMetadataPtr = std::unique_ptr<pw_metadata,PwMetadataDeleter>;
struct PwStreamDeleter {
void operator()(pw_stream *stream) const { pw_stream_destroy(stream); }
};
using PwStreamPtr = std::unique_ptr<pw_stream,PwStreamDeleter>;
/* Enums for bitflags... again... *sigh* */
constexpr pw_stream_flags operator|(pw_stream_flags lhs, pw_stream_flags rhs) noexcept
{ return static_cast<pw_stream_flags>(lhs | al::to_underlying(rhs)); }
constexpr pw_stream_flags& operator|=(pw_stream_flags &lhs, pw_stream_flags rhs) noexcept
{ lhs = lhs | rhs; return lhs; }
class ThreadMainloop {
pw_thread_loop *mLoop{};
public:
ThreadMainloop() = default;
ThreadMainloop(const ThreadMainloop&) = delete;
ThreadMainloop(ThreadMainloop&& rhs) noexcept : mLoop{rhs.mLoop} { rhs.mLoop = nullptr; }
explicit ThreadMainloop(pw_thread_loop *loop) noexcept : mLoop{loop} { }
~ThreadMainloop() { if(mLoop) pw_thread_loop_destroy(mLoop); }
ThreadMainloop& operator=(const ThreadMainloop&) = delete;
ThreadMainloop& operator=(ThreadMainloop&& rhs) noexcept
{ std::swap(mLoop, rhs.mLoop); return *this; }
ThreadMainloop& operator=(std::nullptr_t) noexcept
{
if(mLoop)
pw_thread_loop_destroy(mLoop);
mLoop = nullptr;
return *this;
}
explicit operator bool() const noexcept { return mLoop != nullptr; }
auto start() const { return pw_thread_loop_start(mLoop); }
auto stop() const { return pw_thread_loop_stop(mLoop); }
auto getLoop() const { return pw_thread_loop_get_loop(mLoop); }
auto lock() const { return pw_thread_loop_lock(mLoop); }
auto unlock() const { return pw_thread_loop_unlock(mLoop); }
auto signal(bool wait) const { return pw_thread_loop_signal(mLoop, wait); }
auto newContext(pw_properties *props=nullptr, size_t user_data_size=0)
{ return PwContextPtr{pw_context_new(getLoop(), props, user_data_size)}; }
static auto Create(const char *name, spa_dict *props=nullptr)
{ return ThreadMainloop{pw_thread_loop_new(name, props)}; }
friend struct MainloopUniqueLock;
};
struct MainloopUniqueLock : public std::unique_lock<ThreadMainloop> {
using std::unique_lock<ThreadMainloop>::unique_lock;
MainloopUniqueLock& operator=(MainloopUniqueLock&&) = default;
auto wait() const -> void
{ pw_thread_loop_wait(mutex()->mLoop); }
template<typename Predicate>
auto wait(Predicate done_waiting) const -> void
{ while(!done_waiting()) wait(); }
};
using MainloopLockGuard = std::lock_guard<ThreadMainloop>;
/* There's quite a mess here, but the purpose is to track active devices and
* their default formats, so playback devices can be configured to match. The
* device list is updated asynchronously, so it will have the latest list of
* devices provided by the server.
*/
struct NodeProxy;
struct MetadataProxy;
/* The global thread watching for global events. This particular class responds
* to objects being added to or removed from the registry.
*/
struct EventManager {
ThreadMainloop mLoop{};
PwContextPtr mContext{};
PwCorePtr mCore{};
PwRegistryPtr mRegistry{};
spa_hook mRegistryListener{};
spa_hook mCoreListener{};
/* A list of proxy objects watching for events about changes to objects in
* the registry.
*/
std::vector<NodeProxy*> mNodeList;
MetadataProxy *mDefaultMetadata{nullptr};
/* Initialization handling. When init() is called, mInitSeq is set to a
* SequenceID that marks the end of populating the registry. As objects of
* interest are found, events to parse them are generated and mInitSeq is
* updated with a newer ID. When mInitSeq stops being updated and the event
* corresponding to it is reached, mInitDone will be set to true.
*/
std::atomic<bool> mInitDone{false};
std::atomic<bool> mHasAudio{false};
int mInitSeq{};
bool init();
~EventManager();
void kill();
auto lock() const { return mLoop.lock(); }
auto unlock() const { return mLoop.unlock(); }
/**
* Waits for initialization to finish. The event manager must *NOT* be
* locked when calling this.
*/
void waitForInit()
{
if(!mInitDone.load(std::memory_order_acquire)) UNLIKELY
{
MainloopUniqueLock plock{mLoop};
plock.wait([this](){ return mInitDone.load(std::memory_order_acquire); });
}
}
/**
* Waits for audio support to be detected, or initialization to finish,
* whichever is first. Returns true if audio support was detected. The
* event manager must *NOT* be locked when calling this.
*/
bool waitForAudio()
{
MainloopUniqueLock plock{mLoop};
bool has_audio{};
plock.wait([this,&has_audio]()
{
has_audio = mHasAudio.load(std::memory_order_acquire);
return has_audio || mInitDone.load(std::memory_order_acquire);
});
return has_audio;
}
void syncInit()
{
/* If initialization isn't done, update the sequence ID so it won't
* complete until after currently scheduled events.
*/
if(!mInitDone.load(std::memory_order_relaxed))
mInitSeq = ppw_core_sync(mCore.get(), PW_ID_CORE, mInitSeq);
}
void addCallback(uint32_t id, uint32_t permissions, const char *type, uint32_t version,
const spa_dict *props);
static void addCallbackC(void *object, uint32_t id, uint32_t permissions, const char *type,
uint32_t version, const spa_dict *props)
{ static_cast<EventManager*>(object)->addCallback(id, permissions, type, version, props); }
void removeCallback(uint32_t id);
static void removeCallbackC(void *object, uint32_t id)
{ static_cast<EventManager*>(object)->removeCallback(id); }
static constexpr pw_registry_events CreateRegistryEvents()
{
pw_registry_events ret{};
ret.version = PW_VERSION_REGISTRY_EVENTS;
ret.global = &EventManager::addCallbackC;
ret.global_remove = &EventManager::removeCallbackC;
return ret;
}
void coreCallback(uint32_t id, int seq);
static void coreCallbackC(void *object, uint32_t id, int seq)
{ static_cast<EventManager*>(object)->coreCallback(id, seq); }
static constexpr pw_core_events CreateCoreEvents()
{
pw_core_events ret{};
ret.version = PW_VERSION_CORE_EVENTS;
ret.done = &EventManager::coreCallbackC;
return ret;
}
};
using EventWatcherUniqueLock = std::unique_lock<EventManager>;
using EventWatcherLockGuard = std::lock_guard<EventManager>;
EventManager gEventHandler;
/* Enumerated devices. This is updated asynchronously as the app runs, and the
* gEventHandler thread loop must be locked when accessing the list.
*/
enum class NodeType : unsigned char {
Sink, Source, Duplex
};
constexpr auto InvalidChannelConfig = DevFmtChannels(255);
struct DeviceNode {
uint32_t mId{};
uint64_t mSerial{};
std::string mName;
std::string mDevName;
NodeType mType{};
bool mIsHeadphones{};
bool mIs51Rear{};
uint mSampleRate{};
DevFmtChannels mChannels{InvalidChannelConfig};
static std::vector<DeviceNode> sList;
static DeviceNode &Add(uint32_t id);
static DeviceNode *Find(uint32_t id);
static void Remove(uint32_t id);
static std::vector<DeviceNode> &GetList() noexcept { return sList; }
void parseSampleRate(const spa_pod *value) noexcept;
void parsePositions(const spa_pod *value) noexcept;
void parseChannelCount(const spa_pod *value) noexcept;
};
std::vector<DeviceNode> DeviceNode::sList;
std::string DefaultSinkDevice;
std::string DefaultSourceDevice;
const char *AsString(NodeType type) noexcept
{
switch(type)
{
case NodeType::Sink: return "sink";
case NodeType::Source: return "source";
case NodeType::Duplex: return "duplex";
}
return "<unknown>";
}
DeviceNode &DeviceNode::Add(uint32_t id)
{
auto match_id = [id](DeviceNode &n) noexcept -> bool
{ return n.mId == id; };
/* If the node is already in the list, return the existing entry. */
auto match = std::find_if(sList.begin(), sList.end(), match_id);
if(match != sList.end()) return *match;
sList.emplace_back();
auto &n = sList.back();
n.mId = id;
return n;
}
DeviceNode *DeviceNode::Find(uint32_t id)
{
auto match_id = [id](DeviceNode &n) noexcept -> bool
{ return n.mId == id; };
auto match = std::find_if(sList.begin(), sList.end(), match_id);
if(match != sList.end()) return al::to_address(match);
return nullptr;
}
void DeviceNode::Remove(uint32_t id)
{
auto match_id = [id](DeviceNode &n) noexcept -> bool
{
if(n.mId != id)
return false;
TRACE("Removing device \"%s\"\n", n.mDevName.c_str());
return true;
};
auto end = std::remove_if(sList.begin(), sList.end(), match_id);
sList.erase(end, sList.end());
}
const spa_audio_channel MonoMap[]{
SPA_AUDIO_CHANNEL_MONO
}, StereoMap[] {
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR
}, QuadMap[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR
}, X51Map[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
}, X51RearMap[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR
}, X61Map[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
}, X71Map[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
}, X714Map[]{
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR,
SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR
};
/**
* Checks if every channel in 'map1' exists in 'map0' (that is, map0 is equal
* to or a superset of map1).
*/
template<size_t N>
bool MatchChannelMap(const al::span<const uint32_t> map0, const spa_audio_channel (&map1)[N])
{
if(map0.size() < N)
return false;
for(const spa_audio_channel chid : map1)
{
if(std::find(map0.begin(), map0.end(), chid) == map0.end())
return false;
}
return true;
}
void DeviceNode::parseSampleRate(const spa_pod *value) noexcept
{
/* TODO: Can this be anything else? Long, Float, Double? */
uint32_t nvals{}, choiceType{};
value = spa_pod_get_values(value, &nvals, &choiceType);
const uint podType{get_pod_type(value)};
if(podType != SPA_TYPE_Int)
{
WARN("Unhandled sample rate POD type: %u\n", podType);
return;
}
if(choiceType == SPA_CHOICE_Range)
{
if(nvals != 3)
{
WARN("Unexpected SPA_CHOICE_Range count: %u\n", nvals);
return;
}
auto srates = get_pod_body<int32_t,3>(value);
/* [0] is the default, [1] is the min, and [2] is the max. */
TRACE("Device ID %" PRIu64 " sample rate: %d (range: %d -> %d)\n", mSerial, srates[0],
srates[1], srates[2]);
mSampleRate = static_cast<uint>(clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE));
return;
}
if(choiceType == SPA_CHOICE_Enum)
{
if(nvals == 0)
{
WARN("Unexpected SPA_CHOICE_Enum count: %u\n", nvals);
return;
}
auto srates = get_pod_body<int32_t>(value, nvals);
/* [0] is the default, [1...size()-1] are available selections. */
std::string others{(srates.size() > 1) ? std::to_string(srates[1]) : std::string{}};
for(size_t i{2};i < srates.size();++i)
{
others += ", ";
others += std::to_string(srates[i]);
}
TRACE("Device ID %" PRIu64 " sample rate: %d (%s)\n", mSerial, srates[0], others.c_str());
/* Pick the first rate listed that's within the allowed range (default
* rate if possible).
*/
for(const auto &rate : srates)
{
if(rate >= MIN_OUTPUT_RATE && rate <= MAX_OUTPUT_RATE)
{
mSampleRate = static_cast<uint>(rate);
break;
}
}
return;
}
if(choiceType == SPA_CHOICE_None)
{
if(nvals != 1)
{
WARN("Unexpected SPA_CHOICE_None count: %u\n", nvals);
return;
}
auto srates = get_pod_body<int32_t,1>(value);
TRACE("Device ID %" PRIu64 " sample rate: %d\n", mSerial, srates[0]);
mSampleRate = static_cast<uint>(clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE));
return;
}
WARN("Unhandled sample rate choice type: %u\n", choiceType);
}
void DeviceNode::parsePositions(const spa_pod *value) noexcept
{
const auto chanmap = get_array_span<SPA_TYPE_Id>(value);
if(chanmap.empty()) return;
mIs51Rear = false;
if(MatchChannelMap(chanmap, X714Map))
mChannels = DevFmtX714;
else if(MatchChannelMap(chanmap, X71Map))
mChannels = DevFmtX71;
else if(MatchChannelMap(chanmap, X61Map))
mChannels = DevFmtX61;
else if(MatchChannelMap(chanmap, X51Map))
mChannels = DevFmtX51;
else if(MatchChannelMap(chanmap, X51RearMap))
{
mChannels = DevFmtX51;
mIs51Rear = true;
}
else if(MatchChannelMap(chanmap, QuadMap))
mChannels = DevFmtQuad;
else if(MatchChannelMap(chanmap, StereoMap))
mChannels = DevFmtStereo;
else
mChannels = DevFmtMono;
TRACE("Device ID %" PRIu64 " got %zu position%s for %s%s\n", mSerial, chanmap.size(),
(chanmap.size()==1)?"":"s", DevFmtChannelsString(mChannels), mIs51Rear?"(rear)":"");
}
void DeviceNode::parseChannelCount(const spa_pod *value) noexcept
{
/* As a fallback with just a channel count, just assume mono or stereo. */
const auto chancount = get_value<SPA_TYPE_Int>(value);
if(!chancount) return;
mIs51Rear = false;
if(*chancount >= 2)
mChannels = DevFmtStereo;
else if(*chancount >= 1)
mChannels = DevFmtMono;
TRACE("Device ID %" PRIu64 " got %d channel%s for %s\n", mSerial, *chancount,
(*chancount==1)?"":"s", DevFmtChannelsString(mChannels));
}
constexpr char MonitorPrefix[]{"Monitor of "};
constexpr auto MonitorPrefixLen = al::size(MonitorPrefix) - 1;
constexpr char AudioSinkClass[]{"Audio/Sink"};
constexpr char AudioSourceClass[]{"Audio/Source"};
constexpr char AudioSourceVirtualClass[]{"Audio/Source/Virtual"};
constexpr char AudioDuplexClass[]{"Audio/Duplex"};
constexpr char StreamClass[]{"Stream/"};
/* A generic PipeWire node proxy object used to track changes to sink and
* source nodes.
*/
struct NodeProxy {
static constexpr pw_node_events CreateNodeEvents()
{
pw_node_events ret{};
ret.version = PW_VERSION_NODE_EVENTS;
ret.info = &NodeProxy::infoCallbackC;
ret.param = &NodeProxy::paramCallbackC;
return ret;
}
uint32_t mId{};
PwNodePtr mNode{};
spa_hook mListener{};
NodeProxy(uint32_t id, PwNodePtr node)
: mId{id}, mNode{std::move(node)}
{
static constexpr pw_node_events nodeEvents{CreateNodeEvents()};
ppw_node_add_listener(mNode.get(), &mListener, &nodeEvents, this);
/* Track changes to the enumerable formats (indicates the default
* format, which is what we're interested in).
*/
uint32_t fmtids[]{SPA_PARAM_EnumFormat};
ppw_node_subscribe_params(mNode.get(), al::data(fmtids), al::size(fmtids));
}
~NodeProxy()
{ spa_hook_remove(&mListener); }
void infoCallback(const pw_node_info *info);
static void infoCallbackC(void *object, const pw_node_info *info)
{ static_cast<NodeProxy*>(object)->infoCallback(info); }
void paramCallback(int seq, uint32_t id, uint32_t index, uint32_t next, const spa_pod *param);
static void paramCallbackC(void *object, int seq, uint32_t id, uint32_t index, uint32_t next,
const spa_pod *param)
{ static_cast<NodeProxy*>(object)->paramCallback(seq, id, index, next, param); }
};
void NodeProxy::infoCallback(const pw_node_info *info)
{
/* We only care about property changes here (media class, name/desc).
* Format changes will automatically invoke the param callback.
*
* TODO: Can the media class or name/desc change without being removed and
* readded?
*/
if((info->change_mask&PW_NODE_CHANGE_MASK_PROPS))
{
/* Can this actually change? */
const char *media_class{spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS)};
if(!media_class) UNLIKELY return;
NodeType ntype{};
if(al::strcasecmp(media_class, AudioSinkClass) == 0)
ntype = NodeType::Sink;
else if(al::strcasecmp(media_class, AudioSourceClass) == 0
|| al::strcasecmp(media_class, AudioSourceVirtualClass) == 0)
ntype = NodeType::Source;
else if(al::strcasecmp(media_class, AudioDuplexClass) == 0)
ntype = NodeType::Duplex;
else
{
TRACE("Dropping device node %u which became type \"%s\"\n", info->id, media_class);
DeviceNode::Remove(info->id);
return;
}
const char *devName{spa_dict_lookup(info->props, PW_KEY_NODE_NAME)};
const char *nodeName{spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)};
if(!nodeName || !*nodeName) nodeName = spa_dict_lookup(info->props, PW_KEY_NODE_NICK);
if(!nodeName || !*nodeName) nodeName = devName;
uint64_t serial_id{info->id};
#ifdef PW_KEY_OBJECT_SERIAL
if(const char *serial_str{spa_dict_lookup(info->props, PW_KEY_OBJECT_SERIAL)})
{
char *serial_end{};
serial_id = std::strtoull(serial_str, &serial_end, 0);
if(*serial_end != '\0' || errno == ERANGE)
{
ERR("Unexpected object serial: %s\n", serial_str);
serial_id = info->id;
}
}
#endif
const char *form_factor{spa_dict_lookup(info->props, PW_KEY_DEVICE_FORM_FACTOR)};
TRACE("Got %s device \"%s\"%s%s%s\n", AsString(ntype), devName ? devName : "(nil)",
form_factor?" (":"", form_factor?form_factor:"", form_factor?")":"");
TRACE(" \"%s\" = ID %" PRIu64 "\n", nodeName ? nodeName : "(nil)", serial_id);
DeviceNode &node = DeviceNode::Add(info->id);
node.mSerial = serial_id;
if(nodeName && *nodeName) node.mName = nodeName;
else node.mName = "PipeWire node #"+std::to_string(info->id);
node.mDevName = devName ? devName : "";
node.mType = ntype;
node.mIsHeadphones = form_factor && (al::strcasecmp(form_factor, "headphones") == 0
|| al::strcasecmp(form_factor, "headset") == 0);
}
}
void NodeProxy::paramCallback(int, uint32_t id, uint32_t, uint32_t, const spa_pod *param)
{
if(id == SPA_PARAM_EnumFormat)
{
DeviceNode *node{DeviceNode::Find(mId)};
if(!node) UNLIKELY return;
if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_rate)})
node->parseSampleRate(&prop->value);
if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_position)})
node->parsePositions(&prop->value);
else if((prop=spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_channels)) != nullptr)
node->parseChannelCount(&prop->value);
}
}
/* A metadata proxy object used to query the default sink and source. */
struct MetadataProxy {
static constexpr pw_metadata_events CreateMetadataEvents()
{
pw_metadata_events ret{};
ret.version = PW_VERSION_METADATA_EVENTS;
ret.property = &MetadataProxy::propertyCallbackC;
return ret;
}
uint32_t mId{};
PwMetadataPtr mMetadata{};
spa_hook mListener{};
MetadataProxy(uint32_t id, PwMetadataPtr mdata)
: mId{id}, mMetadata{std::move(mdata)}
{
static constexpr pw_metadata_events metadataEvents{CreateMetadataEvents()};
ppw_metadata_add_listener(mMetadata.get(), &mListener, &metadataEvents, this);
}
~MetadataProxy()
{ spa_hook_remove(&mListener); }
int propertyCallback(uint32_t id, const char *key, const char *type, const char *value);
static int propertyCallbackC(void *object, uint32_t id, const char *key, const char *type,
const char *value)
{ return static_cast<MetadataProxy*>(object)->propertyCallback(id, key, type, value); }
};
int MetadataProxy::propertyCallback(uint32_t id, const char *key, const char *type,
const char *value)
{
if(id != PW_ID_CORE)
return 0;
bool isCapture{};
if(std::strcmp(key, "default.audio.sink") == 0)
isCapture = false;
else if(std::strcmp(key, "default.audio.source") == 0)
isCapture = true;
else
return 0;
if(!type)
{
TRACE("Default %s device cleared\n", isCapture ? "capture" : "playback");
if(!isCapture) DefaultSinkDevice.clear();
else DefaultSourceDevice.clear();
return 0;
}
if(std::strcmp(type, "Spa:String:JSON") != 0)
{
ERR("Unexpected %s property type: %s\n", key, type);
return 0;
}
spa_json it[2]{};
spa_json_init(&it[0], value, strlen(value));
if(spa_json_enter_object(&it[0], &it[1]) <= 0)
return 0;
auto get_json_string = [](spa_json *iter)
{
al::optional<std::string> str;
const char *val{};
int len{spa_json_next(iter, &val)};
if(len <= 0) return str;
str.emplace().resize(static_cast<uint>(len), '\0');
if(spa_json_parse_string(val, len, &str->front()) <= 0)
str.reset();
else while(!str->empty() && str->back() == '\0')
str->pop_back();
return str;
};
while(auto propKey = get_json_string(&it[1]))
{
if(*propKey == "name")
{
auto propValue = get_json_string(&it[1]);
if(!propValue) break;
TRACE("Got default %s device \"%s\"\n", isCapture ? "capture" : "playback",
propValue->c_str());
if(!isCapture)
DefaultSinkDevice = std::move(*propValue);
else
DefaultSourceDevice = std::move(*propValue);
}
else
{
const char *v{};
if(spa_json_next(&it[1], &v) <= 0)
break;
}
}
return 0;
}
bool EventManager::init()
{
mLoop = ThreadMainloop::Create("PWEventThread");
if(!mLoop)
{
ERR("Failed to create PipeWire event thread loop (errno: %d)\n", errno);
return false;
}
mContext = mLoop.newContext(pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr));
if(!mContext)
{
ERR("Failed to create PipeWire event context (errno: %d)\n", errno);
return false;
}
mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
if(!mCore)
{
ERR("Failed to connect PipeWire event context (errno: %d)\n", errno);
return false;
}
mRegistry = PwRegistryPtr{pw_core_get_registry(mCore.get(), PW_VERSION_REGISTRY, 0)};
if(!mRegistry)
{
ERR("Failed to get PipeWire event registry (errno: %d)\n", errno);
return false;
}
static constexpr pw_core_events coreEvents{CreateCoreEvents()};
static constexpr pw_registry_events registryEvents{CreateRegistryEvents()};
ppw_core_add_listener(mCore.get(), &mCoreListener, &coreEvents, this);
ppw_registry_add_listener(mRegistry.get(), &mRegistryListener, &registryEvents, this);
/* Set an initial sequence ID for initialization, to trigger after the
* registry is first populated.
*/
mInitSeq = ppw_core_sync(mCore.get(), PW_ID_CORE, 0);
if(int res{mLoop.start()})
{
ERR("Failed to start PipeWire event thread loop (res: %d)\n", res);
return false;
}
return true;
}
EventManager::~EventManager()
{
if(mLoop) mLoop.stop();
for(NodeProxy *node : mNodeList)
al::destroy_at(node);
if(mDefaultMetadata)
al::destroy_at(mDefaultMetadata);
}
void EventManager::kill()
{
if(mLoop) mLoop.stop();
for(NodeProxy *node : mNodeList)
al::destroy_at(node);
mNodeList.clear();
if(mDefaultMetadata)
al::destroy_at(mDefaultMetadata);
mDefaultMetadata = nullptr;
mRegistry = nullptr;
mCore = nullptr;
mContext = nullptr;
mLoop = nullptr;
}
void EventManager::addCallback(uint32_t id, uint32_t, const char *type, uint32_t version,
const spa_dict *props)
{
/* We're only interested in interface nodes. */
if(std::strcmp(type, PW_TYPE_INTERFACE_Node) == 0)
{
const char *media_class{spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)};
if(!media_class) return;
/* Specifically, audio sinks and sources (and duplexes). */
const bool isGood{al::strcasecmp(media_class, AudioSinkClass) == 0
|| al::strcasecmp(media_class, AudioSourceClass) == 0
|| al::strcasecmp(media_class, AudioSourceVirtualClass) == 0
|| al::strcasecmp(media_class, AudioDuplexClass) == 0};
if(!isGood)
{
if(std::strstr(media_class, "/Video") == nullptr
&& std::strncmp(media_class, StreamClass, sizeof(StreamClass)-1) != 0)
TRACE("Ignoring node class %s\n", media_class);
return;
}
/* Create the proxy object. */
auto node = PwNodePtr{static_cast<pw_node*>(pw_registry_bind(mRegistry.get(), id, type,
version, sizeof(NodeProxy)))};
if(!node)
{
ERR("Failed to create node proxy object (errno: %d)\n", errno);
return;
}
/* Initialize the NodeProxy to hold the node object, add it to the
* active node list, and update the sync point.
*/
auto *proxy = static_cast<NodeProxy*>(pw_proxy_get_user_data(as<pw_proxy*>(node.get())));
mNodeList.emplace_back(al::construct_at(proxy, id, std::move(node)));
syncInit();
/* Signal any waiters that we have found a source or sink for audio
* support.
*/
if(!mHasAudio.exchange(true, std::memory_order_acq_rel))
mLoop.signal(false);
}
else if(std::strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0)
{
const char *data_class{spa_dict_lookup(props, PW_KEY_METADATA_NAME)};
if(!data_class) return;
if(std::strcmp(data_class, "default") != 0)
{
TRACE("Ignoring metadata \"%s\"\n", data_class);
return;
}
if(mDefaultMetadata)
{
ERR("Duplicate default metadata\n");
return;
}
auto mdata = PwMetadataPtr{static_cast<pw_metadata*>(pw_registry_bind(mRegistry.get(), id,
type, version, sizeof(MetadataProxy)))};
if(!mdata)
{
ERR("Failed to create metadata proxy object (errno: %d)\n", errno);
return;
}
auto *proxy = static_cast<MetadataProxy*>(
pw_proxy_get_user_data(as<pw_proxy*>(mdata.get())));
mDefaultMetadata = al::construct_at(proxy, id, std::move(mdata));
syncInit();
}
}
void EventManager::removeCallback(uint32_t id)
{
DeviceNode::Remove(id);
auto clear_node = [id](NodeProxy *node) noexcept
{
if(node->mId != id)
return false;
al::destroy_at(node);
return true;
};
auto node_end = std::remove_if(mNodeList.begin(), mNodeList.end(), clear_node);
mNodeList.erase(node_end, mNodeList.end());
if(mDefaultMetadata && mDefaultMetadata->mId == id)
{
al::destroy_at(mDefaultMetadata);
mDefaultMetadata = nullptr;
}
}
void EventManager::coreCallback(uint32_t id, int seq)
{
if(id == PW_ID_CORE && seq == mInitSeq)
{
/* Initialization done. Remove this callback and signal anyone that may
* be waiting.
*/
spa_hook_remove(&mCoreListener);
mInitDone.store(true);
mLoop.signal(false);
}
}
enum use_f32p_e : bool { UseDevType=false, ForceF32Planar=true };
spa_audio_info_raw make_spa_info(DeviceBase *device, bool is51rear, use_f32p_e use_f32p)
{
spa_audio_info_raw info{};
if(use_f32p)
{
device->FmtType = DevFmtFloat;
info.format = SPA_AUDIO_FORMAT_F32P;
}
else switch(device->FmtType)
{
case DevFmtByte: info.format = SPA_AUDIO_FORMAT_S8; break;
case DevFmtUByte: info.format = SPA_AUDIO_FORMAT_U8; break;
case DevFmtShort: info.format = SPA_AUDIO_FORMAT_S16; break;
case DevFmtUShort: info.format = SPA_AUDIO_FORMAT_U16; break;
case DevFmtInt: info.format = SPA_AUDIO_FORMAT_S32; break;
case DevFmtUInt: info.format = SPA_AUDIO_FORMAT_U32; break;
case DevFmtFloat: info.format = SPA_AUDIO_FORMAT_F32; break;
}
info.rate = device->Frequency;
al::span<const spa_audio_channel> map{};
switch(device->FmtChans)
{
case DevFmtMono: map = MonoMap; break;
case DevFmtStereo: map = StereoMap; break;
case DevFmtQuad: map = QuadMap; break;
case DevFmtX51:
if(is51rear) map = X51RearMap;
else map = X51Map;
break;
case DevFmtX61: map = X61Map; break;
case DevFmtX71: map = X71Map; break;
case DevFmtX714: map = X714Map; break;
case DevFmtX3D71: map = X71Map; break;
case DevFmtAmbi3D:
info.flags |= SPA_AUDIO_FLAG_UNPOSITIONED;
info.channels = device->channelsFromFmt();
break;
}
if(!map.empty())
{
info.channels = static_cast<uint32_t>(map.size());
std::copy(map.begin(), map.end(), info.position);
}
return info;
}
class PipeWirePlayback final : public BackendBase {
void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
const char *error)
{ static_cast<PipeWirePlayback*>(data)->stateChangedCallback(old, state, error); }
void ioChangedCallback(uint32_t id, void *area, uint32_t size);
static void ioChangedCallbackC(void *data, uint32_t id, void *area, uint32_t size)
{ static_cast<PipeWirePlayback*>(data)->ioChangedCallback(id, area, size); }
void outputCallback();
static void outputCallbackC(void *data)
{ static_cast<PipeWirePlayback*>(data)->outputCallback(); }
void open(const char *name) override;
bool reset() override;
void start() override;
void stop() override;
ClockLatency getClockLatency() override;
uint64_t mTargetId{PwIdAny};
nanoseconds mTimeBase{0};
ThreadMainloop mLoop;
PwContextPtr mContext;
PwCorePtr mCore;
PwStreamPtr mStream;
spa_hook mStreamListener{};
spa_io_rate_match *mRateMatch{};
std::unique_ptr<float*[]> mChannelPtrs;
uint mNumChannels{};
static constexpr pw_stream_events CreateEvents()
{
pw_stream_events ret{};
ret.version = PW_VERSION_STREAM_EVENTS;
ret.state_changed = &PipeWirePlayback::stateChangedCallbackC;
ret.io_changed = &PipeWirePlayback::ioChangedCallbackC;
ret.process = &PipeWirePlayback::outputCallbackC;
return ret;
}
public:
PipeWirePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
~PipeWirePlayback()
{
/* Stop the mainloop so the stream can be properly destroyed. */
if(mLoop) mLoop.stop();
}
DEF_NEWDEL(PipeWirePlayback)
};
void PipeWirePlayback::stateChangedCallback(pw_stream_state, pw_stream_state, const char*)
{ mLoop.signal(false); }
void PipeWirePlayback::ioChangedCallback(uint32_t id, void *area, uint32_t size)
{
switch(id)
{
case SPA_IO_RateMatch:
if(size >= sizeof(spa_io_rate_match))
mRateMatch = static_cast<spa_io_rate_match*>(area);
break;
}
}
void PipeWirePlayback::outputCallback()
{
pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())};
if(!pw_buf) UNLIKELY return;
const al::span<spa_data> datas{pw_buf->buffer->datas,
minu(mNumChannels, pw_buf->buffer->n_datas)};
#if PW_CHECK_VERSION(0,3,49)
/* In 0.3.49, pw_buffer::requested specifies the number of samples needed
* by the resampler/graph for this audio update.
*/
uint length{static_cast<uint>(pw_buf->requested)};
#else
/* In 0.3.48 and earlier, spa_io_rate_match::size apparently has the number
* of samples per update.
*/
uint length{mRateMatch ? mRateMatch->size : 0u};
#endif
/* If no length is specified, use the device's update size as a fallback. */
if(!length) UNLIKELY length = mDevice->UpdateSize;
/* For planar formats, each datas[] seems to contain one channel, so store
* the pointers in an array. Limit the render length in case the available
* buffer length in any one channel is smaller than we wanted (shouldn't
* be, but just in case).
*/
float **chanptr_end{mChannelPtrs.get()};
for(const auto &data : datas)
{
length = minu(length, data.maxsize/sizeof(float));
*chanptr_end = static_cast<float*>(data.data);
++chanptr_end;
}
mDevice->renderSamples({mChannelPtrs.get(), chanptr_end}, length);
for(const auto &data : datas)
{
data.chunk->offset = 0;
data.chunk->stride = sizeof(float);
data.chunk->size = length * sizeof(float);
}
pw_buf->size = length;
pw_stream_queue_buffer(mStream.get(), pw_buf);
}
void PipeWirePlayback::open(const char *name)
{
static std::atomic<uint> OpenCount{0};
uint64_t targetid{PwIdAny};
std::string devname{};
gEventHandler.waitForInit();
if(!name)
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match = devlist.cend();
if(!DefaultSinkDevice.empty())
{
auto match_default = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSinkDevice; };
match = std::find_if(devlist.cbegin(), devlist.cend(), match_default);
}
if(match == devlist.cend())
{
auto match_playback = [](const DeviceNode &n) -> bool
{ return n.mType != NodeType::Source; };
match = std::find_if(devlist.cbegin(), devlist.cend(), match_playback);
if(match == devlist.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"No PipeWire playback device found"};
}
targetid = match->mSerial;
devname = match->mName;
}
else
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match_name = [name](const DeviceNode &n) -> bool
{ return n.mType != NodeType::Source && n.mName == name; };
auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_name);
if(match == devlist.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"Device name \"%s\" not found", name};
targetid = match->mSerial;
devname = match->mName;
}
if(!mLoop)
{
const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)};
const std::string thread_name{"ALSoftP" + std::to_string(count)};
mLoop = ThreadMainloop::Create(thread_name.c_str());
if(!mLoop)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire mainloop (errno: %d)", errno};
if(int res{mLoop.start()})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire mainloop (res: %d)", res};
}
MainloopUniqueLock mlock{mLoop};
if(!mContext)
{
pw_properties *cprops{pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr)};
mContext = mLoop.newContext(cprops);
if(!mContext)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire event context (errno: %d)\n", errno};
}
if(!mCore)
{
mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
if(!mCore)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to connect PipeWire event context (errno: %d)\n", errno};
}
mlock.unlock();
/* TODO: Ensure the target ID is still valid/usable and accepts streams. */
mTargetId = targetid;
if(!devname.empty())
mDevice->DeviceName = std::move(devname);
else
mDevice->DeviceName = pwireDevice;
}
bool PipeWirePlayback::reset()
{
if(mStream)
{
MainloopLockGuard _{mLoop};
mStream = nullptr;
}
mStreamListener = {};
mRateMatch = nullptr;
mTimeBase = GetDeviceClockTime(mDevice);
/* If connecting to a specific device, update various device parameters to
* match its format.
*/
bool is51rear{false};
mDevice->Flags.reset(DirectEar);
if(mTargetId != PwIdAny)
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match_id = [targetid=mTargetId](const DeviceNode &n) -> bool
{ return targetid == n.mSerial; };
auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_id);
if(match != devlist.cend())
{
if(!mDevice->Flags.test(FrequencyRequest) && match->mSampleRate > 0)
{
/* Scale the update size if the sample rate changes. */
const double scale{static_cast<double>(match->mSampleRate) / mDevice->Frequency};
const double numbufs{static_cast<double>(mDevice->BufferSize)/mDevice->UpdateSize};
mDevice->Frequency = match->mSampleRate;
mDevice->UpdateSize = static_cast<uint>(clampd(mDevice->UpdateSize*scale + 0.5,
64.0, 8192.0));
mDevice->BufferSize = static_cast<uint>(numbufs*mDevice->UpdateSize + 0.5);
}
if(!mDevice->Flags.test(ChannelsRequest) && match->mChannels != InvalidChannelConfig)
mDevice->FmtChans = match->mChannels;
if(match->mChannels == DevFmtStereo && match->mIsHeadphones)
mDevice->Flags.set(DirectEar);
is51rear = match->mIs51Rear;
}
}
/* Force planar 32-bit float output for playback. This is what PipeWire
* handles internally, and it's easier for us too.
*/
spa_audio_info_raw info{make_spa_info(mDevice, is51rear, ForceF32Planar)};
/* TODO: How to tell what an appropriate size is? Examples just use this
* magic value.
*/
constexpr uint32_t pod_buffer_size{1024};
auto pod_buffer = std::make_unique<al::byte[]>(pod_buffer_size);
spa_pod_builder b{make_pod_builder(pod_buffer.get(), pod_buffer_size)};
const spa_pod *params{spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info)};
if(!params)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to set PipeWire audio format parameters"};
/* TODO: Which properties are actually needed here? Any others that could
* be useful?
*/
auto&& binary = GetProcBinary();
const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
pw_properties *props{pw_properties_new(PW_KEY_NODE_NAME, appname,
PW_KEY_NODE_DESCRIPTION, appname,
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Game",
PW_KEY_NODE_ALWAYS_PROCESS, "true",
nullptr)};
if(!props)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire stream properties (errno: %d)", errno};
pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", mDevice->UpdateSize,
mDevice->Frequency);
pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", mDevice->Frequency);
#ifdef PW_KEY_TARGET_OBJECT
pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%" PRIu64, mTargetId);
#else
pw_properties_setf(props, PW_KEY_NODE_TARGET, "%" PRIu64, mTargetId);
#endif
MainloopUniqueLock plock{mLoop};
/* The stream takes overship of 'props', even in the case of failure. */
mStream = PwStreamPtr{pw_stream_new(mCore.get(), "Playback Stream", props)};
if(!mStream)
throw al::backend_exception{al::backend_error::NoDevice,
"Failed to create PipeWire stream (errno: %d)", errno};
static constexpr pw_stream_events streamEvents{CreateEvents()};
pw_stream_add_listener(mStream.get(), &mStreamListener, &streamEvents, this);
pw_stream_flags flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
| PW_STREAM_FLAG_MAP_BUFFERS};
if(GetConfigValueBool(mDevice->DeviceName.c_str(), "pipewire", "rt-mix", true))
flags |= PW_STREAM_FLAG_RT_PROCESS;
if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_OUTPUT, PwIdAny, flags, &params, 1)})
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream (res: %d)", res};
/* Wait for the stream to become paused (ready to start streaming). */
plock.wait([stream=mStream.get()]()
{
const char *error{};
pw_stream_state state{pw_stream_get_state(stream, &error)};
if(state == PW_STREAM_STATE_ERROR)
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream: \"%s\"", error};
return state == PW_STREAM_STATE_PAUSED;
});
/* TODO: Update mDevice->UpdateSize with the stream's quantum, and
* mDevice->BufferSize with the total known buffering delay from the head
* of this playback stream to the tail of the device output.
*
* This info is apparently not available until after the stream starts.
*/
plock.unlock();
mNumChannels = mDevice->channelsFromFmt();
mChannelPtrs = std::make_unique<float*[]>(mNumChannels);
setDefaultWFXChannelOrder();
return true;
}
void PipeWirePlayback::start()
{
MainloopUniqueLock plock{mLoop};
if(int res{pw_stream_set_active(mStream.get(), true)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire stream (res: %d)", res};
/* Wait for the stream to start playing (would be nice to not, but we need
* the actual update size which is only available after starting).
*/
plock.wait([stream=mStream.get()]()
{
const char *error{};
pw_stream_state state{pw_stream_get_state(stream, &error)};
if(state == PW_STREAM_STATE_ERROR)
throw al::backend_exception{al::backend_error::DeviceError,
"PipeWire stream error: %s", error ? error : "(unknown)"};
return state == PW_STREAM_STATE_STREAMING;
});
/* HACK: Try to work out the update size and total buffering size. There's
* no actual query for this, so we have to work it out from the stream time
* info, and assume it stays accurate with future updates. The stream time
* info may also not be available right away, so we have to wait until it
* is (up to about 2 seconds).
*/
int wait_count{100};
do {
pw_time ptime{};
if(int res{pw_stream_get_time_n(mStream.get(), &ptime, sizeof(ptime))})
{
ERR("Failed to get PipeWire stream time (res: %d)\n", res);
break;
}
/* The rate match size is the update size for each buffer. */
const uint updatesize{mRateMatch ? mRateMatch->size : 0u};
#if PW_CHECK_VERSION(0,3,50)
/* Assume ptime.avail_buffers+ptime.queued_buffers is the target buffer
* queue size.
*/
if(ptime.rate.denom > 0 && (ptime.avail_buffers || ptime.queued_buffers) && updatesize > 0)
{
const uint totalbuffers{ptime.avail_buffers + ptime.queued_buffers};
/* Ensure the delay is in sample frames. */
const uint64_t delay{static_cast<uint64_t>(ptime.delay) * mDevice->Frequency *
ptime.rate.num / ptime.rate.denom};
mDevice->UpdateSize = updatesize;
mDevice->BufferSize = static_cast<uint>(ptime.buffered + delay +
totalbuffers*updatesize);
break;
}
#else
/* Prior to 0.3.50, we can only measure the delay with the update size,
* assuming one buffer and no resample buffering.
*/
if(ptime.rate.denom > 0 && updatesize > 0)
{
/* Ensure the delay is in sample frames. */
const uint64_t delay{static_cast<uint64_t>(ptime.delay) * mDevice->Frequency *
ptime.rate.num / ptime.rate.denom};
mDevice->UpdateSize = updatesize;
mDevice->BufferSize = static_cast<uint>(delay + updatesize);
break;
}
#endif
if(!--wait_count)
break;
plock.unlock();
std::this_thread::sleep_for(milliseconds{20});
plock.lock();
} while(pw_stream_get_state(mStream.get(), nullptr) == PW_STREAM_STATE_STREAMING);
}
void PipeWirePlayback::stop()
{
MainloopUniqueLock plock{mLoop};
if(int res{pw_stream_set_active(mStream.get(), false)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to stop PipeWire stream (res: %d)", res};
/* Wait for the stream to stop playing. */
plock.wait([stream=mStream.get()]()
{ return pw_stream_get_state(stream, nullptr) != PW_STREAM_STATE_STREAMING; });
}
ClockLatency PipeWirePlayback::getClockLatency()
{
/* Given a real-time low-latency output, this is rather complicated to get
* accurate timing. So, here we go.
*/
/* First, get the stream time info (tick delay, ticks played, and the
* CLOCK_MONOTONIC time closest to when that last tick was played).
*/
pw_time ptime{};
if(mStream)
{
MainloopLockGuard _{mLoop};
if(int res{pw_stream_get_time_n(mStream.get(), &ptime, sizeof(ptime))})
ERR("Failed to get PipeWire stream time (res: %d)\n", res);
}
/* Now get the mixer time and the CLOCK_MONOTONIC time atomically (i.e. the
* monotonic clock closest to 'now', and the last mixer time at 'now').
*/
nanoseconds mixtime{};
timespec tspec{};
uint refcount;
do {
refcount = mDevice->waitForMix();
mixtime = GetDeviceClockTime(mDevice);
clock_gettime(CLOCK_MONOTONIC, &tspec);
std::atomic_thread_fence(std::memory_order_acquire);
} while(refcount != ReadRef(mDevice->MixCount));
/* Convert the monotonic clock, stream ticks, and stream delay to
* nanoseconds.
*/
nanoseconds monoclock{seconds{tspec.tv_sec} + nanoseconds{tspec.tv_nsec}};
nanoseconds curtic{}, delay{};
if(ptime.rate.denom < 1) UNLIKELY
{
/* If there's no stream rate, the stream hasn't had a chance to get
* going and return time info yet. Just use dummy values.
*/
ptime.now = monoclock.count();
curtic = mixtime;
delay = nanoseconds{seconds{mDevice->BufferSize}} / mDevice->Frequency;
}
else
{
/* The stream gets recreated with each reset, so include the time that
* had already passed with previous streams.
*/
curtic = mTimeBase;
/* More safely scale the ticks to avoid overflowing the pre-division
* temporary as it gets larger.
*/
curtic += seconds{ptime.ticks / ptime.rate.denom} * ptime.rate.num;
curtic += nanoseconds{seconds{ptime.ticks%ptime.rate.denom} * ptime.rate.num} /
ptime.rate.denom;
/* The delay should be small enough to not worry about overflow. */
delay = nanoseconds{seconds{ptime.delay} * ptime.rate.num} / ptime.rate.denom;
}
/* If the mixer time is ahead of the stream time, there's that much more
* delay relative to the stream delay.
*/
if(mixtime > curtic)
delay += mixtime - curtic;
/* Reduce the delay according to how much time has passed since the known
* stream time. This isn't 100% accurate since the system monotonic clock
* doesn't tick at the exact same rate as the audio device, but it should
* be good enough with ptime.now being constantly updated every few
* milliseconds with ptime.ticks.
*/
delay -= monoclock - nanoseconds{ptime.now};
/* Return the mixer time and delay. Clamp the delay to no less than 0,
* incase timer drift got that severe.
*/
ClockLatency ret{};
ret.ClockTime = mixtime;
ret.Latency = std::max(delay, nanoseconds{});
return ret;
}
class PipeWireCapture final : public BackendBase {
void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
const char *error)
{ static_cast<PipeWireCapture*>(data)->stateChangedCallback(old, state, error); }
void inputCallback();
static void inputCallbackC(void *data)
{ static_cast<PipeWireCapture*>(data)->inputCallback(); }
void open(const char *name) override;
void start() override;
void stop() override;
void captureSamples(al::byte *buffer, uint samples) override;
uint availableSamples() override;
uint64_t mTargetId{PwIdAny};
ThreadMainloop mLoop;
PwContextPtr mContext;
PwCorePtr mCore;
PwStreamPtr mStream;
spa_hook mStreamListener{};
RingBufferPtr mRing{};
static constexpr pw_stream_events CreateEvents()
{
pw_stream_events ret{};
ret.version = PW_VERSION_STREAM_EVENTS;
ret.state_changed = &PipeWireCapture::stateChangedCallbackC;
ret.process = &PipeWireCapture::inputCallbackC;
return ret;
}
public:
PipeWireCapture(DeviceBase *device) noexcept : BackendBase{device} { }
~PipeWireCapture() { if(mLoop) mLoop.stop(); }
DEF_NEWDEL(PipeWireCapture)
};
void PipeWireCapture::stateChangedCallback(pw_stream_state, pw_stream_state, const char*)
{ mLoop.signal(false); }
void PipeWireCapture::inputCallback()
{
pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())};
if(!pw_buf) UNLIKELY return;
spa_data *bufdata{pw_buf->buffer->datas};
const uint offset{minu(bufdata->chunk->offset, bufdata->maxsize)};
const uint size{minu(bufdata->chunk->size, bufdata->maxsize - offset)};
mRing->write(static_cast<char*>(bufdata->data) + offset, size / mRing->getElemSize());
pw_stream_queue_buffer(mStream.get(), pw_buf);
}
void PipeWireCapture::open(const char *name)
{
static std::atomic<uint> OpenCount{0};
uint64_t targetid{PwIdAny};
std::string devname{};
gEventHandler.waitForInit();
if(!name)
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match = devlist.cend();
if(!DefaultSourceDevice.empty())
{
auto match_default = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSourceDevice; };
match = std::find_if(devlist.cbegin(), devlist.cend(), match_default);
}
if(match == devlist.cend())
{
auto match_capture = [](const DeviceNode &n) -> bool
{ return n.mType != NodeType::Sink; };
match = std::find_if(devlist.cbegin(), devlist.cend(), match_capture);
}
if(match == devlist.cend())
{
match = devlist.cbegin();
if(match == devlist.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"No PipeWire capture device found"};
}
targetid = match->mSerial;
if(match->mType != NodeType::Sink) devname = match->mName;
else devname = MonitorPrefix+match->mName;
}
else
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match_name = [name](const DeviceNode &n) -> bool
{ return n.mType != NodeType::Sink && n.mName == name; };
auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_name);
if(match == devlist.cend() && std::strncmp(name, MonitorPrefix, MonitorPrefixLen) == 0)
{
const char *sinkname{name + MonitorPrefixLen};
auto match_sinkname = [sinkname](const DeviceNode &n) -> bool
{ return n.mType == NodeType::Sink && n.mName == sinkname; };
match = std::find_if(devlist.cbegin(), devlist.cend(), match_sinkname);
}
if(match == devlist.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"Device name \"%s\" not found", name};
targetid = match->mSerial;
devname = name;
}
if(!mLoop)
{
const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)};
const std::string thread_name{"ALSoftC" + std::to_string(count)};
mLoop = ThreadMainloop::Create(thread_name.c_str());
if(!mLoop)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire mainloop (errno: %d)", errno};
if(int res{mLoop.start()})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire mainloop (res: %d)", res};
}
MainloopUniqueLock mlock{mLoop};
if(!mContext)
{
pw_properties *cprops{pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr)};
mContext = mLoop.newContext(cprops);
if(!mContext)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire event context (errno: %d)\n", errno};
}
if(!mCore)
{
mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
if(!mCore)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to connect PipeWire event context (errno: %d)\n", errno};
}
mlock.unlock();
/* TODO: Ensure the target ID is still valid/usable and accepts streams. */
mTargetId = targetid;
if(!devname.empty())
mDevice->DeviceName = std::move(devname);
else
mDevice->DeviceName = pwireInput;
bool is51rear{false};
if(mTargetId != PwIdAny)
{
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match_id = [targetid=mTargetId](const DeviceNode &n) -> bool
{ return targetid == n.mSerial; };
auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_id);
if(match != devlist.cend())
is51rear = match->mIs51Rear;
}
spa_audio_info_raw info{make_spa_info(mDevice, is51rear, UseDevType)};
constexpr uint32_t pod_buffer_size{1024};
auto pod_buffer = std::make_unique<al::byte[]>(pod_buffer_size);
spa_pod_builder b{make_pod_builder(pod_buffer.get(), pod_buffer_size)};
const spa_pod *params[]{spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info)};
if(!params[0])
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to set PipeWire audio format parameters"};
auto&& binary = GetProcBinary();
const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
pw_properties *props{pw_properties_new(
PW_KEY_NODE_NAME, appname,
PW_KEY_NODE_DESCRIPTION, appname,
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Capture",
PW_KEY_MEDIA_ROLE, "Game",
PW_KEY_NODE_ALWAYS_PROCESS, "true",
nullptr)};
if(!props)
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to create PipeWire stream properties (errno: %d)", errno};
/* We don't actually care what the latency/update size is, as long as it's
* reasonable. Unfortunately, when unspecified PipeWire seems to default to
* around 40ms, which isn't great. So request 20ms instead.
*/
pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", (mDevice->Frequency+25) / 50,
mDevice->Frequency);
pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", mDevice->Frequency);
#ifdef PW_KEY_TARGET_OBJECT
pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%" PRIu64, mTargetId);
#else
pw_properties_setf(props, PW_KEY_NODE_TARGET, "%" PRIu64, mTargetId);
#endif
MainloopUniqueLock plock{mLoop};
mStream = PwStreamPtr{pw_stream_new(mCore.get(), "Capture Stream", props)};
if(!mStream)
throw al::backend_exception{al::backend_error::NoDevice,
"Failed to create PipeWire stream (errno: %d)", errno};
static constexpr pw_stream_events streamEvents{CreateEvents()};
pw_stream_add_listener(mStream.get(), &mStreamListener, &streamEvents, this);
constexpr pw_stream_flags Flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
| PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS};
if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_INPUT, PwIdAny, Flags, params, 1)})
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream (res: %d)", res};
/* Wait for the stream to become paused (ready to start streaming). */
plock.wait([stream=mStream.get()]()
{
const char *error{};
pw_stream_state state{pw_stream_get_state(stream, &error)};
if(state == PW_STREAM_STATE_ERROR)
throw al::backend_exception{al::backend_error::DeviceError,
"Error connecting PipeWire stream: \"%s\"", error};
return state == PW_STREAM_STATE_PAUSED;
});
plock.unlock();
setDefaultWFXChannelOrder();
/* Ensure at least a 100ms capture buffer. */
mRing = RingBuffer::Create(maxu(mDevice->Frequency/10, mDevice->BufferSize),
mDevice->frameSizeFromFmt(), false);
}
void PipeWireCapture::start()
{
MainloopUniqueLock plock{mLoop};
if(int res{pw_stream_set_active(mStream.get(), true)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start PipeWire stream (res: %d)", res};
plock.wait([stream=mStream.get()]()
{
const char *error{};
pw_stream_state state{pw_stream_get_state(stream, &error)};
if(state == PW_STREAM_STATE_ERROR)
throw al::backend_exception{al::backend_error::DeviceError,
"PipeWire stream error: %s", error ? error : "(unknown)"};
return state == PW_STREAM_STATE_STREAMING;
});
}
void PipeWireCapture::stop()
{
MainloopUniqueLock plock{mLoop};
if(int res{pw_stream_set_active(mStream.get(), false)})
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to stop PipeWire stream (res: %d)", res};
plock.wait([stream=mStream.get()]()
{ return pw_stream_get_state(stream, nullptr) != PW_STREAM_STATE_STREAMING; });
}
uint PipeWireCapture::availableSamples()
{ return static_cast<uint>(mRing->readSpace()); }
void PipeWireCapture::captureSamples(al::byte *buffer, uint samples)
{ mRing->read(buffer, samples); }
} // namespace
bool PipeWireBackendFactory::init()
{
if(!pwire_load())
return false;
const char *version{pw_get_library_version()};
if(!check_version(version))
{
WARN("PipeWire version \"%s\" too old (%s or newer required)\n", version,
pw_get_headers_version());
return false;
}
TRACE("Found PipeWire version \"%s\" (%s or newer)\n", version, pw_get_headers_version());
pw_init(0, nullptr);
if(!gEventHandler.init())
return false;
if(!GetConfigValueBool(nullptr, "pipewire", "assume-audio", false)
&& !gEventHandler.waitForAudio())
{
gEventHandler.kill();
/* TODO: Temporary warning, until PipeWire gets a proper way to report
* audio support.
*/
WARN("No audio support detected in PipeWire. See the PipeWire options in alsoftrc.sample if this is wrong.\n");
return false;
}
return true;
}
bool PipeWireBackendFactory::querySupport(BackendType type)
{ return type == BackendType::Playback || type == BackendType::Capture; }
std::string PipeWireBackendFactory::probe(BackendType type)
{
std::string outnames;
gEventHandler.waitForInit();
EventWatcherLockGuard _{gEventHandler};
auto&& devlist = DeviceNode::GetList();
auto match_defsink = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSinkDevice; };
auto match_defsource = [](const DeviceNode &n) -> bool
{ return n.mDevName == DefaultSourceDevice; };
auto sort_devnode = [](DeviceNode &lhs, DeviceNode &rhs) noexcept -> bool
{ return lhs.mId < rhs.mId; };
std::sort(devlist.begin(), devlist.end(), sort_devnode);
auto defmatch = devlist.cbegin();
switch(type)
{
case BackendType::Playback:
defmatch = std::find_if(defmatch, devlist.cend(), match_defsink);
if(defmatch != devlist.cend())
{
/* Includes null char. */
outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1);
}
for(auto iter = devlist.cbegin();iter != devlist.cend();++iter)
{
if(iter != defmatch && iter->mType != NodeType::Source)
outnames.append(iter->mName.c_str(), iter->mName.length()+1);
}
break;
case BackendType::Capture:
defmatch = std::find_if(defmatch, devlist.cend(), match_defsource);
if(defmatch != devlist.cend())
{
if(defmatch->mType == NodeType::Sink)
outnames.append(MonitorPrefix);
outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1);
}
for(auto iter = devlist.cbegin();iter != devlist.cend();++iter)
{
if(iter != defmatch)
{
if(iter->mType == NodeType::Sink)
outnames.append(MonitorPrefix);
outnames.append(iter->mName.c_str(), iter->mName.length()+1);
}
}
break;
}
return outnames;
}
BackendPtr PipeWireBackendFactory::createBackend(DeviceBase *device, BackendType type)
{
if(type == BackendType::Playback)
return BackendPtr{new PipeWirePlayback{device}};
if(type == BackendType::Capture)
return BackendPtr{new PipeWireCapture{device}};
return nullptr;
}
BackendFactory &PipeWireBackendFactory::getFactory()
{
static PipeWireBackendFactory factory{};
return factory;
}