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
19QT_BEGIN_NAMESPACE
20
21namespace QtPipeWire {
22
23Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink");
24static constexpr bool pipewireRealtimeTracing = false;
25
26using namespace Qt::Literals;
27
28////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
29
31 const QAudioFormat &format,
32 std::optional<qsizetype> ringbufferSize,
33 QPipewireAudioSink *parent,
34 float volume,
35 std::optional<NativePeriodFrames> nativePeriodFrames,
36 AudioEndpointRole role
37 ):
39 format,
40 },
42 std::move(device),
43 format,
46 volume,
47 },
48 m_role {
49 role,
50 },
51 m_parent{
52 parent,
53 }
54{
55 m_xrunNotification = m_xrunOccurred.callOnActivated(&m_xrunOccurred, [this, parent] {
56 if (isStopRequested())
57 return;
58 parent->reportXRuns(m_xrunCount.exchange(0));
59 });
60}
61
62QPipewireAudioSinkStream::~QPipewireAudioSinkStream()
63{
64 Q_ASSERT(!m_ringbufferDrained.thread() || m_ringbufferDrained.thread()->isCurrentThread());
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();
98 QAudioContextManager::instance()->registerStreamReference(m_self);
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();
136 QAudioContextManager::instance()->registerStreamReference(m_self);
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 if (m_deviceRemovalObserver)
155 unregisterDeviceObserver();
156
157 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer || m_audioCallback) {
158 // disconnect immediately
159 disconnectStream();
160 }
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_nativePeriodFrames,
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) {
328 // drop refcount on app thread
329 });
330 break;
331
332 default:
333 break;
334 }
335 }
336}
337
342
344{
345 auto self = shared_from_this(); // extend lifetime until this function returns;
346
348
350}
351
353{
354 struct spa_buffer *buf = b->buffer;
355 buf->datas[0].chunk->offset = 0;
358
360}
361
363{
364 constexpr bool forceXRun = true;
365 if constexpr (forceXRun) {
366 // force xrun
367 static int i = 0;
368 if (++i == 10)
370 }
371}
372
373////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
374// QPipewireAudioSink
375
381
383 = default;
384
385void QPipewireAudioSink::reportXRuns(int numberOfXruns)
386{
387 qDebug() << "XRuns occurred:" << numberOfXruns;
388}
389
390} // namespace QtPipeWire
391
392QT_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)