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
qpulseaudiosink.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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 <QtCore/qdebug.h>
7#include <QtMultimedia/private/qaudiohelpers_p.h>
8#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
9#include <QtMultimedia/private/qpulseaudio_contextmanager_p.h>
10#include <QtMultimedia/private/qpulsehelpers_p.h>
11
12#include <mutex> // for std::lock_guard
13#include <unistd.h>
14
15QT_BEGIN_NAMESPACE
16
18
19QPulseAudioSinkStream::QPulseAudioSinkStream(QAudioDevice device, const QAudioFormat &format,
20 std::optional<qsizetype> ringbufferSize, QPulseAudioSink *parent,
21 float volume,
22 std::optional<int32_t> hardwareBufferSize,
23 AudioEndpointRole role)
26 },
27 m_parent{
28 parent,
29 }
30{
31 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
32
33 pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(format);
34 pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(format);
35
36 if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) {
37 qCDebug(qLcPulseAudioOut) << "Opening stream with.";
38 qCDebug(qLcPulseAudioOut) << "\tFormat: " << spec.format;
39 qCDebug(qLcPulseAudioOut) << "\tRate: " << spec.rate;
40 qCDebug(qLcPulseAudioOut) << "\tChannels: " << spec.channels;
41 qCDebug(qLcPulseAudioOut) << "\tFrame size: " << pa_frame_size(&spec);
42 }
43
44 const QByteArray streamName =
45 QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8();
46
47 PAProplistHandle propList{
48 pa_proplist_new(),
49 };
50 const char *roleString = [&]() -> const char * {
51 switch (role) {
52 case AudioEndpointRole::MediaPlayback:
53 return "music";
54 case AudioEndpointRole::SoundEffect:
55 return "event";
56 case AudioEndpointRole::Accessibility:
57 return "a11y";
58 case AudioEndpointRole::Other:
59 return nullptr;
60 default:
61 Q_UNREACHABLE_RETURN(nullptr);
62 }
63 }();
64
65 if (roleString)
66 pa_proplist_sets(propList.get(), PA_PROP_MEDIA_ROLE, roleString);
67
68 std::lock_guard engineLock{ *pulseEngine };
69
70 m_stream = PAStreamHandle{
71 pa_stream_new_with_proplist(pulseEngine->context(), streamName.constData(), &spec,
72 &channel_map, propList.get()),
73 PAStreamHandle::HasRef,
74 };
75
76 if (!m_stream) {
77 qWarning() << "Failed to create PulseAudio stream";
78 return;
79 }
80}
81
82QPulseAudioSinkStream::~QPulseAudioSinkStream()
83{
84 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
85 std::lock_guard engineLock{ *pulseEngine };
86 m_stream = {};
87}
88
89bool QPulseAudioSinkStream::start(QIODevice *device)
90{
91 setQIODevice(device);
92 pullFromQIODevice();
93
94 createQIODeviceConnections(device);
95
96 bool streamStarted = startStream(StreamType::Ringbuffer);
97 return streamStarted;
98}
99
100bool QPulseAudioSinkStream::start(AudioCallback &&callback)
101{
102 m_audioCallback = std::move(callback);
103
104 bool streamStarted = startStream(StreamType::Callback);
105 return streamStarted;
106}
107
108QIODevice *QPulseAudioSinkStream::start()
109{
110 QIODevice *device = createRingbufferWriterDevice();
111
112 setIdleState(true);
113 bool started = start(device);
114 if (!started)
115 return nullptr;
116
117 return device;
118}
119
120void QPulseAudioSinkStream::stop(ShutdownPolicy policy)
121{
122 requestStop();
123
124 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
125 std::lock_guard engineLock{ *pulseEngine };
126
127 uninstallCallbacks();
128 // Note: we need to cork to ensure that the stream is stopped immediately
129 std::ignore = streamCork(m_stream, true);
130
131 if (m_audioCallback) {
132 switch (policy) {
133 case ShutdownPolicy::DrainRingbuffer:
134 case ShutdownPolicy::DiscardRingbuffer:
135 break;
136 default:
137 Q_UNREACHABLE_RETURN();
138 }
139 } else {
140 switch (policy) {
141 case ShutdownPolicy::DrainRingbuffer: {
142 bool writeFailed = false;
143
144 visitRingbuffer([&](auto &ringbuffer) {
145 ringbuffer.consumeAll([&](auto region) {
146 if (writeFailed)
147 return;
148
149 QSpan<const std::byte> writeRegion = as_bytes(region);
150 int status =
151 pa_stream_write(m_stream.get(), writeRegion.data(), writeRegion.size(),
152 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
153 if (status != 0)
154 writeFailed = true;
155 });
156 });
157
158 break;
159 }
160 case ShutdownPolicy::DiscardRingbuffer: {
161 break;
162 }
163 default:
164 Q_UNREACHABLE_RETURN();
165 }
166 }
167 pa_stream_disconnect(m_stream.get());
168}
169
170void QPulseAudioSinkStream::suspend()
171{
172 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
173 std::lock_guard engineLock{ *pulseEngine };
174
175 std::ignore = streamCork(m_stream, true);
176}
177
178void QPulseAudioSinkStream::resume()
179{
180 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
181 std::lock_guard engineLock{ *pulseEngine };
182
183 std::ignore = streamCork(m_stream, false);
184}
185
186bool QPulseAudioSinkStream::open() const
187{
188 return m_stream.isValid();
189}
190
191void QPulseAudioSinkStream::installCallbacks(StreamType streamType)
192{
193 pa_stream_set_overflow_callback(m_stream.get(), [](pa_stream *stream, void *data) {
194 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
195 Q_ASSERT(stream == self->m_stream.get());
196 self->underflowCallback();
197 }, this);
198
199 pa_stream_set_underflow_callback(m_stream.get(), [](pa_stream *stream, void *data) {
200 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
201 Q_ASSERT(stream == self->m_stream.get());
202 self->overflowCallback();
203 }, this);
204
205 pa_stream_set_state_callback(m_stream.get(), [](pa_stream *stream, void *data) {
206 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
207 Q_ASSERT(stream == self->m_stream.get());
208 self->stateCallback();
209 }, this);
210
211 switch (streamType) {
212 case StreamType::Ringbuffer:
213 pa_stream_set_write_callback(m_stream.get(),
214 [](pa_stream *stream, size_t nbytes, void *data) {
215 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
216 Q_ASSERT(stream == self->m_stream.get());
217 self->writeCallbackRingbuffer(nbytes);
218 }, this);
219 break;
220 case StreamType::Callback:
221 pa_stream_set_write_callback(m_stream.get(),
222 [](pa_stream *stream, size_t nbytes, void *data) {
223 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
224 Q_ASSERT(stream == self->m_stream.get());
225 self->writeCallbackAudioCallback(nbytes);
226 }, this);
227 break;
228
229 default:
230 Q_UNREACHABLE_RETURN();
231 }
232
233 pa_stream_set_latency_update_callback(m_stream.get(), [](pa_stream *stream, void *data) {
234 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
235 Q_ASSERT(stream == self->m_stream.get());
236 self->latencyUpdateCallback();
237 }, this);
238}
239
240void QPulseAudioSinkStream::uninstallCallbacks()
241{
242 pa_stream_set_overflow_callback(m_stream.get(), nullptr, nullptr);
243 pa_stream_set_underflow_callback(m_stream.get(), nullptr, nullptr);
244 pa_stream_set_state_callback(m_stream.get(), nullptr, nullptr);
245 pa_stream_set_write_callback(m_stream.get(), nullptr, nullptr);
246 pa_stream_set_latency_update_callback(m_stream.get(), nullptr, nullptr);
247}
248
249bool QPulseAudioSinkStream::startStream(StreamType streamType)
250{
251 pa_buffer_attr attr{
252 .maxlength = uint32_t(m_format.bytesForFrames(m_hardwareBufferFrames.value_or(1024))),
253 .tlength = uint32_t(-1),
254 .prebuf = uint32_t(-1),
255 .minreq = uint32_t(-1),
256 .fragsize = uint32_t(-1),
257 };
258
259 installCallbacks(streamType);
260
261 constexpr pa_stream_flags flags =
262 pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
263
264 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
265 std::lock_guard engineLock{ *pulseEngine };
266
267 int status = pa_stream_connect_playback(m_stream.get(), m_audioDevice.id().data(), &attr, flags,
268 nullptr, nullptr);
269
270 if (status != 0) {
271 qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!";
272 m_stream = {};
273 return false;
274 }
275 return true;
276}
277
278void QPulseAudioSinkStream::updateStreamIdle(bool idle)
279{
280 m_parent->updateStreamIdle(idle);
281}
282
283void QPulseAudioSinkStream::writeCallbackRingbuffer(size_t requestedBytes)
284{
285 // ensure round down to number of requested frames
286 uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
287 size_t nbytes = m_format.bytesForFrames(requestedFrames);
288
289 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
290 Q_ASSERT(pulseEngine->isInMainLoop());
291
292 void *dest = nullptr;
293
294 int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
295 if (status != 0) {
296 qCWarning(qLcPulseAudioOut)
297 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
298
299 QMetaObject::invokeMethod(m_parent, [this] {
300 handleIOError(m_parent);
301 });
302 }
303 QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
304
305 const uint64_t consumedFrames = process(hostBuffer, requestedFrames);
306 if (consumedFrames != requestedFrames) {
307 auto remainder = drop(hostBuffer, m_format.bytesForFrames(consumedFrames));
308 std::fill(remainder.begin(), remainder.end(), std::byte{});
309 }
310 status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
311 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
312 if (status != 0) {
313 qCWarning(qLcPulseAudioOut)
314 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
315
316 QMetaObject::invokeMethod(m_parent, [this] {
317 handleIOError(m_parent);
318 });
319 }
320}
321
322void QPulseAudioSinkStream::writeCallbackAudioCallback(size_t requestedBytes)
323{
324 // ensure round down to number of requested frames
325 uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
326 size_t nbytes = m_format.bytesForFrames(requestedFrames);
327
328 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
329 Q_ASSERT(pulseEngine->isInMainLoop());
330
331 void *dest = nullptr;
332
333 int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
334 if (status != 0) {
335 qCWarning(qLcPulseAudioOut)
336 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
337
338 invokeOnAppThread([this] {
339 handleIOError(m_parent);
340 });
341 }
342 QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
343 runAudioCallback(*m_audioCallback, hostBuffer, m_format, volume());
344
345 status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
346 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
347 if (status != 0) {
348 qCWarning(qLcPulseAudioOut)
349 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
350
351 invokeOnAppThread([this] {
352 handleIOError(m_parent);
353 });
354 }
355}
356
357QPulseAudioSink::QPulseAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
359{
360}
361
362QPulseAudioSink::~QPulseAudioSink()
363 = default;
364
365bool QPulseAudioSink::validatePulseaudio()
366{
367 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
368 if (!pulseEngine->contextIsGood()) {
369 qWarning() << "Invalid PulseAudio context:" << pulseEngine->getContextState();
370 setError(QtAudio::Error::FatalError);
371 return false;
372 }
373 return true;
374}
375
376void QPulseAudioSink::start(QIODevice *device)
377{
378 if (!validatePulseaudio())
379 return;
380 return BaseClass::start(device);
381}
382
383void QPulseAudioSink::start(AudioCallback &&callback)
384{
385 if (!validatePulseaudio())
386 return;
387 return BaseClass::start(std::forward<AudioCallback>(callback));
388}
389
390QIODevice *QPulseAudioSink::start()
391{
392 if (!validatePulseaudio())
393 return nullptr;
394 return BaseClass::start();
395}
396
397} // namespace QPulseAudioInternal
398
399QT_END_NAMESPACE
QPulseAudioSink(QAudioDevice, const QAudioFormat &, QObject *parent)
void start(QIODevice *device) override
void start(AudioCallback &&) override
QPulseAudioSinkStream(QAudioDevice, const QAudioFormat &format, std::optional< qsizetype > ringbufferSize, QPulseAudioSink *parent, float volume, std::optional< int32_t > hardwareBufferSize, AudioEndpointRole)