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