|
| 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 |
0 commit comments