Skip to content

Commit 4c0cce0

Browse files
committed
implement PipewireEnumerator and SoundDevicePipewire
1 parent e05113f commit 4c0cce0

6 files changed

Lines changed: 1903 additions & 16 deletions

File tree

src/soundio/pipewireenumerator.cpp

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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, &registry_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+
}

src/soundio/pipewireenumerator.h

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#pragma once
2+
3+
#include <pipewire/extensions/metadata.h>
4+
#include <pipewire/pipewire.h>
5+
#include <spa/utils/defs.h>
6+
7+
#include <QObject>
8+
9+
#include "preferences/usersettings.h"
10+
#include "soundio/sounddevice.h"
11+
#include "soundio/sounddeviceenumerator.h"
12+
#include "soundio/sounddevicepipewire.h"
13+
#include "soundio/soundmanager.h"
14+
15+
class PipewireEnumerator : public SoundDeviceEnumerator {
16+
Q_OBJECT
17+
public:
18+
PipewireEnumerator(UserSettingsPointer config,
19+
SoundManager* sm);
20+
~PipewireEnumerator() override;
21+
22+
QList<mixxx::audio::SampleRate> getSampleRates() const override;
23+
std::vector<SoundDevicePointer> queryDevices() const override;
24+
std::vector<std::string> getAPIs() const override;
25+
26+
void initialize();
27+
28+
pw_core* getCore();
29+
pw_thread_loop* getThreadLoop();
30+
31+
signals:
32+
void deviceAdded(SoundDevicePointer device);
33+
void deviceRemoved(SoundDevicePointer device);
34+
void portAdded(SoundDevicePointer device);
35+
void portRemoved(SoundDevicePointer device);
36+
void linkAdded(SoundDevicePointer device);
37+
void linkRemoved(SoundDevicePointer device);
38+
39+
private:
40+
/// Source of all events, like a soundcard is detected/removed, link is created/destroyed
41+
void registryEventGlobal(uint32_t id,
42+
uint32_t permissions,
43+
const char* type,
44+
uint32_t version,
45+
const struct spa_dict* props);
46+
static void registryEventGlobalOuter(void* data,
47+
uint32_t id,
48+
uint32_t permissions,
49+
const char* type,
50+
uint32_t version,
51+
const struct spa_dict* props);
52+
53+
static void registryEventGlobalRemove(void* data, unsigned int id);
54+
55+
static constexpr struct pw_registry_events registry_events = {
56+
.version = PW_VERSION_REGISTRY_EVENTS,
57+
.global = registryEventGlobalOuter,
58+
.global_remove = registryEventGlobalRemove,
59+
};
60+
61+
struct Filter {
62+
struct Port {
63+
uint32_t id;
64+
};
65+
66+
pw_filter* filter;
67+
std::unordered_map<std::shared_ptr<SoundDevicePipewire>, std::vector<Port>> devices;
68+
};
69+
70+
struct Device {
71+
pw_device* device;
72+
pw_node* in_node;
73+
pw_node* out_node;
74+
Filter filter;
75+
};
76+
77+
std::unordered_map<uint32_t, Device> m_devices;
78+
79+
static int metadataProperty(void* data,
80+
uint32_t id,
81+
const char* key,
82+
const char* type,
83+
const char* value);
84+
85+
static constexpr struct pw_metadata_events metadataEvents = {
86+
.version = PW_VERSION_METADATA_EVENTS,
87+
.property = metadataProperty};
88+
89+
void createSoundDevice(uint32_t id, const char* name);
90+
void destroySoundDevice(uint32_t id);
91+
92+
std::unordered_map<uint32_t, QSharedPointer<SoundDevicePipewire>> m_soundDevices;
93+
94+
struct Link {
95+
uint32_t portId;
96+
};
97+
98+
struct Port {
99+
// this is not global id, but port.id, which starts from 0
100+
// using this we can directly index into the port vector
101+
// on the node from nodeId
102+
uint32_t id;
103+
uint32_t nodeId;
104+
spa_direction direction;
105+
};
106+
107+
struct Node {};
108+
using Object = std::variant<Node, Port, Link>;
109+
110+
std::unordered_map<uint32_t, Object> m_objects;
111+
QList<mixxx::audio::SampleRate> m_samplerates;
112+
113+
SoundManager* m_pManager;
114+
UserSettingsPointer m_pConfig;
115+
116+
pw_core* m_pCore;
117+
pw_registry* m_pRegistry;
118+
pw_context* m_pContext;
119+
pw_metadata* m_pMetadata;
120+
pw_thread_loop* m_pThreadLoop;
121+
spa_hook m_registryListener;
122+
spa_hook m_metadataListener;
123+
124+
bool m_initialized;
125+
};

0 commit comments

Comments
 (0)