Skip to content

Commit c60871a

Browse files
committed
service/pipewire: set device node volumes with device object
Fixes discrepancies between pulse and qs volumes, and volumes not persisting across reboot or vt switches.
1 parent b40d414 commit c60871a

File tree

9 files changed

+380
-74
lines changed

9 files changed

+380
-74
lines changed

src/services/pipewire/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ qt_add_library(quickshell-service-pipewire STATIC
99
node.cpp
1010
metadata.cpp
1111
link.cpp
12+
device.cpp
1213
)
1314

1415
qt_add_qml_module(quickshell-service-pipewire

src/services/pipewire/core.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <qloggingcategory.h>
1010
#include <qobject.h>
1111
#include <qsocketnotifier.h>
12+
#include <qtmetamacros.h>
1213
#include <spa/utils/defs.h>
1314
#include <spa/utils/hook.h>
1415

@@ -68,11 +69,12 @@ bool PwCore::isValid() const {
6869
return this->core != nullptr;
6970
}
7071

71-
void PwCore::poll() const {
72+
void PwCore::poll() {
7273
qCDebug(logLoop) << "Pipewire event loop received new events, iterating.";
7374
// Spin pw event loop.
7475
pw_loop_iterate(this->loop, 0);
7576
qCDebug(logLoop) << "Done iterating pipewire event loop.";
77+
emit this->polled();
7678
}
7779

7880
SpaHook::SpaHook() { // NOLINT

src/services/pipewire/core.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ class PwCore: public QObject {
2828
pw_context* context = nullptr;
2929
pw_core* core = nullptr;
3030

31+
signals:
32+
void polled();
33+
3134
private slots:
32-
void poll() const;
35+
void poll();
3336

3437
private:
3538
QSocketNotifier notifier;

src/services/pipewire/device.cpp

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#include "device.hpp"
2+
#include <array>
3+
#include <cstdint>
4+
#include <functional>
5+
6+
#include <pipewire/device.h>
7+
#include <qcontainerfwd.h>
8+
#include <qlogging.h>
9+
#include <qloggingcategory.h>
10+
#include <qobject.h>
11+
#include <qtypes.h>
12+
#include <spa/param/param.h>
13+
#include <spa/param/props.h>
14+
#include <spa/param/route.h>
15+
#include <spa/pod/builder.h>
16+
#include <spa/pod/parser.h>
17+
#include <spa/pod/pod.h>
18+
#include <spa/pod/vararg.h>
19+
#include <spa/utils/type.h>
20+
21+
#include "core.hpp"
22+
23+
namespace qs::service::pipewire {
24+
25+
Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg);
26+
27+
// https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397
28+
// https://github.com/PipeWire/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743
29+
30+
void PwDevice::bindHooks() {
31+
pw_device_add_listener(this->proxy(), &this->listener.hook, &PwDevice::EVENTS, this);
32+
QObject::connect(this->registry->core, &PwCore::polled, this, &PwDevice::polled);
33+
}
34+
35+
void PwDevice::unbindHooks() {
36+
QObject::disconnect(this->registry->core, &PwCore::polled, this, &PwDevice::polled);
37+
this->listener.remove();
38+
this->stagingIndexes.clear();
39+
this->routeDeviceIndexes.clear();
40+
}
41+
42+
const pw_device_events PwDevice::EVENTS = {
43+
.version = PW_VERSION_DEVICE_EVENTS,
44+
.info = &PwDevice::onInfo,
45+
.param = &PwDevice::onParam,
46+
};
47+
48+
void PwDevice::onInfo(void* data, const pw_device_info* info) {
49+
auto* self = static_cast<PwDevice*>(data);
50+
51+
if ((info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) == PW_DEVICE_CHANGE_MASK_PARAMS) {
52+
for (quint32 i = 0; i != info->n_params; i++) {
53+
auto& param = info->params[i]; // NOLINT
54+
55+
if (param.id == SPA_PARAM_Route) {
56+
if ((param.flags & SPA_PARAM_INFO_READWRITE) == SPA_PARAM_INFO_READWRITE) {
57+
qCDebug(logDevice) << "Enumerating routes param for" << self;
58+
self->stagingIndexes.clear();
59+
pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
60+
} else {
61+
qCWarning(logDevice) << "Unable to enumerate route param for" << self
62+
<< "as the param does not have read+write permissions.";
63+
}
64+
65+
break;
66+
}
67+
}
68+
}
69+
}
70+
71+
void PwDevice::onParam(
72+
void* data,
73+
qint32 /*seq*/,
74+
quint32 id,
75+
quint32 /*index*/,
76+
quint32 next,
77+
const spa_pod* param
78+
) {
79+
auto* self = static_cast<PwDevice*>(data);
80+
81+
if (id == SPA_PARAM_Route) {
82+
self->addDeviceIndexPairs(param);
83+
}
84+
}
85+
86+
void PwDevice::addDeviceIndexPairs(const spa_pod* param) {
87+
auto parser = spa_pod_parser();
88+
spa_pod_parser_pod(&parser, param);
89+
90+
qint32 device = 0;
91+
qint32 index = 0;
92+
93+
// clang-format off
94+
quint32 id = SPA_PARAM_Route;
95+
spa_pod_parser_get_object(
96+
&parser, SPA_TYPE_OBJECT_ParamRoute, &id,
97+
SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
98+
SPA_PARAM_ROUTE_index, SPA_POD_Int(&index)
99+
);
100+
// clang-format on
101+
102+
this->stagingIndexes.insert(device, index);
103+
// Insert into the main map as well, staging's purpose is to remove old entries.
104+
this->routeDeviceIndexes.insert(device, index);
105+
106+
qCDebug(logDevice).nospace() << "Registered device/index pair for " << this
107+
<< ": [device: " << device << ", index: " << index << ']';
108+
}
109+
110+
void PwDevice::polled() {
111+
// It is far more likely that the list content has not come in yet than it having no entries,
112+
// and there isn't a way to check in the case that there *aren't* actually any entries.
113+
if (!this->stagingIndexes.isEmpty() && this->stagingIndexes != this->routeDeviceIndexes) {
114+
this->routeDeviceIndexes = this->stagingIndexes;
115+
qCDebug(logDevice) << "Updated device/index pair list for" << this << "to"
116+
<< this->routeDeviceIndexes;
117+
}
118+
}
119+
120+
bool PwDevice::setVolumes(qint32 routeDevice, const QVector<float>& volumes) {
121+
return this->setRouteProps(routeDevice, [this, routeDevice, &volumes](spa_pod_builder* builder) {
122+
auto cubedVolumes = QVector<float>();
123+
for (auto volume: volumes) {
124+
cubedVolumes.push_back(volume * volume * volume);
125+
}
126+
127+
// clang-format off
128+
auto* props = spa_pod_builder_add_object(
129+
builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
130+
SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data())
131+
);
132+
// clang-format on
133+
134+
qCInfo(logDevice) << "Changed volumes of" << this << "on route device" << routeDevice << "to"
135+
<< volumes;
136+
return props;
137+
});
138+
}
139+
140+
bool PwDevice::setMuted(qint32 routeDevice, bool muted) {
141+
return this->setRouteProps(routeDevice, [this, routeDevice, muted](spa_pod_builder* builder) {
142+
// clang-format off
143+
auto* props = spa_pod_builder_add_object(
144+
builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
145+
SPA_PROP_mute, SPA_POD_Bool(muted)
146+
);
147+
// clang-format on
148+
149+
qCInfo(logDevice) << "Changed muted state of" << this << "on route device" << routeDevice
150+
<< "to" << muted;
151+
return props;
152+
});
153+
}
154+
155+
bool PwDevice::setRouteProps(
156+
qint32 routeDevice,
157+
const std::function<void*(spa_pod_builder*)>& propsCallback
158+
) {
159+
if (this->proxy() == nullptr) {
160+
qCCritical(logDevice) << "Tried to change device route props for" << this
161+
<< "which is not bound.";
162+
return false;
163+
}
164+
165+
if (!this->routeDeviceIndexes.contains(routeDevice)) {
166+
qCCritical(logDevice) << "Tried to change device route props for" << this
167+
<< "with untracked route device" << routeDevice;
168+
return false;
169+
}
170+
171+
auto routeIndex = this->routeDeviceIndexes.value(routeDevice);
172+
173+
auto buffer = std::array<quint8, 1024>();
174+
auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
175+
176+
auto* props = propsCallback(&builder);
177+
178+
// clang-format off
179+
auto* route = spa_pod_builder_add_object(
180+
&builder, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route,
181+
SPA_PARAM_ROUTE_device, SPA_POD_Int(routeDevice),
182+
SPA_PARAM_ROUTE_index, SPA_POD_Int(routeIndex),
183+
SPA_PARAM_ROUTE_props, SPA_POD_PodObject(props),
184+
SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)
185+
);
186+
// clang-format on
187+
188+
pw_device_set_param(this->proxy(), SPA_PARAM_Route, 0, static_cast<spa_pod*>(route));
189+
return true;
190+
}
191+
192+
} // namespace qs::service::pipewire

src/services/pipewire/device.hpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
3+
#include <functional>
4+
5+
#include <pipewire/core.h>
6+
#include <pipewire/device.h>
7+
#include <pipewire/type.h>
8+
#include <qcontainerfwd.h>
9+
#include <qhash.h>
10+
#include <qtmetamacros.h>
11+
#include <spa/pod/builder.h>
12+
13+
#include "core.hpp"
14+
#include "registry.hpp"
15+
16+
namespace qs::service::pipewire {
17+
18+
class PwDevice;
19+
20+
constexpr const char TYPE_INTERFACE_Device[] = PW_TYPE_INTERFACE_Device; // NOLINT
21+
class PwDevice: public PwBindable<pw_device, TYPE_INTERFACE_Device, PW_VERSION_DEVICE> {
22+
Q_OBJECT;
23+
24+
public:
25+
void bindHooks() override;
26+
void unbindHooks() override;
27+
28+
bool setVolumes(qint32 routeDevice, const QVector<float>& volumes);
29+
bool setMuted(qint32 routeDevice, bool muted);
30+
31+
private slots:
32+
void polled();
33+
34+
private:
35+
static const pw_device_events EVENTS;
36+
static void onInfo(void* data, const pw_device_info* info);
37+
static void
38+
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
39+
40+
QHash<qint32, qint32> routeDeviceIndexes;
41+
QHash<qint32, qint32> stagingIndexes;
42+
void addDeviceIndexPairs(const spa_pod* param);
43+
44+
bool
45+
setRouteProps(qint32 routeDevice, const std::function<void*(spa_pod_builder*)>& propsCallback);
46+
47+
SpaHook listener;
48+
};
49+
50+
} // namespace qs::service::pipewire

0 commit comments

Comments
 (0)