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 Q_ASSERT(!m_deviceRemovalObserver);
66}
67
69{
70 return true;
71}
72
73bool QPipewireAudioSinkStream::start(QIODevice *device)
74{
75 createStream(StreamType::Ringbuffer);
76
77 Q_ASSERT(hasStream());
78 auto sinkNodeSerial = findSinkNodeSerial();
79 if (!sinkNodeSerial) {
80 requestStop();
81 return false;
82 }
83
84 setQIODevice(device);
85 pullFromQIODevice();
86
87 createQIODeviceConnections(device);
88
89 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
90 if (!connected) {
91 requestStop();
92 return false;
93 }
94
95 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
96 m_self = shared_from_this();
97 QAudioContextManager::instance()->registerStreamReference(m_self);
98
99 return true;
100}
101
103{
104 QIODevice *device = createRingbufferWriterDevice();
105
106 setIdleState(true);
107 bool started = start(device);
108 if (!started)
109 return nullptr;
110
111 return device;
112}
113
114bool QPipewireAudioSinkStream::start(AudioCallback audioCallback)
115{
116 createStream(StreamType::Callback);
117
118 Q_ASSERT(hasStream());
119 auto sinkNodeSerial = findSinkNodeSerial();
120 if (!sinkNodeSerial) {
121 requestStop();
122 return false;
123 }
124
125 m_audioCallback = std::move(audioCallback);
126
127 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
128 if (!connected) {
129 requestStop();
130 return false;
131 }
132
133 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
134 m_self = shared_from_this();
135 QAudioContextManager::instance()->registerStreamReference(m_self);
136 return true;
137}
138
139void QPipewireAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
140{
141 m_shutdownPolicy.store(shutdownPolicy, std::memory_order_relaxed);
142 if (shutdownPolicy == ShutdownPolicy::DrainRingbuffer) {
143 // disconnect when ringbuffer is drained
144 m_ringbufferDrained.callOnActivated([this] {
145 disconnectStream();
146 });
147 }
148
149 requestStop();
150 m_parent = nullptr;
151
152 disconnectQIODeviceConnections();
153
154 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer || m_audioCallback) {
155 // disconnect immediately
156 disconnectStream();
157 }
158
159 unregisterDeviceObserver();
160
161 if (m_audioCallback)
162 // ensure that no callback is sent after we stop the stream
163 m_disconnectSemaphore.acquire();
164}
165
167{
168 m_parent->updateStreamIdle(idle);
169}
170
171void QPipewireAudioSinkStream::createStream(StreamType streamType)
172{
173 const char *roleString = [&] {
174 switch (m_role) {
175 case AudioEndpointRole::MediaPlayback:
176 case AudioEndpointRole::Other:
177 return "Music";
178 case AudioEndpointRole::Accessibility:
179 return "Accessibility";
180 case AudioEndpointRole::SoundEffect:
181 return "Notification";
182 default:
183 Q_UNREACHABLE_RETURN("Music");
184 }
185 }();
186
187 auto extraProperties = std::array{
188 spa_dict_item{ PW_KEY_MEDIA_CATEGORY, "Playback" },
189 spa_dict_item{ PW_KEY_MEDIA_ROLE, roleString },
190 };
191
192 QString applicationName = qApp->applicationName();
193 if (applicationName.isNull())
194 applicationName = u"QPipewireAudioSink"_s;
195
196 QPipewireAudioStream::createStream(extraProperties, m_hardwareBufferFrames,
197 applicationName.toUtf8().constData(), streamType);
198}
199
200std::optional<ObjectSerial> QPipewireAudioSinkStream::findSinkNodeSerial()
201{
202 const QPipewireAudioDevicePrivate *device =
203 QAudioDevicePrivate::handle<QPipewireAudioDevicePrivate>(m_audioDevice);
204
205 QByteArray nodeName = device->nodeName();
206 auto ret = QAudioContextManager::deviceMonitor().findSinkNodeSerial(std::string_view{
207 nodeName.data(),
208 size_t(nodeName.size()),
209 });
210
211 if (!ret)
212 qWarning() << "Cannot find device: " << nodeName;
213 return ret;
214}
215
217{
218 if (!isStopRequested())
219 // note: as long as the stream is not stopped, m_parent is valid
220 handleIOError(m_parent);
221}
222
223static auto resolveHostBuffer(pw_buffer *b, const QAudioFormat &format)
224{
225 struct spa_buffer *buf = b->buffer;
226 uint64_t strideBytes = format.bytesPerSample() * format.channelCount();
227 Q_ASSERT(strideBytes > 0);
228 uint64_t totalNumberOfFrames = buf->datas[0].maxsize / strideBytes;
229
230#if PW_CHECK_VERSION(0, 3, 49)
231 if (pw_check_library_version(0, 3, 49))
232 // LATER: drop support for 0.3.49
233 if (b->requested)
234 totalNumberOfFrames = std::min(b->requested, totalNumberOfFrames);
235#endif
236
237 const uint64_t requestedSamples = totalNumberOfFrames * format.channelCount();
238
239 QSpan<std::byte> writeBuffer{
240 reinterpret_cast<std::byte *>(buf->datas[0].data),
241 qsizetype(requestedSamples * format.bytesPerSample()),
242 };
243
244 struct HostBufferData
245 {
246 QSpan<std::byte> writeBuffer;
247 const uint64_t requestedSamples{};
248 const uint64_t totalNumberOfFrames{};
249 };
250
251 return HostBufferData{
252 writeBuffer,
253 requestedSamples,
254 totalNumberOfFrames,
255 };
256}
257
259{
261 if (!b) {
262 qCritical() << "pw_stream_dequeue_buffer failed";
263 return;
264 }
265
267
271
273 // discarding ringbuffer: we silence the last block and exit early
276
277 if constexpr (pipewireRealtimeTracing)
279 << "QPipewireAudioSinkStream: shutdown with DiscardRingbuffer";
280 return;
281 }
282
284
287
288 if (stopRequested) {
290 if constexpr (pipewireRealtimeTracing)
292 << "QPipewireAudioSinkStream: shutdown after draining ringbuffer";
294 }
295 }
296
299}
300
302{
303 using namespace QtMultimediaPrivate;
305 if (!b) {
306 qCritical() << "pw_stream_dequeue_buffer failed";
307 return;
308 }
309
311
313
315}
316
318 const char *)
319{
320 qCDebug(lcPipewireAudioSink) << "QPipewireAudioSinkStream::stateChanged" << oldState << state;
321
322 switch (state) {
326 m_self.reset();
327 // CAVEAT: m_self may have been the last owner causing the object to be destroyed now.
328 break;
329
330 default:
331 break;
332 }
333 }
334}
335
337{
338 auto self = shared_from_this(); // extend lifetime until this function returns;
339
341
343}
344
346{
347 struct spa_buffer *buf = b->buffer;
348 buf->datas[0].chunk->offset = 0;
351
353}
354
356{
357 constexpr bool forceXRun = true;
358 if constexpr (forceXRun) {
359 // force xrun
360 static int i = 0;
361 if (++i == 10)
363 }
364}
365
366////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
367// QPipewireAudioSink
368
374
376{
377 qDebug() << "XRuns occurred:" << numberOfXruns;
378}
379
380} // namespace QtPipeWire
381
382QT_END_NAMESPACE
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)