diff options
author | Chris Robinson <[email protected]> | 2021-08-14 08:34:58 -0700 |
---|---|---|
committer | Chris Robinson <[email protected]> | 2021-08-14 08:34:58 -0700 |
commit | 4800749c8e9822fba07408e132357786b83ea761 (patch) | |
tree | a0147a2774bdd71e48f914491675a873bd04feae /alc/backends | |
parent | dc9b39f4192b0ab28bdfb2699fbcef591f4cf290 (diff) |
Add enumeration support to the PipeWire backend
This does not currently track the default devices, and simply gives which came
up first. More work is needed to find the default source/sink node IDs.
Diffstat (limited to 'alc/backends')
-rw-r--r-- | alc/backends/pipewire.cpp | 712 |
1 files changed, 667 insertions, 45 deletions
diff --git a/alc/backends/pipewire.cpp b/alc/backends/pipewire.cpp index e5c4db72..3ed26496 100644 --- a/alc/backends/pipewire.cpp +++ b/alc/backends/pipewire.cpp @@ -26,6 +26,7 @@ #include <atomic> #include <cstring> #include <cerrno> +#include <list> #include <memory> #include <mutex> #include <stdint.h> @@ -34,7 +35,9 @@ #include "albyte.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" @@ -51,6 +54,36 @@ _Pragma("GCC diagnostic ignored \"-Weverything\"") #include "spa/param/audio/raw.h" #include "spa/param/param.h" #include "spa/pod/builder.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_node_subscribe_params(pw_proxy *proxy, Args&& ...args) +{ return pw_node_subscribe_params(proxy, 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)...); } + + +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) noexcept +{ return static_cast<T*>(SPA_POD_BODY(pod)); } + +constexpr auto make_pod_builder(void *data, uint32_t size) noexcept +{ return SPA_POD_BUILDER_INIT(data, size); } + +constexpr auto PwIdAny = PW_ID_ANY; + +} // namespace _Pragma("GCC diagnostic pop") namespace { @@ -62,13 +95,18 @@ constexpr char pwireDevice[] = "PipeWire Output"; #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_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_connect) \ MAGIC(pw_stream_dequeue_buffer) \ MAGIC(pw_stream_destroy) \ @@ -92,13 +130,18 @@ PWIRE_FUNCS(MAKE_FUNC) #undef MAKE_FUNC #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_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_connect ppw_stream_connect #define pw_stream_dequeue_buffer ppw_stream_dequeue_buffer #define pw_stream_destroy ppw_stream_destroy @@ -200,42 +243,549 @@ using PwStreamPtr = std::unique_ptr<pw_stream,PwStreamDeleter>; constexpr pw_stream_flags operator|(pw_stream_flags lhs, pw_stream_flags rhs) noexcept { return static_cast<pw_stream_flags>(lhs | uint{rhs}); } -/* Using PW_ID_ANY causes a compiler warning, so use our own variable with the - * same type/value. + +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 +}; + +/** + * Checks if every channel in 'map1' exists in 'map0' (that is, map0 is equal + * to or a superset of map1). */ -constexpr uint32_t IdAny{0xffffffff}; +template<size_t N> +bool MatchChannelMap(const al::span<uint32_t> map0, const spa_audio_channel (&map1)[N]) +{ + for(const spa_audio_channel chid : map1) + { + if(std::find(map0.begin(), map0.end(), chid) == map0.end()) + return false; + } + return true; +} + + +/* 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. + * + * TODO: Find the default sink/source nodes. Also find the "monitor" source + * nodes relating to sink nodes. + */ + +struct NodeProxy; + +/* 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{}; + pw_core *mCore{}; + pw_context *mContext{}; + pw_registry *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*> mProxyList; + + /* 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}; + int mInitSeq{}; -/* SPA_POD_BUILDER_INIT causes a compiler warning, so make this function for - * the same functionality. + bool init(); + ~EventManager(); + + auto lock() const { return mLoop.lock(); } + auto unlock() const { return mLoop.unlock(); } + + /** + * Waits for initialization to finish. The event manager must be locked + * when calling this. + */ + void waitForInit() + { + while UNLIKELY(!mInitDone.load(std::memory_order_acquire)) + mLoop.wait(); + } + + 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, 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 const pw_registry_events sRegistryEvents; + 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 const pw_core_events sCoreEvents; + static constexpr pw_core_events CreateCoreEvents() + { + pw_core_events ret{}; + ret.version = PW_VERSION_NODE_EVENTS; + ret.done = &EventManager::coreCallbackC; + return ret; + } +}; +using EventWatcherUniqueLock = std::unique_lock<EventManager>; +using EventWatcherLockGuard = std::lock_guard<EventManager>; + +const pw_core_events EventManager::sCoreEvents{EventManager::CreateCoreEvents()}; +const pw_registry_events EventManager::sRegistryEvents{EventManager::CreateRegistryEvents()}; +EventManager gEventHandler; + + +/* Enumerated devices. This is updated asynchronously as the app runs, and the + * gEventHandler thread loop must be locked when accessing the list. */ -inline spa_pod_builder make_pod_builder(void *data, uint32_t size) noexcept +constexpr auto InvalidChannelConfig = DevFmtChannels(255); +struct DeviceNode { + std::string mName; + + uint32_t mId{}; + bool mCapture{}; + + uint mSampleRate{}; + DevFmtChannels mChannels{InvalidChannelConfig}; +}; +std::vector<DeviceNode> DeviceList; + +DeviceNode &AddDeviceNode(uint32_t id) { - spa_pod_builder ret{}; - spa_pod_builder_init(&ret, data, size); - return ret; + 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(DeviceList.begin(), DeviceList.end(), match_id); + if(match != DeviceList.end()) return *match; + + DeviceList.emplace_back(); + auto &n = DeviceList.back(); + n.mId = id; + return n; +} + +DeviceNode *FindDeviceNode(uint32_t id) +{ + auto match_id = [id](DeviceNode &n) noexcept -> bool + { return n.mId == id; }; + + auto match = std::find_if(DeviceList.begin(), DeviceList.end(), match_id); + if(match != DeviceList.end()) return std::addressof(*match); + + return nullptr; +} + +void RemoveDevice(uint32_t id) +{ + auto match_id = [id](DeviceNode &n) noexcept -> bool + { return n.mId == id; }; + + auto end = std::remove_if(DeviceList.begin(), DeviceList.end(), match_id); + DeviceList.erase(end, DeviceList.end()); +} + + +/* A generic PipeWire node proxy object used to track changes to sink and + * source nodes. + */ +struct NodeProxy { + uint32_t mId{}; + + pw_proxy *mProxy{nullptr}; + spa_hook mNodeListener{}; + + NodeProxy(uint32_t id, pw_proxy *proxy) + : mId{id}, mProxy{proxy} + { + pw_proxy_add_object_listener(mProxy, &mNodeListener, &sNodeEvents, 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(mProxy, al::data(fmtids), al::size(fmtids)); + } + ~NodeProxy() + { + spa_hook_remove(&mNodeListener); + pw_proxy_destroy(mProxy); + } + + + 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); } + + static const pw_node_events sNodeEvents; + 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; + } +}; +const pw_node_events NodeProxy::sNodeEvents{NodeProxy::CreateNodeEvents()}; + +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 UNLIKELY(!media_class) return; + + bool isCapture{}; + if(al::strcasecmp(media_class, "Audio/Sink") == 0) + isCapture = false; + else if(al::strcasecmp(media_class, "Audio/Source") == 0) + isCapture = true; + else + { + TRACE("Dropping device node %u which became type \"%s\"\n", info->id, media_class); + RemoveDevice(info->id); + return; + } + + const char *nodeName{spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)}; + if(!nodeName) nodeName = spa_dict_lookup(info->props, PW_KEY_NODE_NICK); + if(!nodeName) nodeName = spa_dict_lookup(info->props, PW_KEY_NODE_NAME); + + TRACE("Got %s device \"%s\" = ID %u\n", isCapture ? "capture" : "playback", + nodeName ? nodeName : "(nil)", info->id); + + DeviceNode &node = AddDeviceNode(info->id); + if(nodeName && *nodeName) node.mName = nodeName; + else node.mName = "PipeWire node #"+std::to_string(info->id); + node.mCapture = isCapture; + } +} + +/* 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> +uint32_t get_param_range(const spa_pod *value, const al::span<Pod_t<T>,3> vals) +{ + uint32_t nvals{}, choice{}; + value = spa_pod_get_values(value, &nvals, &choice); + + if(get_pod_type(value) == T && nvals >= vals.size() && choice == SPA_CHOICE_Range) + { + std::copy_n(get_pod_body<Pod_t<T>>(value), vals.size(), vals.begin()); + return nvals; + } + + return 0; +} + +template<uint32_t T, size_t N> +uint32_t get_param_array(const spa_pod *value, const al::span<Pod_t<T>,N> vals) +{ + return spa_pod_copy_array(value, T, vals.data(), static_cast<uint32_t>(vals.size())); +} + +template<uint32_t T> +al::optional<Pod_t<T>> get_param(const spa_pod *value) +{ + Pod_t<T> val{}; + if(PodInfo<T>::get_value(value, &val) == 0) + return al::make_optional(val); + return al::nullopt; +} + +void parse_srate(DeviceNode *node, const spa_pod *value) +{ + /* TODO: Can this be anything else? An "enum" choice? Floats? Or will the + * sample rate always be a range choice between ints? + */ + if(get_pod_type(value) == SPA_TYPE_Choice) + { + int32_t srate[3]{}; + if(get_param_range<SPA_TYPE_Int>(value, al::span<int32_t,3>{srate}) < 1) + return; + + /* [0] is the default, [1] is the min, and [2] is the max. */ + TRACE("Device ID %u sample rate: %d (range: %d -> %d)\n", node->mId, srate[0], srate[1], + srate[2]); + srate[0] = clampi(srate[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE); + node->mSampleRate = static_cast<uint>(srate[0]); + } +} + +void parse_positions(DeviceNode *node, const spa_pod *value) +{ + constexpr size_t MaxChannels{SPA_AUDIO_MAX_CHANNELS}; + + auto posdata = std::make_unique<uint32_t[]>(MaxChannels); + const al::span<uint32_t,MaxChannels> pos{posdata.get(), MaxChannels}; + if(auto got = get_param_array<SPA_TYPE_Id>(value, pos)) + { + const al::span<uint32_t> chanmap{pos.first(got)}; + + /* TODO: Does 5.1(rear) need to be tracked, or will PipeWire do the + * right thing and re-route the Side-lavelled Surround channels to + * Rear-labelled Surround? + */ + if(got >= 8 && MatchChannelMap(chanmap, X71Map)) + node->mChannels = DevFmtX71; + else if(got >= 7 && MatchChannelMap(chanmap, X61Map)) + node->mChannels = DevFmtX61; + else if(got >= 6 && MatchChannelMap(chanmap, X51Map)) + node->mChannels = DevFmtX51; + else if(got >= 6 && MatchChannelMap(chanmap, X51RearMap)) + node->mChannels = DevFmtX51; + else if(got >= 4 && MatchChannelMap(chanmap, QuadMap)) + node->mChannels = DevFmtQuad; + else if(got >= 2 && MatchChannelMap(chanmap, StereoMap)) + node->mChannels = DevFmtStereo; + else if(got >= 1) + node->mChannels = DevFmtMono; + TRACE("Device ID %u got %u position%s for %s\n", node->mId, got, (got==1)?"":"s", + DevFmtChannelsString(node->mChannels)); + } +} + +void parse_channels(DeviceNode *node, const spa_pod *value) +{ + /* As a fallback with just a channel count, just assume mono or stereo. */ + if(auto chans = get_param<SPA_TYPE_Int>(value)) + { + if(*chans >= 2) + node->mChannels = DevFmtStereo; + else if(*chans >= 1) + node->mChannels = DevFmtMono; + TRACE("Device ID %u got %d channel%s for %s\n", node->mId, *chans, (*chans==1)?"":"s", + DevFmtChannelsString(node->mChannels)); + } +} + +void NodeProxy::paramCallback(int, uint32_t id, uint32_t, uint32_t, const spa_pod *param) +{ + if(id == SPA_PARAM_EnumFormat) + { + DeviceNode *node{FindDeviceNode(mId)}; + if UNLIKELY(!node) return; + + if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_rate)}) + parse_srate(node, &prop->value); + + if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_position)}) + parse_positions(node, &prop->value); + else if((prop=spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_channels)) != nullptr) + parse_channels(node, &prop->value); + } +} + + +bool EventManager::init() +{ + mLoop = ThreadMainloop{pw_thread_loop_new("PWEventThread", nullptr)}; + if(!mLoop) + { + ERR("Failed to create PipeWire event thread loop (errno: %d)\n", errno); + return false; + } + + mContext = pw_context_new(mLoop.getLoop(), nullptr, 0); + if(!mContext) + { + ERR("Failed to create PipeWire event context (errno: %d)\n", errno); + return false; + } + + mCore = pw_context_connect(mContext, nullptr, 0); + if(!mCore) + { + ERR("Failed to connect PipeWire event context (errno: %d)\n", errno); + return false; + } + + mRegistry = pw_core_get_registry(mCore, PW_VERSION_REGISTRY, 0); + if(!mRegistry) + { + ERR("Failed to get PipeWire event registry (errno: %d)\n", errno); + return false; + } + + ppw_registry_add_listener(mRegistry, &mRegistryListener, &sRegistryEvents, this); + ppw_core_add_listener(mCore, &mCoreListener, &sCoreEvents, this); + + /* Set an initial sequence ID for initialization, to trigger after the + * registry is first populated. + */ + mInitSeq = ppw_core_sync(mCore, 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 : mProxyList) + al::destroy_at(node); + mProxyList.clear(); + + if(mRegistry) pw_proxy_destroy(reinterpret_cast<pw_proxy*>(mRegistry)); + if(mCore) pw_core_disconnect(mCore); + if(mContext) pw_context_destroy(mContext); +} + +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. */ + const bool isGood{al::strcasecmp(media_class, "Audio/Sink") == 0 + || al::strcasecmp(media_class, "Audio/Source") == 0}; + if(!isGood) + { + TRACE("Skipping node type \"%s\"\n", media_class); + return; + } + + /* Create the proxy object. */ + auto *proxy = static_cast<pw_proxy*>(pw_registry_bind(mRegistry, id, type, version, + sizeof(NodeProxy))); + if(!proxy) + { + ERR("Failed to create node proxy object (errno: %d)\n", errno); + return; + } + + /* Initialize the NodeProxy to hold the proxy object, add it to the + * active proxy list, and update the sync point. + */ + auto *node = ::new(pw_proxy_get_user_data(proxy)) NodeProxy{id, proxy}; + mProxyList.emplace_back(node); + syncInit(); + } +} + +void EventManager::removeCallback(uint32_t id) +{ + RemoveDevice(id); + + auto elem = mProxyList.begin(); + while(elem != mProxyList.end()) + { + NodeProxy *node{*elem}; + if(node->mId == id) + { + al::destroy_at(node); + elem = mProxyList.erase(elem); + continue; + } + ++elem; + } +} + +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, use_f32p_e use_f32p) { - static 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 - }, 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 - }; - spa_audio_info_raw info{}; if(use_f32p) { @@ -297,6 +847,7 @@ struct PipeWirePlayback final : public BackendBase { void stop() override; ThreadMainloop mLoop; + uint32_t mTargetId{PwIdAny}; PwStreamPtr mStream; std::unique_ptr<float*[]> mChannelPtrs; uint mNumChannels{}; @@ -333,9 +884,6 @@ void PipeWirePlayback::stateChangedCallback(pw_stream_state, pw_stream_state, co void PipeWirePlayback::outputCallback() { - /* TODO: Should all buffers be filled? There can be more than one buffer to - * dequeue, but example code only ever does one. - */ pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())}; if UNLIKELY(!pw_buf) return; @@ -374,11 +922,38 @@ void PipeWirePlayback::open(const char *name) { static std::atomic<uint> OpenCount{0}; + uint32_t targetid{PwIdAny}; + std::string devname{}; if(!name) - name = pwireDevice; - else if(strcmp(name, pwireDevice) != 0) - throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found", - name}; + { + EventWatcherLockGuard _{gEventHandler}; + gEventHandler.waitForInit(); + + auto match_playback = [](const DeviceNode &n) -> bool + { return !n.mCapture; }; + auto match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_playback); + if(match == DeviceList.cend()) + throw al::backend_exception{al::backend_error::NoDevice, + "Device name \"%s\" not found", name}; + + targetid = match->mId; + devname = match->mName; + } + else + { + EventWatcherLockGuard _{gEventHandler}; + gEventHandler.waitForInit(); + + auto match_name = [name](const DeviceNode &n) -> bool + { return !n.mCapture && n.mName == name; }; + auto match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_name); + if(match == DeviceList.cend()) + throw al::backend_exception{al::backend_error::NoDevice, + "Device name \"%s\" not found", name}; + + targetid = match->mId; + devname = match->mName; + } if(!mLoop) { @@ -393,7 +968,13 @@ void PipeWirePlayback::open(const char *name) "Failed to start PipeWire mainloop (res: %d)", res}; } - mDevice->DeviceName = name; + /* 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() @@ -404,8 +985,33 @@ bool PipeWirePlayback::reset() mStream = nullptr; } - /* TODO: Detect format from output device to avoid unnecessary conversions. - * Force planar 32-bit float output for playback. This is what PipeWire + /* If connecting to a specific device, update various device parameters to + * match its format. + */ + mDevice->IsHeadphones = false; + if(mTargetId != PwIdAny) + { + EventWatcherLockGuard _{gEventHandler}; + + auto match_id = [targetid=mTargetId](const DeviceNode &n) -> bool + { return targetid == n.mId; }; + auto match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_id); + if(match != DeviceList.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}; + mDevice->Frequency = match->mSampleRate; + mDevice->UpdateSize = static_cast<uint>(clampd(mDevice->UpdateSize*scale + 0.5, + 64.0, 8192.0)); + mDevice->BufferSize = mDevice->UpdateSize * 2; + } + if(!mDevice->Flags.test(ChannelsRequest) && match->mChannels != InvalidChannelConfig) + mDevice->FmtChans = match->mChannels; + } + } + /* 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, ForceF32Planar)}; @@ -443,19 +1049,16 @@ bool PipeWirePlayback::reset() mDevice->Frequency); MainloopUniqueLock plock{mLoop}; + /* The stream takes overship of 'props', even in the case of failure. */ mStream = PwStreamPtr{pw_stream_new_simple(mLoop.getLoop(), "Playback Stream", props, &sEvents, this)}; if(!mStream) - { - plock.unlock(); - pw_properties_free(props); throw al::backend_exception{al::backend_error::NoDevice, "Failed to create PipeWire stream (errno: %d)", errno}; - } - static constexpr pw_stream_flags Flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE + 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_OUTPUT, IdAny, Flags, ¶ms, 1)}) + if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_OUTPUT, mTargetId, Flags, ¶ms, 1)}) throw al::backend_exception{al::backend_error::DeviceError, "Error connecting PipeWire stream (res: %d)", res}; @@ -469,6 +1072,10 @@ bool PipeWirePlayback::reset() "Error connecting PipeWire stream: \"%s\"", error}; mLoop.wait(); } + /* TODO: Update mDevice->BufferSize with the total known buffering delay + * from the head of this playback stream to the tail of the device output. + */ + mDevice->BufferSize = mDevice->UpdateSize * 2; plock.unlock(); mNumChannels = mDevice->channelsFromFmt(); @@ -493,6 +1100,11 @@ void PipeWirePlayback::stop() 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. */ + pw_stream_state state{}; + while((state=pw_stream_get_state(mStream.get(), nullptr)) == PW_STREAM_STATE_STREAMING) + mLoop.wait(); } } // namespace @@ -507,7 +1119,7 @@ bool PipeWireBackendFactory::init() /* TODO: Check that audio devices are supported. */ - return true; + return gEventHandler.init(); } bool PipeWireBackendFactory::querySupport(BackendType type) @@ -516,15 +1128,25 @@ bool PipeWireBackendFactory::querySupport(BackendType type) std::string PipeWireBackendFactory::probe(BackendType type) { std::string outnames; + + EventWatcherLockGuard _{gEventHandler}; + gEventHandler.waitForInit(); switch(type) { case BackendType::Playback: - /* Includes null char. */ - outnames.append(pwireDevice, sizeof(pwireDevice)); + for(const auto &node : DeviceList) + { + if(!node.mCapture) + { + /* Includes null char. */ + outnames.append(node.mName.c_str(), node.mName.length()+1); + } + } break; case BackendType::Capture: break; } + return outnames; } |