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
6#include <QtMultimedia/private/qpipewire_audiocontextmanager_p.h>
7#include <QtMultimedia/private/qpipewire_audiodevice_p.h>
8#include <QtMultimedia/private/qpipewire_support_p.h>
9#include <QtCore/qcoreapplication.h>
10#include <QtCore/qdebug.h>
11#include <QtCore/qloggingcategory.h>
12
13#include <pipewire/pipewire.h>
14#include <pipewire/stream.h>
15#include <spa/pod/builder.h>
16
17#include <thread>
18#include <utility>
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<NativePeriodFrames> nativePeriodFrames,
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_ringbufferDrained.thread() || m_ringbufferDrained.thread()->isCurrentThread());
66 resetStream();
67 Q_ASSERT(!m_deviceRemovalObserver);
68}
69
71{
72 return true;
73}
74
75bool QPipewireAudioSinkStream::start(QIODevice *device)
76{
77 createStream(StreamType::Ringbuffer);
78
79 Q_ASSERT(hasStream());
80 auto sinkNodeSerial = findSinkNodeSerial();
81 if (!sinkNodeSerial) {
82 requestStop();
83 return false;
84 }
85
86 setQIODevice(device);
87 pullFromQIODevice();
88
89 createQIODeviceConnections(device);
90
91 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
92 if (!connected) {
93 requestStop();
94 return false;
95 }
96
97 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
98 m_self = shared_from_this();
99 QAudioContextManager::instance()->registerStreamReference(m_self);
100
101 return true;
102}
103
105{
106 QIODevice *device = createRingbufferWriterDevice();
107
108 setIdleState(true);
109 bool started = start(device);
110 if (!started)
111 return nullptr;
112
113 return device;
114}
115
116bool QPipewireAudioSinkStream::start(AudioCallback audioCallback)
117{
118 createStream(StreamType::Callback);
119
120 Q_ASSERT(hasStream());
121 auto sinkNodeSerial = findSinkNodeSerial();
122 if (!sinkNodeSerial) {
123 requestStop();
124 return false;
125 }
126
127 m_audioCallback = std::move(audioCallback);
128
129 bool connected = connectStream(*sinkNodeSerial, SPA_DIRECTION_OUTPUT);
130 if (!connected) {
131 requestStop();
132 return false;
133 }
134
135 // keep instance alive until PW_STREAM_STATE_UNCONNECTED
136 m_self = shared_from_this();
137 QAudioContextManager::instance()->registerStreamReference(m_self);
138 return true;
139}
140
141void QPipewireAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
142{
143 m_shutdownPolicy.store(shutdownPolicy, std::memory_order_relaxed);
144 if (shutdownPolicy == ShutdownPolicy::DrainRingbuffer) {
145 // disconnect when ringbuffer is drained
146 m_ringbufferDrained.callOnActivated([this] {
147 disconnectStream();
148 });
149 }
150
151 requestStop();
152 m_parent = nullptr;
153
154 disconnectQIODeviceConnections();
155 if (m_deviceRemovalObserver)
156 unregisterDeviceObserver();
157
158 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer || m_audioCallback) {
159 // disconnect immediately
160 disconnectStream();
161 }
162
163 if (m_audioCallback)
164 // ensure that no callback is sent after we stop the stream
165 m_disconnectSemaphore.acquire();
166}
167
169{
170 if (m_parent)
171 m_parent->updateStreamIdle(idle);
172}
173
174void QPipewireAudioSinkStream::createStream(StreamType streamType)
175{
176 const char *roleString = [&] {
177 switch (m_role) {
178 case AudioEndpointRole::MediaPlayback:
179 case AudioEndpointRole::Other:
180 return "Music";
181 case AudioEndpointRole::Accessibility:
182 return "Accessibility";
183 case AudioEndpointRole::SoundEffect:
184 return "Notification";
185 default:
186 Q_UNREACHABLE_RETURN("Music");
187 }
188 }();
189
190 auto extraProperties = std::array{
191 spa_dict_item{ PW_KEY_MEDIA_CATEGORY, "Playback" },
192 spa_dict_item{ PW_KEY_MEDIA_ROLE, roleString },
193 };
194
195 QString applicationName = qApp->applicationName();
196 if (applicationName.isNull())
197 applicationName = u"QPipewireAudioSink"_s;
198
199 QPipewireAudioStream::createStream(extraProperties, m_nativePeriodFrames,
200 applicationName.toUtf8().constData(), streamType);
201}
202
203std::optional<ObjectSerial> QPipewireAudioSinkStream::findSinkNodeSerial()
204{
205 const QPipewireAudioDevicePrivate *device =
206 QAudioDevicePrivate::handle<QPipewireAudioDevicePrivate>(m_audioDevice);
207
208 QByteArray nodeName = device->nodeName();
209 auto ret = QAudioContextManager::deviceMonitor().findSinkNodeSerial(std::string_view{
210 nodeName.data(),
211 size_t(nodeName.size()),
212 });
213
214 if (!ret)
215 qWarning() << "Cannot find device: " << nodeName;
216 return ret;
217}
218
220{
221 if (isStopRequested())
222 return;
223 requestStop();
224 disconnectQIODeviceConnections();
225 handleIOError(std::exchange(m_parent, nullptr));
226}
227
228static auto resolveHostBuffer(pw_buffer *b, const QAudioFormat &format)
229{
230 struct spa_buffer *buf = b->buffer;
231 uint64_t strideBytes = format.bytesPerSample() * format.channelCount();
232 Q_ASSERT(strideBytes > 0);
233 uint64_t totalNumberOfFrames = buf->datas[0].maxsize / strideBytes;
234
235#if PW_CHECK_VERSION(0, 3, 49)
236 if (pw_check_library_version(0, 3, 49))
237 // LATER: drop support for 0.3.49
238 if (b->requested)
239 totalNumberOfFrames = std::min(b->requested, totalNumberOfFrames);
240#endif
241
242 const uint64_t requestedSamples = totalNumberOfFrames * format.channelCount();
243
244 QSpan<std::byte> writeBuffer{
245 reinterpret_cast<std::byte *>(buf->datas[0].data),
246 qsizetype(requestedSamples * format.bytesPerSample()),
247 };
248
249 struct HostBufferData
250 {
251 QSpan<std::byte> writeBuffer;
252 const uint64_t requestedSamples{};
253 const uint64_t totalNumberOfFrames{};
254 };
255
256 return HostBufferData{
257 writeBuffer,
258 requestedSamples,
259 totalNumberOfFrames,
260 };
261}
262
264{
266 if (!b) {
267 qCritical() << "pw_stream_dequeue_buffer failed";
268 return;
269 }
270
272
276
278 // discarding ringbuffer: we silence the last block and exit early
281
282 if constexpr (pipewireRealtimeTracing)
284 << "QPipewireAudioSinkStream: shutdown with DiscardRingbuffer";
285 return;
286 }
287
289
292
293 if (stopRequested) {
295 if constexpr (pipewireRealtimeTracing)
297 << "QPipewireAudioSinkStream: shutdown after draining ringbuffer";
299 }
300 }
301
304}
305
307{
308 using namespace QtMultimediaPrivate;
310 if (!b) {
311 qCritical() << "pw_stream_dequeue_buffer failed";
312 return;
313 }
314
316
318
320}
321
323 const char *)
324{
325 qCDebug(lcPipewireAudioSink) << "QPipewireAudioSinkStream::stateChanged" << oldState << state;
326
327 switch (state) {
332 // drop refcount on app thread
333 });
334 break;
335
336 default:
337 break;
338 }
339 }
340}
341
346
348{
349 auto self = shared_from_this(); // extend lifetime until this function returns;
350
352
354}
355
357{
358 struct spa_buffer *buf = b->buffer;
359 buf->datas[0].chunk->offset = 0;
362
364}
365
367{
368 constexpr bool forceXRun = true;
369 if constexpr (forceXRun) {
370 // force xrun
371 static int i = 0;
372 if (++i == 10)
374 }
375}
376
377////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
378// QPipewireAudioSink
379
385
387 = default;
388
389void QPipewireAudioSink::reportXRuns(int numberOfXruns)
390{
391 qDebug() << "XRuns occurred:" << numberOfXruns;
392}
393
394} // namespace QtPipeWire
395
396QT_END_NAMESPACE
static constexpr bool pipewireRealtimeTracing
Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink")
static auto resolveHostBuffer(pw_buffer *b, const QAudioFormat &format)
QPipewireAudioSinkStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QPipewireAudioSink *parent, float volume, std::optional< NativePeriodFrames > nativePeriodFrames, AudioEndpointRole)