Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qpipewire_audiosink.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
9
10#include <QtCore/qcoreapplication.h>
11#include <QtCore/qdebug.h>
12#include <QtCore/qloggingcategory.h>
13
14#include <pipewire/pipewire.h>
15#include <pipewire/stream.h>
16#include <spa/pod/builder.h>
17
18#include <thread>
19
20QT_BEGIN_NAMESPACE
21
22namespace QtPipeWire {
23
24Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink");
25static constexpr bool pipewireRealtimeTracing = false;
26
27using namespace Qt::Literals;
28
29////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
30
32 const QAudioFormat &format,
33 std::optional<qsizetype> ringbufferSize,
34 QPipewireAudioSink *parent,
35 float volume,
36 std::optional<int32_t> hardwareBufferFrames,
37 AudioEndpointRole role
38 ):
40 format,
41 },
43 std::move(device),
44 format,
47 volume,
48 },
49 m_role {
50 role,
51 },
52 m_parent{
53 parent,
54 }
55{
56 m_xrunNotification = m_xrunOccurred.callOnActivated(&m_xrunOccurred, [this, parent] {
57 if (isStopRequested())
58 return;
59 parent->reportXRuns(m_xrunCount.exchange(0));
60 });
61}
62
63QPipewireAudioSinkStream::~QPipewireAudioSinkStream()
64{
65 resetStream();
66 Q_ASSERT(!m_deviceRemovalObserver);
67}
68
70{
71 return true;
72}
73
74bool QPipewireAudioSinkStream::start(QIODevice *device)
75{
76 createStream(StreamType::Ringbuffer);
77
78 Q_ASSERT(hasStream());
79 auto sinkNodeSerial = findSinkNodeSerial();
80 if (!sinkNodeSerial) {
81 requestStop();
82 return false;
83 }
84
85 setQIODevice(device);
86 pullFromQIODevice();
87
88 createQIODeviceConnections(device);
89
90 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
91 if (!connected) {
92 requestStop();
93 return false;
94 }
95
96 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
97 m_self = shared_from_this();
99
100 return true;
101}
102
104{
105 QIODevice *device = createRingbufferWriterDevice();
106
107 setIdleState(true);
108 bool started = start(device);
109 if (!started)
110 return nullptr;
111
112 return device;
113}
114
115bool QPipewireAudioSinkStream::start(AudioCallback audioCallback)
116{
117 createStream(StreamType::Callback);
118
119 Q_ASSERT(hasStream());
120 auto sinkNodeSerial = findSinkNodeSerial();
121 if (!sinkNodeSerial) {
122 requestStop();
123 return false;
124 }
125
126 m_audioCallback = std::move(audioCallback);
127
128 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
129 if (!connected) {
130 requestStop();
131 return false;
132 }
133
134 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
135 m_self = shared_from_this();
137 return true;
138}
139
140void QPipewireAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
141{
142 m_shutdownPolicy.store(shutdownPolicy, std::memory_order_relaxed);
143 if (shutdownPolicy == ShutdownPolicy::DrainRingbuffer) {
144 // disconnect when ringbuffer is drained
145 m_ringbufferDrained.callOnActivated([this] {
146 disconnectStream();
147 });
148 }
149
150 requestStop();
151 m_parent = nullptr;
152
153 disconnectQIODeviceConnections();
154
155 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer || m_audioCallback) {
156 // disconnect immediately
157 disconnectStream();
158 }
159
160 unregisterDeviceObserver();
161
162 if (m_audioCallback)
163 // ensure that no callback is sent after we stop the stream
164 m_disconnectSemaphore.acquire();
165}
166
168{
169 m_parent->updateStreamIdle(idle);
170}
171
172void QPipewireAudioSinkStream::createStream(StreamType streamType)
173{
174 const char *roleString = [&] {
175 switch (m_role) {
176 case AudioEndpointRole::MediaPlayback:
177 case AudioEndpointRole::Other:
178 return "Music";
179 case AudioEndpointRole::Accessibility:
180 return "Accessibility";
181 case AudioEndpointRole::SoundEffect:
182 return "Notification";
183 default:
184 Q_UNREACHABLE_RETURN("Music");
185 }
186 }();
187
188 auto extraProperties = std::array{
189 spa_dict_item{ PW_KEY_MEDIA_CATEGORY, "Playback" },
190 spa_dict_item{ PW_KEY_MEDIA_ROLE, roleString },
191 };
192
193 QString applicationName = qApp->applicationName();
194 if (applicationName.isNull())
195 applicationName = u"QPipewireAudioSink"_s;
196
197 QPipewireAudioStream::createStream(extraProperties, m_hardwareBufferFrames,
198 applicationName.toUtf8().constData(), streamType);
199}
200
201std::optional<ObjectSerial> QPipewireAudioSinkStream::findSinkNodeSerial()
202{
203 const QPipewireAudioDevicePrivate *device =
204 QAudioDevicePrivate::handle<QPipewireAudioDevicePrivate>(m_audioDevice);
205
206 QByteArray nodeName = device->nodeName();
207 auto ret = QAudioContextManager::deviceMonitor().findSinkNodeSerial(std::string_view{
208 nodeName.data(),
209 size_t(nodeName.size()),
210 });
211
212 if (!ret)
213 qWarning() << "Cannot find device: " << nodeName;
214 return ret;
215}
216
218{
219 if (!isStopRequested())
220 // note: as long as the stream is not stopped, m_parent is valid
221 handleIOError(m_parent);
222}
223
224static auto resolveHostBuffer(pw_buffer *b, const QAudioFormat &format)
225{
226 struct spa_buffer *buf = b->buffer;
227 uint64_t strideBytes = format.bytesPerSample() * format.channelCount();
228 Q_ASSERT(strideBytes > 0);
229 uint64_t totalNumberOfFrames = buf->datas[0].maxsize / strideBytes;
230
231#if PW_CHECK_VERSION(0, 3, 49)
232 if (pw_check_library_version(0, 3, 49))
233 // LATER: drop support for 0.3.49
234 if (b->requested)
235 totalNumberOfFrames = std::min(b->requested, totalNumberOfFrames);
236#endif
237
238 const uint64_t requestedSamples = totalNumberOfFrames * format.channelCount();
239
240 QSpan<std::byte> writeBuffer{
241 reinterpret_cast<std::byte *>(buf->datas[0].data),
242 qsizetype(requestedSamples * format.bytesPerSample()),
243 };
244
245 struct HostBufferData
246 {
247 QSpan<std::byte> writeBuffer;
248 const uint64_t requestedSamples{};
249 const uint64_t totalNumberOfFrames{};
250 };
251
252 return HostBufferData{
253 writeBuffer,
254 requestedSamples,
255 totalNumberOfFrames,
256 };
257}
258
260{
262 if (!b) {
263 qCritical() << "pw_stream_dequeue_buffer failed";
264 return;
265 }
266
268
272
274 // discarding ringbuffer: we silence the last block and exit early
277
278 if constexpr (pipewireRealtimeTracing)
280 << "QPipewireAudioSinkStream: shutdown with DiscardRingbuffer";
281 return;
282 }
283
285
288
289 if (stopRequested) {
291 if constexpr (pipewireRealtimeTracing)
293 << "QPipewireAudioSinkStream: shutdown after draining ringbuffer";
295 }
296 }
297
300}
301
303{
304 using namespace QtMultimediaPrivate;
306 if (!b) {
307 qCritical() << "pw_stream_dequeue_buffer failed";
308 return;
309 }
310
312
314
316}
317
319 const char *)
320{
321 qCDebug(lcPipewireAudioSink) << "QPipewireAudioSinkStream::stateChanged" << oldState << state;
322
323 switch (state) {
327 m_self.reset();
328 // CAVEAT: m_self may have been the last owner causing the object to be destroyed now.
329 break;
330
331 default:
332 break;
333 }
334 }
335}
336
341
343{
344 auto self = shared_from_this(); // extend lifetime until this function returns;
345
347
349}
350
352{
353 struct spa_buffer *buf = b->buffer;
354 buf->datas[0].chunk->offset = 0;
357
359}
360
362{
363 constexpr bool forceXRun = true;
364 if constexpr (forceXRun) {
365 // force xrun
366 static int i = 0;
367 if (++i == 10)
369 }
370}
371
372////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
373// QPipewireAudioSink
374
380
382 = default;
383
384void QPipewireAudioSink::reportXRuns(int numberOfXruns)
385{
386 qDebug() << "XRuns occurred:" << numberOfXruns;
387}
388
389} // namespace QtPipeWire
390
391QT_END_NAMESPACE
void registerStreamReference(std::shared_ptr< QPipewireAudioStream >)
Q_STATIC_LOGGING_CATEGORY(lcPipewireRegistry, "qt.multimedia.pipewire.registry")
static constexpr bool pipewireRealtimeTracing
static auto resolveHostBuffer(pw_buffer *b, const QAudioFormat &format)
StrongIdType< uint64_t, ObjectSerialTag > ObjectSerial
QPipewireAudioSinkStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QPipewireAudioSink *parent, float volume, std::optional< int32_t > hardwareBufferFrames, AudioEndpointRole)