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() = default;
83
84bool QPulseAudioSinkStream::start(QIODevice *device)
85{
86 setQIODevice(device);
87 pullFromQIODevice();
88
89 createQIODeviceConnections(device);
90
91 bool streamStarted = startStream(StreamType::Ringbuffer);
92 return streamStarted;
93}
94
95bool QPulseAudioSinkStream::start(AudioCallback &&callback)
96{
97 m_audioCallback = std::move(callback);
98
99 bool streamStarted = startStream(StreamType::Callback);
100 return streamStarted;
101}
102
103QIODevice *QPulseAudioSinkStream::start()
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
115void QPulseAudioSinkStream::stop(ShutdownPolicy policy)
116{
117 requestStop();
118
119 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
120 std::lock_guard engineLock{ *pulseEngine };
121
122 uninstallCallbacks();
123 // Note: we need to cork to ensure that the stream is stopped immediately
124 std::ignore = streamCork(m_stream, true);
125
126 if (m_audioCallback) {
127 switch (policy) {
128 case ShutdownPolicy::DrainRingbuffer:
129 case ShutdownPolicy::DiscardRingbuffer:
130 break;
131 default:
132 Q_UNREACHABLE_RETURN();
133 }
134 } else {
135 switch (policy) {
136 case ShutdownPolicy::DrainRingbuffer: {
137 bool writeFailed = false;
138
139 visitRingbuffer([&](auto &ringbuffer) {
140 ringbuffer.consumeAll([&](auto region) {
141 if (writeFailed)
142 return;
143
144 QSpan<const std::byte> writeRegion = as_bytes(region);
145 int status =
146 pa_stream_write(m_stream.get(), writeRegion.data(), writeRegion.size(),
147 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
148 if (status != 0)
149 writeFailed = true;
150 });
151 });
152
153 break;
154 }
155 case ShutdownPolicy::DiscardRingbuffer: {
156 break;
157 }
158 default:
159 Q_UNREACHABLE_RETURN();
160 }
161 }
162 pa_stream_disconnect(m_stream.get());
163}
164
165void QPulseAudioSinkStream::suspend()
166{
167 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
168 std::lock_guard engineLock{ *pulseEngine };
169
170 std::ignore = streamCork(m_stream, true);
171}
172
173void QPulseAudioSinkStream::resume()
174{
175 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
176 std::lock_guard engineLock{ *pulseEngine };
177
178 std::ignore = streamCork(m_stream, false);
179}
180
181bool QPulseAudioSinkStream::open() const
182{
183 return m_stream.isValid();
184}
185
186void QPulseAudioSinkStream::installCallbacks(StreamType streamType)
187{
188 pa_stream_set_overflow_callback(m_stream.get(), [](pa_stream *stream, void *data) {
189 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
190 Q_ASSERT(stream == self->m_stream.get());
191 self->underflowCallback();
192 }, this);
193
194 pa_stream_set_underflow_callback(m_stream.get(), [](pa_stream *stream, void *data) {
195 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
196 Q_ASSERT(stream == self->m_stream.get());
197 self->overflowCallback();
198 }, this);
199
200 pa_stream_set_state_callback(m_stream.get(), [](pa_stream *stream, void *data) {
201 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
202 Q_ASSERT(stream == self->m_stream.get());
203 self->stateCallback();
204 }, this);
205
206 switch (streamType) {
207 case StreamType::Ringbuffer:
208 pa_stream_set_write_callback(m_stream.get(),
209 [](pa_stream *stream, size_t nbytes, void *data) {
210 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
211 Q_ASSERT(stream == self->m_stream.get());
212 self->writeCallbackRingbuffer(nbytes);
213 }, this);
214 break;
215 case StreamType::Callback:
216 pa_stream_set_write_callback(m_stream.get(),
217 [](pa_stream *stream, size_t nbytes, void *data) {
218 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
219 Q_ASSERT(stream == self->m_stream.get());
220 self->writeCallbackAudioCallback(nbytes);
221 }, this);
222 break;
223
224 default:
225 Q_UNREACHABLE_RETURN();
226 }
227
228 pa_stream_set_latency_update_callback(m_stream.get(), [](pa_stream *stream, void *data) {
229 auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
230 Q_ASSERT(stream == self->m_stream.get());
231 self->latencyUpdateCallback();
232 }, this);
233}
234
235void QPulseAudioSinkStream::uninstallCallbacks()
236{
237 pa_stream_set_overflow_callback(m_stream.get(), nullptr, nullptr);
238 pa_stream_set_underflow_callback(m_stream.get(), nullptr, nullptr);
239 pa_stream_set_state_callback(m_stream.get(), nullptr, nullptr);
240 pa_stream_set_write_callback(m_stream.get(), nullptr, nullptr);
241 pa_stream_set_latency_update_callback(m_stream.get(), nullptr, nullptr);
242}
243
244bool QPulseAudioSinkStream::startStream(StreamType streamType)
245{
246 pa_buffer_attr attr{
247 .maxlength = uint32_t(m_format.bytesForFrames(m_hardwareBufferFrames.value_or(1024))),
248 .tlength = uint32_t(-1),
249 .prebuf = uint32_t(-1),
250 .minreq = uint32_t(-1),
251 .fragsize = uint32_t(-1),
252 };
253
254 installCallbacks(streamType);
255
256 constexpr pa_stream_flags flags =
257 pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
258
259 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
260 std::lock_guard engineLock{ *pulseEngine };
261
262 int status = pa_stream_connect_playback(m_stream.get(), m_audioDevice.id().data(), &attr, flags,
263 nullptr, nullptr);
264
265 if (status != 0) {
266 qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!";
267 m_stream = {};
268 return false;
269 }
270 return true;
271}
272
273void QPulseAudioSinkStream::updateStreamIdle(bool idle)
274{
275 m_parent->updateStreamIdle(idle);
276}
277
278void QPulseAudioSinkStream::writeCallbackRingbuffer(size_t requestedBytes)
279{
280 // ensure round down to number of requested frames
281 uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
282 size_t nbytes = m_format.bytesForFrames(requestedFrames);
283
284 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
285 Q_ASSERT(pulseEngine->isInMainLoop());
286
287 void *dest = nullptr;
288
289 int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
290 if (status != 0) {
291 qCWarning(qLcPulseAudioOut)
292 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
293
294 QMetaObject::invokeMethod(m_parent, [this] {
295 handleIOError(m_parent);
296 });
297 }
298 QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
299
300 const uint64_t consumedFrames = process(hostBuffer, requestedFrames);
301 if (consumedFrames != requestedFrames) {
302 auto remainder = drop(hostBuffer, m_format.bytesForFrames(consumedFrames));
303 std::fill(remainder.begin(), remainder.end(), std::byte{});
304 }
305 status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
306 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
307 if (status != 0) {
308 qCWarning(qLcPulseAudioOut)
309 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
310
311 QMetaObject::invokeMethod(m_parent, [this] {
312 handleIOError(m_parent);
313 });
314 }
315}
316
317void QPulseAudioSinkStream::writeCallbackAudioCallback(size_t requestedBytes)
318{
319 // ensure round down to number of requested frames
320 uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
321 size_t nbytes = m_format.bytesForFrames(requestedFrames);
322
323 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
324 Q_ASSERT(pulseEngine->isInMainLoop());
325
326 void *dest = nullptr;
327
328 int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
329 if (status != 0) {
330 qCWarning(qLcPulseAudioOut)
331 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
332
333 invokeOnAppThread([this] {
334 handleIOError(m_parent);
335 });
336 }
337 QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
338 runAudioCallback(*m_audioCallback, hostBuffer, m_format, volume());
339
340 status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
341 /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
342 if (status != 0) {
343 qCWarning(qLcPulseAudioOut)
344 << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
345
346 invokeOnAppThread([this] {
347 handleIOError(m_parent);
348 });
349 }
350}
351
352QPulseAudioSink::QPulseAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
354{
355}
356
357bool QPulseAudioSink::validatePulseaudio()
358{
359 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
360 if (!pulseEngine->contextIsGood()) {
361 qWarning() << "Invalid PulseAudio context:" << pulseEngine->getContextState();
362 setError(QtAudio::Error::FatalError);
363 return false;
364 }
365 return true;
366}
367
368void QPulseAudioSink::start(QIODevice *device)
369{
370 if (!validatePulseaudio())
371 return;
372 return BaseClass::start(device);
373}
374
375void QPulseAudioSink::start(AudioCallback &&callback)
376{
377 if (!validatePulseaudio())
378 return;
379 return BaseClass::start(std::forward<AudioCallback>(callback));
380}
381
382QIODevice *QPulseAudioSink::start()
383{
384 if (!validatePulseaudio())
385 return nullptr;
386 return BaseClass::start();
387}
388
389} // namespace QPulseAudioInternal
390
391QT_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)