diff options
Diffstat (limited to 'alc')
-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; } |