/** * 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 #include #include #include #include #include #include #include #include #include #include #include #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). */ _Pragma("GCC diagnostic push") _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 auto ppw_core_add_listener(pw_core *core, Args&& ...args) { return pw_core_add_listener(core, std::forward(args)...); } template auto ppw_core_sync(pw_core *core, Args&& ...args) { return pw_core_sync(core, std::forward(args)...); } template auto ppw_node_subscribe_params(pw_proxy *proxy, Args&& ...args) { return pw_node_subscribe_params(proxy, std::forward(args)...); } template auto ppw_registry_add_listener(pw_registry *reg, Args&& ...args) { return pw_registry_add_listener(reg, std::forward(args)...); } constexpr auto get_pod_type(const spa_pod *pod) noexcept { return SPA_POD_TYPE(pod); } template constexpr auto get_pod_body(const spa_pod *pod, size_t count) noexcept { return al::span{static_cast(SPA_POD_BODY(pod)), count}; } template constexpr auto get_pod_body(const spa_pod *pod) noexcept { return al::span{static_cast(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 PwIdAny = PW_ID_ANY; } // namespace _Pragma("GCC diagnostic pop") namespace { using std::chrono::seconds; using std::chrono::nanoseconds; using uint = unsigned int; constexpr char pwireDevice[] = "PipeWire Output"; constexpr char pwireInput[] = "PipeWire Input"; #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) \ MAGIC(pw_stream_get_state) \ MAGIC(pw_stream_get_time) \ MAGIC(pw_stream_new_simple) \ 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) \ void *pwire_handle; #define MAKE_FUNC(f) decltype(f) * p##f; PWIRE_FUNCS(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(GetSymbol(pwire_handle, #f)); \ if(p##f == nullptr) missing_funcs += "\n" #f; \ } while(0); PWIRE_FUNCS(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_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 #define pw_stream_get_state ppw_stream_get_state #define pw_stream_get_time ppw_stream_get_time #define pw_stream_new_simple ppw_stream_new_simple #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 #endif #else constexpr bool pwire_load() { return true; } #endif 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; } 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); } friend struct MainloopUniqueLock; }; struct MainloopUniqueLock : public std::unique_lock { using std::unique_lock::unique_lock; using std::unique_lock::operator=; auto wait() const -> void { pw_thread_loop_wait(mutex()->mLoop); } template auto wait(Predicate done_waiting) const -> void { while(!done_waiting()) wait(); } }; using MainloopLockGuard = std::lock_guard; struct PwContextDeleter { void operator()(pw_context *context) const { pw_context_destroy(context); } }; using PwContextPtr = std::unique_ptr; struct PwCoreDeleter { void operator()(pw_core *core) const { pw_core_disconnect(core); } }; using PwCorePtr = std::unique_ptr; struct PwRegistryDeleter { void operator()(pw_registry *reg) const { pw_proxy_destroy(reinterpret_cast(reg)); } }; using PwRegistryPtr = std::unique_ptr; struct PwProxyDeleter { void operator()(pw_proxy *proxy) const { pw_proxy_destroy(proxy); } }; using PwProxyPtr = std::unique_ptr; struct PwStreamDeleter { void operator()(pw_stream *stream) const { pw_stream_destroy(stream); } }; using PwStreamPtr = std::unique_ptr; /* Enums for bitflags... again... *sigh* */ constexpr pw_stream_flags operator|(pw_stream_flags lhs, pw_stream_flags rhs) noexcept { return static_cast(lhs | std::underlying_type_t{rhs}); } /* 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 mProxyList; 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 mInitDone{false}; std::atomic 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(unlikely(!mInitDone.load(std::memory_order_acquire))) { 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(object)->addCallback(id, permissions, type, version, props); } void removeCallback(uint32_t id); static void removeCallbackC(void *object, uint32_t id) { static_cast(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(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; using EventWatcherLockGuard = std::lock_guard; EventManager gEventHandler; /* Enumerated devices. This is updated asynchronously as the app runs, and the * gEventHandler thread loop must be locked when accessing the list. */ constexpr auto InvalidChannelConfig = DevFmtChannels(255); struct DeviceNode { std::string mName; std::string mDevName; uint32_t mId{}; bool mCapture{}; bool mIsHeadphones{}; uint mSampleRate{}; DevFmtChannels mChannels{InvalidChannelConfig}; }; constexpr char MonitorPrefix[]{"Monitor of "}; constexpr auto MonitorPrefixLen = al::size(MonitorPrefix) - 1; constexpr char AudioSinkClass[]{"Audio/Sink"}; constexpr char AudioSourceClass[]{"Audio/Source"}; std::vector DeviceList; std::string DefaultSinkDevice; std::string DefaultSourceDevice; DeviceNode &AddDeviceNode(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(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()); } 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). */ template bool MatchChannelMap(const al::span 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; } /* 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{}; PwProxyPtr mProxy{}; spa_hook mNodeListener{}; NodeProxy(uint32_t id, PwProxyPtr proxy) : mId{id}, mProxy{std::move(proxy)} { static constexpr pw_node_events nodeEvents{CreateNodeEvents()}; pw_proxy_add_object_listener(mProxy.get(), &mNodeListener, &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(mProxy.get(), al::data(fmtids), al::size(fmtids)); } ~NodeProxy() { spa_hook_remove(&mNodeListener); } void infoCallback(const pw_node_info *info); static void infoCallbackC(void *object, const pw_node_info *info) { static_cast(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(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(unlikely(!media_class)) return; bool isCapture{}; if(al::strcasecmp(media_class, AudioSinkClass) == 0) isCapture = false; else if(al::strcasecmp(media_class, AudioSourceClass) == 0) isCapture = true; else { TRACE("Dropping device node %u which became type \"%s\"\n", info->id, media_class); RemoveDevice(info->id); return; } bool isHeadphones{}; if(const char *form_factor{spa_dict_lookup(info->props, PW_KEY_DEVICE_FORM_FACTOR)}) { if(al::strcasecmp(form_factor, "headphones") == 0 || al::strcasecmp(form_factor, "headset") == 0) isHeadphones = true; } 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; TRACE("Got %s device \"%s\"%s\n", isCapture ? "capture" : "playback", devName ? devName : "(nil)", isHeadphones ? " (headphones)" : ""); TRACE(" \"%s\" = ID %u\n", 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.mDevName = devName ? devName : ""; node.mCapture = isCapture; node.mIsHeadphones = isHeadphones; } } /* Helpers for retrieving values from params */ template struct PodInfo { }; template<> struct PodInfo { 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 { using Type = uint32_t; static auto get_value(const spa_pod *pod, uint32_t *val) { return spa_pod_get_id(pod, val); } }; template using Pod_t = typename PodInfo::Type; template uint32_t get_param_array(const spa_pod *value, const al::span,N> vals) { return spa_pod_copy_array(value, T, vals.data(), static_cast(vals.size())); } template al::optional> get_param(const spa_pod *value) { Pod_t val{}; if(PodInfo::get_value(value, &val) == 0) return val; return al::nullopt; } void parse_srate(DeviceNode *node, const spa_pod *value) { /* 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(value); /* [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, srates[0], srates[1], srates[2]); srates[0] = clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE); node->mSampleRate = static_cast(srates[0]); return; } if(choiceType == SPA_CHOICE_Enum) { if(nvals == 0) { WARN("Unexpected SPA_CHOICE_Enum count: %u\n", nvals); return; } auto srates = get_pod_body(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 %u sample rate: %d (%s)\n", node->mId, 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) { node->mSampleRate = static_cast(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(value); TRACE("Device ID %u sample rate: %d\n", node->mId, srates[0]); int srate{clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE)}; node->mSampleRate = static_cast(srate); return; } WARN("Unhandled sample rate choice type: %u\n", choiceType); } void parse_positions(DeviceNode *node, const spa_pod *value) { constexpr size_t MaxChannels{SPA_AUDIO_MAX_CHANNELS}; auto posdata = std::make_unique(MaxChannels); const al::span posarray{posdata.get(), MaxChannels}; if(auto got = get_param_array(value, posarray)) { const al::span chanmap{posarray.first(got)}; /* TODO: Does 5.1(rear) need to be tracked, or will PipeWire do the * right thing and re-route the Side-labelled 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(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); } } /* 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{}; PwProxyPtr mProxy{}; spa_hook mListener{}; MetadataProxy(uint32_t id, PwProxyPtr proxy) : mId{id}, mProxy{std::move(proxy)} { static constexpr pw_metadata_events metadataEvents{CreateMetadataEvents()}; pw_proxy_add_object_listener(mProxy.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(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; char k[128]{}; while(spa_json_get_string(&it[1], k, sizeof(k)-1) > 0) { if(std::strcmp(k, "name") == 0) { const char *name{}; int len{spa_json_next(&it[1], &name)}; if(len <= 0) break; std::string nametmp; nametmp.resize(static_cast(len)+1, '\0'); if(spa_json_parse_string(name, len, &nametmp[0]) <= 0) break; while(!nametmp.empty() && nametmp.back() == '\0') nametmp.pop_back(); TRACE("Got default %s device \"%s\"\n", isCapture ? "capture" : "playback", nametmp.c_str()); if(!isCapture) DefaultSinkDevice = nametmp; else DefaultSourceDevice = nametmp; } else { const char *v{}; if(spa_json_next(&it[1], &v) <= 0) break; } } return 0; } 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 = PwContextPtr{pw_context_new(mLoop.getLoop(), nullptr, 0)}; 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, ®istryEvents, 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 : mProxyList) al::destroy_at(node); if(mDefaultMetadata) al::destroy_at(mDefaultMetadata); } void EventManager::kill() { if(mLoop) mLoop.stop(); for(NodeProxy *node : mProxyList) al::destroy_at(node); mProxyList.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. */ const bool isGood{al::strcasecmp(media_class, AudioSinkClass) == 0 || al::strcasecmp(media_class, AudioSourceClass) == 0}; if(!isGood) return; /* Create the proxy object. */ auto proxy = PwProxyPtr{static_cast(pw_registry_bind(mRegistry.get(), 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 = static_cast(pw_proxy_get_user_data(proxy.get())); mProxyList.emplace_back(al::construct_at(node, id, std::move(proxy))); 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 proxy = PwProxyPtr{static_cast(pw_registry_bind(mRegistry.get(), id, type, version, sizeof(MetadataProxy)))}; if(!proxy) { ERR("Failed to create metadata proxy object (errno: %d)\n", errno); return; } auto *mdata = static_cast(pw_proxy_get_user_data(proxy.get())); mDefaultMetadata = al::construct_at(mdata, id, std::move(proxy)); 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; } 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, 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 map{}; switch(device->FmtChans) { case DevFmtMono: map = MonoMap; break; case DevFmtStereo: map = StereoMap; break; case DevFmtQuad: map = QuadMap; break; case DevFmtX51: map = X51Map; break; case DevFmtX61: map = X61Map; break; case DevFmtX71: map = X71Map; break; case DevFmtAmbi3D: info.flags |= SPA_AUDIO_FLAG_UNPOSITIONED; info.channels = device->channelsFromFmt(); break; } if(!map.empty()) { info.channels = static_cast(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(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(data)->ioChangedCallback(id, area, size); } void outputCallback(); static void outputCallbackC(void *data) { static_cast(data)->outputCallback(); } void open(const char *name) override; bool reset() override; void start() override; void stop() override; ClockLatency getClockLatency() override; uint32_t mTargetId{PwIdAny}; nanoseconds mTimeBase{0}; ThreadMainloop mLoop; PwStreamPtr mStream; spa_io_rate_match *mRateMatch{}; std::unique_ptr 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(); DEF_NEWDEL(PipeWirePlayback) }; PipeWirePlayback::~PipeWirePlayback() { /* Stop the mainloop so the stream can be properly destroyed. */ if(mLoop) mLoop.stop(); } 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(area); break; } } void PipeWirePlayback::outputCallback() { pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())}; if(unlikely(!pw_buf)) return; /* 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). */ spa_data *datas{pw_buf->buffer->datas}; const size_t chancount{minu(mNumChannels, pw_buf->buffer->n_datas)}; /* TODO: How many samples should actually be written? 'maxsize' can be 16k * samples, which is excessive (~341ms @ 48khz). SPA_IO_RateMatch contains * a 'size' field that apparently indicates how many samples should be * written per update, but it's not obviously right. */ uint length{mRateMatch ? mRateMatch->size : mDevice->UpdateSize}; for(size_t i{0};i < chancount;++i) { length = minu(length, datas[i].maxsize/sizeof(float)); mChannelPtrs[i] = static_cast(datas[i].data); } mDevice->renderSamples({mChannelPtrs.get(), chancount}, length); for(size_t i{0};i < chancount;++i) { datas[i].chunk->offset = 0; datas[i].chunk->stride = sizeof(float); datas[i].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 OpenCount{0}; uint32_t targetid{PwIdAny}; std::string devname{}; gEventHandler.waitForInit(); if(!name) { EventWatcherLockGuard _{gEventHandler}; auto match = DeviceList.cend(); if(!DefaultSinkDevice.empty()) { auto match_default = [](const DeviceNode &n) -> bool { return n.mDevName == DefaultSinkDevice; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_default); } if(match == DeviceList.cend()) { auto match_playback = [](const DeviceNode &n) -> bool { return !n.mCapture; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_playback); if(match == DeviceList.cend()) throw al::backend_exception{al::backend_error::NoDevice, "No PipeWire playback device found"}; } targetid = match->mId; devname = match->mName; } else { EventWatcherLockGuard _{gEventHandler}; 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) { const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)}; const std::string thread_name{"ALSoftP" + std::to_string(count)}; mLoop = ThreadMainloop{pw_thread_loop_new(thread_name.c_str(), nullptr)}; 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}; } /* 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; } mRateMatch = nullptr; mTimeBase = GetDeviceClockTime(mDevice); /* If connecting to a specific device, update various device parameters to * match its format. */ mDevice->Flags.reset(DirectEar); 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(match->mSampleRate) / mDevice->Frequency}; mDevice->Frequency = match->mSampleRate; mDevice->UpdateSize = static_cast(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; if(match->mChannels == DevFmtStereo && match->mIsHeadphones) mDevice->Flags.set(DirectEar); } } /* 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)}; /* 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(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"}; pw_properties *props{pw_properties_new( 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}; auto&& binary = GetProcBinary(); const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"}; /* TODO: Which properties are actually needed here? Any others that could * be useful? */ pw_properties_set(props, PW_KEY_NODE_NAME, appname); pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, appname); 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); MainloopUniqueLock plock{mLoop}; static constexpr pw_stream_events streamEvents{CreateEvents()}; /* The stream takes overship of 'props', even in the case of failure. */ mStream = PwStreamPtr{pw_stream_new_simple(mLoop.getLoop(), "Playback Stream", props, &streamEvents, this)}; if(!mStream) throw al::backend_exception{al::backend_error::NoDevice, "Failed to create PipeWire stream (errno: %d)", errno}; 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, mTargetId, Flags, ¶ms, 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). */ pw_stream_state state{}; const char *error{}; plock.wait([stream=mStream.get(),&state,&error]() { 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->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(); mChannelPtrs = std::make_unique(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). */ pw_stream_state state{}; const char *error{}; plock.wait([stream=mStream.get(),&state,&error]() { state = pw_stream_get_state(stream, &error); return state != PW_STREAM_STATE_PAUSED; }); if(state == PW_STREAM_STATE_ERROR) throw al::backend_exception{al::backend_error::DeviceError, "PipeWire stream error: %s", error ? error : "(unknown)"}; if(state == PW_STREAM_STATE_STREAMING && mRateMatch && mRateMatch->size) { mDevice->UpdateSize = mRateMatch->size; mDevice->BufferSize = mDevice->UpdateSize * 2; } } 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(mStream.get(), &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(unlikely(ptime.rate.denom < 1)) { /* 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(data)->stateChangedCallback(old, state, error); } void inputCallback(); static void inputCallbackC(void *data) { static_cast(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; uint32_t mTargetId{PwIdAny}; ThreadMainloop mLoop; PwStreamPtr mStream; 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(); DEF_NEWDEL(PipeWireCapture) }; PipeWireCapture::~PipeWireCapture() { if(mLoop) mLoop.stop(); } 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(unlikely(!pw_buf)) 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(bufdata->data) + offset, size / mRing->getElemSize()); pw_stream_queue_buffer(mStream.get(), pw_buf); } void PipeWireCapture::open(const char *name) { static std::atomic OpenCount{0}; uint32_t targetid{PwIdAny}; std::string devname{}; gEventHandler.waitForInit(); if(!name) { EventWatcherLockGuard _{gEventHandler}; auto match = DeviceList.cend(); if(!DefaultSourceDevice.empty()) { auto match_default = [](const DeviceNode &n) -> bool { return n.mDevName == DefaultSourceDevice; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_default); } if(match == DeviceList.cend()) { auto match_capture = [](const DeviceNode &n) -> bool { return n.mCapture; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_capture); } if(match == DeviceList.cend()) { auto match_playback = [](const DeviceNode &n) -> bool { return !n.mCapture; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_playback); if(match == DeviceList.cend()) throw al::backend_exception{al::backend_error::NoDevice, "No PipeWire capture device found"}; } targetid = match->mId; if(match->mCapture) devname = match->mName; else devname = MonitorPrefix+match->mName; } else { EventWatcherLockGuard _{gEventHandler}; 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() && std::strncmp(name, MonitorPrefix, MonitorPrefixLen) == 0) { const char *sinkname{name + MonitorPrefixLen}; auto match_sinkname = [sinkname](const DeviceNode &n) -> bool { return !n.mCapture && n.mName == sinkname; }; match = std::find_if(DeviceList.cbegin(), DeviceList.cend(), match_sinkname); } if(match == DeviceList.cend()) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found", name}; targetid = match->mId; 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{pw_thread_loop_new(thread_name.c_str(), nullptr)}; 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}; } /* 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; spa_audio_info_raw info{make_spa_info(mDevice, UseDevType)}; constexpr uint32_t pod_buffer_size{1024}; auto pod_buffer = std::make_unique(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"}; pw_properties *props{pw_properties_new( 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}; auto&& binary = GetProcBinary(); const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"}; pw_properties_set(props, PW_KEY_NODE_NAME, appname); pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, appname); /* 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); MainloopUniqueLock plock{mLoop}; static constexpr pw_stream_events streamEvents{CreateEvents()}; mStream = PwStreamPtr{pw_stream_new_simple(mLoop.getLoop(), "Capture Stream", props, &streamEvents, this)}; if(!mStream) throw al::backend_exception{al::backend_error::NoDevice, "Failed to create PipeWire stream (errno: %d)", errno}; 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, mTargetId, 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). */ pw_stream_state state{}; const char *error{}; plock.wait([stream=mStream.get(),&state,&error]() { 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}; pw_stream_state state{}; const char *error{}; plock.wait([stream=mStream.get(),&state,&error]() { state = pw_stream_get_state(stream, &error); return state != PW_STREAM_STATE_PAUSED; }); if(state == PW_STREAM_STATE_ERROR) throw al::backend_exception{al::backend_error::DeviceError, "PipeWire stream error: %s", error ? error : "(unknown)"}; } 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(mRing->readSpace()); } void PipeWireCapture::captureSamples(al::byte *buffer, uint samples) { mRing->read(buffer, samples); } } // namespace bool PipeWireBackendFactory::init() { if(!pwire_load()) return false; 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 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(DeviceList.begin(), DeviceList.end(), sort_devnode); auto defmatch = DeviceList.cbegin(); switch(type) { case BackendType::Playback: defmatch = std::find_if(defmatch, DeviceList.cend(), match_defsink); if(defmatch != DeviceList.cend()) { /* Includes null char. */ outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1); } for(auto iter = DeviceList.cbegin();iter != DeviceList.cend();++iter) { if(iter != defmatch && !iter->mCapture) outnames.append(iter->mName.c_str(), iter->mName.length()+1); } break; case BackendType::Capture: defmatch = std::find_if(defmatch, DeviceList.cend(), match_defsource); if(defmatch != DeviceList.cend()) { if(!defmatch->mCapture) outnames.append(MonitorPrefix); outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1); } for(auto iter = DeviceList.cbegin();iter != DeviceList.cend();++iter) { if(iter != defmatch && iter->mCapture) outnames.append(iter->mName.c_str(), iter->mName.length()+1); } for(auto iter = DeviceList.cbegin();iter != DeviceList.cend();++iter) { if(iter != defmatch && !iter->mCapture) outnames.append(MonitorPrefix).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; }