|
| 1 | +#include "soundio/pipewireenumerator.h" |
| 2 | + |
| 3 | +#include <pipewire/pipewire.h> |
| 4 | +#include <spa/utils/defs.h> |
| 5 | +#include <spa/utils/dict.h> |
| 6 | + |
| 7 | +#include <cstdlib> |
| 8 | + |
| 9 | +#include "moc_pipewireenumerator.cpp" |
| 10 | +#include "soundio/sounddevice.h" |
| 11 | +#include "soundio/sounddevicepipewire.h" |
| 12 | +#include "soundio/soundmanager.h" |
| 13 | + |
| 14 | +namespace { |
| 15 | + |
| 16 | +static const char* find_node_name(const struct spa_dict* props) { |
| 17 | + static const char* const name_keys[] = { |
| 18 | + PW_KEY_NODE_NAME, |
| 19 | + PW_KEY_NODE_DESCRIPTION, |
| 20 | + PW_KEY_APP_NAME, |
| 21 | + PW_KEY_MEDIA_NAME, |
| 22 | + }; |
| 23 | + |
| 24 | + SPA_FOR_EACH_ELEMENT_VAR(name_keys, key) { |
| 25 | + const char* name = spa_dict_lookup(props, *key); |
| 26 | + if (name) { |
| 27 | + return name; |
| 28 | + } |
| 29 | + } |
| 30 | + return nullptr; |
| 31 | +} |
| 32 | + |
| 33 | +} // namespace |
| 34 | + |
| 35 | +PipewireEnumerator::PipewireEnumerator( |
| 36 | + [[maybe_unused]] UserSettingsPointer pConfig, SoundManager* pManager) |
| 37 | + : m_pManager(pManager) { |
| 38 | + pw_init(nullptr, nullptr); |
| 39 | + |
| 40 | + m_pThreadLoop = pw_thread_loop_new("mixxx_pw_loop", nullptr); |
| 41 | + m_pContext = pw_context_new(pw_thread_loop_get_loop(m_pThreadLoop), nullptr, 0); |
| 42 | + m_pCore = pw_context_connect(m_pContext, nullptr, 0); |
| 43 | + m_pRegistry = pw_core_get_registry(m_pCore, PW_VERSION_REGISTRY, 0); |
| 44 | + |
| 45 | + spa_zero(m_registryListener); |
| 46 | + pw_registry_add_listener(m_pRegistry, &m_registryListener, ®istry_events, this); |
| 47 | + |
| 48 | + pw_thread_loop_start(m_pThreadLoop); |
| 49 | +} |
| 50 | + |
| 51 | +PipewireEnumerator::~PipewireEnumerator() { |
| 52 | + spa_hook_remove(&m_registryListener); |
| 53 | + spa_hook_remove(&m_metadataListener); |
| 54 | + pw_proxy_destroy((struct pw_proxy*)m_pRegistry); |
| 55 | + pw_proxy_destroy((struct pw_proxy*)m_pMetadata); |
| 56 | + pw_core_disconnect(m_pCore); |
| 57 | + pw_context_destroy(m_pContext); |
| 58 | + pw_thread_loop_destroy(m_pThreadLoop); |
| 59 | + pw_deinit(); |
| 60 | +} |
| 61 | + |
| 62 | +QList<mixxx::audio::SampleRate> PipewireEnumerator::getSampleRates() const { |
| 63 | + if (m_samplerates.empty()) { |
| 64 | + return QList<mixxx::audio::SampleRate>{ |
| 65 | + mixxx::audio::SampleRate(44100), |
| 66 | + mixxx::audio::SampleRate(48000), |
| 67 | + mixxx::audio::SampleRate(96000), |
| 68 | + }; |
| 69 | + } |
| 70 | + |
| 71 | + return m_samplerates; |
| 72 | +} |
| 73 | + |
| 74 | +void PipewireEnumerator::registryEventGlobalOuter(void* data, |
| 75 | + uint32_t id, |
| 76 | + uint32_t permissions, |
| 77 | + const char* type, |
| 78 | + uint32_t version, |
| 79 | + const struct spa_dict* props) { |
| 80 | + ((PipewireEnumerator*)data)->registryEventGlobal(id, permissions, type, version, props); |
| 81 | +} |
| 82 | + |
| 83 | +void PipewireEnumerator::registryEventGlobal(uint32_t id, |
| 84 | + [[maybe_unused]] uint32_t permissions, |
| 85 | + const char* type, |
| 86 | + [[maybe_unused]] uint32_t version, |
| 87 | + const struct spa_dict* props) { |
| 88 | + if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) { |
| 89 | + const char* name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); |
| 90 | + if (strcmp(name, "settings")) { |
| 91 | + return; |
| 92 | + } |
| 93 | + |
| 94 | + void* data = pw_registry_bind(m_pRegistry, |
| 95 | + id, |
| 96 | + PW_TYPE_INTERFACE_Metadata, |
| 97 | + PW_VERSION_METADATA, |
| 98 | + 0); |
| 99 | + m_pMetadata = static_cast<pw_metadata*>(data); |
| 100 | + pw_metadata_add_listener(m_pMetadata, &m_metadataListener, &metadataEvents, this); |
| 101 | + } else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { |
| 102 | + const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); |
| 103 | + const char* media_type = spa_dict_lookup(props, PW_KEY_MEDIA_TYPE); |
| 104 | + |
| 105 | + bool isAudioNode = (media_class && strstr(media_class, "Audio")) || |
| 106 | + (media_type && strstr(media_type, "Audio")); |
| 107 | + |
| 108 | + if (!isAudioNode) { |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + const char* name = find_node_name(props); |
| 113 | + |
| 114 | + // exclude mixxx own node |
| 115 | + if (strcmp(name, "mixxx")) { |
| 116 | + m_objects.insert_or_assign(id, Object{Node{}}); |
| 117 | + auto device = QSharedPointer<SoundDevicePipewire>::create( |
| 118 | + m_pConfig, m_pManager, this, id, name); |
| 119 | + emit deviceAdded(device); |
| 120 | + m_soundDevices.insert_or_assign(id, std::move(device)); |
| 121 | + } |
| 122 | + } else if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) { |
| 123 | + const uint32_t node_id = pw_properties_parse_int(spa_dict_lookup(props, PW_KEY_NODE_ID)); |
| 124 | + const uint32_t port_id = pw_properties_parse_int(spa_dict_lookup(props, PW_KEY_PORT_ID)); |
| 125 | + const char* dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); |
| 126 | + const spa_direction direction = |
| 127 | + strcmp(dir, "out") ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT; |
| 128 | + |
| 129 | + if (m_soundDevices.contains(node_id)) { |
| 130 | + m_objects.insert_or_assign(id, Object{Port(port_id, node_id, direction)}); |
| 131 | + m_soundDevices[node_id]->registerDevicePort(id, props); |
| 132 | + } else { |
| 133 | + for (auto& [device_id, device] : m_soundDevices) { |
| 134 | + if (device->isOpen()) { |
| 135 | + uint32_t filterId = pw_filter_get_node_id(device->getFilter()); |
| 136 | + if (node_id == filterId) { |
| 137 | + m_soundDevices[device_id]->registerFilterPort(id, props); |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + } |
| 142 | + } else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { |
| 143 | + const int out_port = pw_properties_parse_int( |
| 144 | + spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)); |
| 145 | + const int in_port = pw_properties_parse_int(spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)); |
| 146 | + |
| 147 | + if (m_objects.contains(in_port)) { |
| 148 | + m_objects.insert_or_assign(id, Object{Link(in_port)}); |
| 149 | + } else if (m_objects.contains(out_port)) { |
| 150 | + m_objects.insert_or_assign(id, Object{Link(out_port)}); |
| 151 | + } |
| 152 | + |
| 153 | + const int out_node = pw_properties_parse_int( |
| 154 | + spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_NODE)); |
| 155 | + |
| 156 | + if (m_soundDevices.contains(out_node)) { |
| 157 | + m_soundDevices[out_node]->registerLink(id, SPA_DIRECTION_INPUT, props); |
| 158 | + return; |
| 159 | + } |
| 160 | + |
| 161 | + const int in_node = pw_properties_parse_int(spa_dict_lookup(props, PW_KEY_LINK_INPUT_NODE)); |
| 162 | + |
| 163 | + if (m_soundDevices.contains(in_node)) { |
| 164 | + m_soundDevices[in_node]->registerLink(id, SPA_DIRECTION_OUTPUT, props); |
| 165 | + return; |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +void PipewireEnumerator::registryEventGlobalRemove(void* data, unsigned int id) { |
| 171 | + PipewireEnumerator* pEnumerator = static_cast<PipewireEnumerator*>(data); |
| 172 | + |
| 173 | + if (!pEnumerator->m_objects.contains(id)) { |
| 174 | + return; |
| 175 | + } |
| 176 | + |
| 177 | + Object& object = pEnumerator->m_objects.at(id); |
| 178 | + |
| 179 | + if (std::get_if<Node>(&object)) { |
| 180 | + auto& device = pEnumerator->m_soundDevices.extract(id).mapped(); |
| 181 | + device->close(); |
| 182 | + emit pEnumerator->deviceRemoved(device); |
| 183 | + } else if (auto* port = std::get_if<Port>(&object)) { |
| 184 | + pEnumerator->m_soundDevices[port->nodeId]->unregisterDevicePort(port->id, port->direction); |
| 185 | + } else if (auto* link = std::get_if<Link>(&object)) { |
| 186 | + if (pEnumerator->m_objects.contains(link->portId)) { |
| 187 | + const Port& port = std::get<Port>(pEnumerator->m_objects[link->portId]); |
| 188 | + pEnumerator->m_soundDevices[port.nodeId]->unregisterLink(id, port.id, port.direction); |
| 189 | + } |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +pw_core* PipewireEnumerator::getCore() { |
| 194 | + return m_pCore; |
| 195 | +} |
| 196 | + |
| 197 | +pw_thread_loop* PipewireEnumerator::getThreadLoop() { |
| 198 | + return m_pThreadLoop; |
| 199 | +} |
| 200 | + |
| 201 | +std::vector<SoundDevicePointer> PipewireEnumerator::queryDevices() const { |
| 202 | + std::vector<SoundDevicePointer> devices{}; |
| 203 | + |
| 204 | + for (const auto& [id, device] : m_soundDevices) { |
| 205 | + devices.push_back(device); |
| 206 | + } |
| 207 | + |
| 208 | + return devices; |
| 209 | +} |
| 210 | + |
| 211 | +std::vector<std::string> PipewireEnumerator::getAPIs() const { |
| 212 | + return std::vector{std::string("PipeWire")}; |
| 213 | +} |
| 214 | + |
| 215 | +void PipewireEnumerator::initialize() { |
| 216 | + connect(this, |
| 217 | + &PipewireEnumerator::deviceAdded, |
| 218 | + m_pManager, |
| 219 | + &SoundManager::addDevice, |
| 220 | + Qt::QueuedConnection); |
| 221 | + connect(this, |
| 222 | + &PipewireEnumerator::deviceRemoved, |
| 223 | + m_pManager, |
| 224 | + &SoundManager::removeDevice, |
| 225 | + Qt::QueuedConnection); |
| 226 | + connect(this, |
| 227 | + &PipewireEnumerator::portAdded, |
| 228 | + m_pManager, |
| 229 | + &SoundManager::updateDevice, |
| 230 | + Qt::QueuedConnection); |
| 231 | + connect(this, |
| 232 | + &PipewireEnumerator::portRemoved, |
| 233 | + m_pManager, |
| 234 | + &SoundManager::updateDevice, |
| 235 | + Qt::QueuedConnection); |
| 236 | + connect(this, |
| 237 | + &PipewireEnumerator::linkAdded, |
| 238 | + m_pManager, |
| 239 | + &SoundManager::updateDevice, |
| 240 | + Qt::QueuedConnection); |
| 241 | + connect(this, |
| 242 | + &PipewireEnumerator::linkRemoved, |
| 243 | + m_pManager, |
| 244 | + &SoundManager::updateDevice, |
| 245 | + Qt::QueuedConnection); |
| 246 | +} |
| 247 | + |
| 248 | +int PipewireEnumerator::metadataProperty(void* data, |
| 249 | + [[maybe_unused]] uint32_t id, |
| 250 | + const char* key, |
| 251 | + [[maybe_unused]] const char* type, |
| 252 | + const char* value) { |
| 253 | + PipewireEnumerator* pEnumerator = static_cast<PipewireEnumerator*>(data); |
| 254 | + |
| 255 | + if (strcmp(key, "clock.allowed-rates") == 0) { |
| 256 | + // parse json arrays like [ 44100, 48000, 96000 ] |
| 257 | + QString s = value; |
| 258 | + s.remove('['); |
| 259 | + s.remove(']'); |
| 260 | + |
| 261 | + QStringList parts = s.split(',', Qt::SkipEmptyParts); |
| 262 | + |
| 263 | + for (const QString& part : parts) { |
| 264 | + pEnumerator->m_samplerates.push_back(mixxx::audio::SampleRate(part.trimmed().toInt())); |
| 265 | + } |
| 266 | + pEnumerator->m_pManager->checkConfig(); |
| 267 | + } |
| 268 | + return 0; |
| 269 | +} |
0 commit comments