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
qpulseaudiosource.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/qaudiosystem_platform_stream_support_p.h>
7#include <QtMultimedia/private/qpulseaudio_contextmanager_p.h>
8#include <QtMultimedia/private/qpulsehelpers_p.h>
9#include <QtCore/qsemaphore.h>
10
11#include <mutex> // for std::lock_guard
12#include <unistd.h>
13
14QT_BEGIN_NAMESPACE
15
16namespace QPulseAudioInternal {
17
18using namespace QtMultimediaPrivate;
19
20QPulseAudioSourceStream::QPulseAudioSourceStream(QAudioDevice device, const QAudioFormat &format,
21 std::optional<qsizetype> ringbufferSize,
22 QPulseAudioSource *parent,
23 float volume,
24 std::optional<NativePeriodFrames> nativePeriodFrames)
27 },
28 m_parent(parent)
29{
30 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
31 pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(format);
32 pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(format);
33
34 if (!pa_sample_spec_valid(&spec))
35 return;
36
37 const QByteArray streamName =
38 QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8();
39
40 if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) {
41 qCDebug(qLcPulseAudioIn) << "Format: " << spec.format;
42 qCDebug(qLcPulseAudioIn) << "Rate: " << spec.rate;
43 qCDebug(qLcPulseAudioIn) << "Channels: " << spec.channels;
44 qCDebug(qLcPulseAudioIn) << "Frame size: " << pa_frame_size(&spec);
45 }
46
47 std::lock_guard engineLock{ *pulseEngine };
48
49 m_stream = PAStreamHandle{
50 pa_stream_new(pulseEngine->context(), streamName.constData(), &spec, &channel_map),
51 PAStreamHandle::HasRef,
52 };
53}
54
55QPulseAudioSourceStream::~QPulseAudioSourceStream() = default;
56
57bool QPulseAudioSourceStream::start(QIODevice *device)
58{
59 setQIODevice(device);
60
61 createQIODeviceConnections(device);
62
63 return startStream(StreamType::Ringbuffer);
64}
65
66bool QPulseAudioSourceStream::start(AudioCallback &&audioCallback)
67{
68 m_audioCallback = std::move(audioCallback);
69 return startStream(StreamType::Callback);
70}
71
73{
74 QIODevice *device = createRingbufferReaderDevice();
75 bool started = start(device);
76 if (!started)
77 return nullptr;
78
79 return device;
80}
81
82void QPulseAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
83{
84 requestStop();
85
86 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
87 std::lock_guard engineLock{ *pulseEngine };
88
89 uninstallCallbacks();
90 disconnectQIODeviceConnections();
91
92 if (shutdownPolicy == ShutdownPolicy::DrainRingbuffer) {
93 size_t bytesToRead = pa_stream_readable_size(m_stream.get());
94 if (bytesToRead != size_t(-1))
95 readCallbackRingbuffer(bytesToRead);
96 }
97
98 // Note: we need to cork the stream before disconnecting to prevent pulseaudio from deadlocking
99 auto op = streamCork(m_stream, true);
100 pulseEngine->waitForAsyncOperation(op);
101
102 pa_stream_disconnect(m_stream.get());
103
104 finalizeQIODevice(shutdownPolicy);
105 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
106 emptyRingbuffer();
107}
108
110{
111 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
112 std::lock_guard engineLock{ *pulseEngine };
113
114 std::ignore = streamCork(m_stream, true);
115}
116
118{
119 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
120 std::lock_guard engineLock{ *pulseEngine };
121
122 std::ignore = streamCork(m_stream, false);
123}
124
126{
127 return bool(m_stream);
128}
129
131{
132 m_parent->updateStreamIdle(idle);
133}
134
135bool QPulseAudioSourceStream::startStream(StreamType streamType)
136{
137 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
138 static const bool serverIsPipewire = [&] {
139 return pulseEngine->serverName().contains(u"PulseAudio (on PipeWire");
140 }();
141
142 pa_buffer_attr attr{
143 .maxlength = uint32_t(m_format.bytesForFrames(
144 m_nativePeriodFrames ? qToUnderlying(*m_nativePeriodFrames) : 1024)),
145 .tlength = uint32_t(-1),
146 .prebuf = uint32_t(-1),
147 .minreq = uint32_t(-1),
148
149 // pulseaudio's vanilla implementation requires us to set a fragment size, otherwise we only
150 // get a single callback every 2-ish seconds.
151 .fragsize = serverIsPipewire
152 ? uint32_t(-1)
153 : uint32_t(m_format.bytesForFrames(
154 m_nativePeriodFrames ? qToUnderlying(*m_nativePeriodFrames)
155 : 1024)),
156 };
157
158 constexpr pa_stream_flags flags =
159 pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
160
161 std::unique_lock engineLock{ *pulseEngine };
162 installCallbacks(streamType);
163
164 struct state_t
165 {
166 pa_stream_state state{};
167 QSemaphore sem;
168 };
169
170 state_t state;
171
172 pa_stream_set_state_callback(m_stream.get(), [](pa_stream *stream, void *data) {
173 auto *state = reinterpret_cast<state_t *>(data);
174 switch (pa_stream_get_state(stream)) {
175 case PA_STREAM_READY:
176 case PA_STREAM_FAILED:
177 case PA_STREAM_TERMINATED:
178 state->state = pa_stream_get_state(stream);
179 state->sem.release();
180 break;
181 default:
182 break;
183 }
184 }, &state);
185
186 const auto id = m_audioDevice.id();
187 int status = pa_stream_connect_record(m_stream.get(), id.data(), &attr, flags);
188 if (status != 0) {
189 qCWarning(qLcPulseAudioOut) << "pa_stream_connect_record() failed!";
190 m_stream = {};
191 return false;
192 }
193
194 engineLock.unlock();
195 state.sem.acquire();
196 if (state.state != PA_STREAM_READY) {
197 qCWarning(qLcPulseAudioOut) << "Stream failed to connect:" << state.state;
198 m_stream = {};
199 return false;
200 }
201
202 engineLock.lock();
203 pa_stream_set_state_callback(m_stream.get(), nullptr, nullptr);
204
205 return true;
206}
207
208void QPulseAudioSourceStream::installCallbacks(StreamType streamType)
209{
210 switch (streamType) {
211 case StreamType::Ringbuffer: {
212 pa_stream_set_read_callback(m_stream.get(),
213 [](pa_stream *stream, size_t nbytes, void *data) {
214 auto *self = reinterpret_cast<QPulseAudioSourceStream *>(data);
215 Q_ASSERT(stream == self->m_stream.get());
216 self->readCallbackRingbuffer(nbytes);
217 }, this);
218 break;
219 }
220 case StreamType::Callback: {
221 pa_stream_set_read_callback(m_stream.get(),
222 [](pa_stream *stream, size_t nbytes, void *data) {
223 auto *self = reinterpret_cast<QPulseAudioSourceStream *>(data);
224 Q_ASSERT(stream == self->m_stream.get());
225 self->readCallbackAudioCallback(nbytes);
226 }, this);
227 break;
228 }
229 }
230}
231
232void QPulseAudioSourceStream::uninstallCallbacks()
233{
234 pa_stream_set_read_callback(m_stream.get(), nullptr, nullptr);
235}
236
237void QPulseAudioSourceStream::readCallbackRingbuffer([[maybe_unused]] size_t bytesToRead)
238{
239 const void *data{};
240 size_t nBytes{};
241 int status = pa_stream_peek(m_stream.get(), &data, &nBytes);
242 if (status < 0) {
243 invokeOnAppThread([this] {
244 handleIOError(m_parent);
245 });
246 return;
247 }
248
249 QSpan<const std::byte> hostBuffer{
250 reinterpret_cast<const std::byte *>(data),
251 qsizetype(nBytes),
252 };
253
254 uint32_t numberOfFrames = m_format.framesForBytes(nBytes);
255
256 [[maybe_unused]] uint64_t framesWritten =
257 QPlatformAudioSourceStream::process(hostBuffer, numberOfFrames);
258 status = pa_stream_drop(m_stream.get());
259 if (status < 0) {
260 if (!isStopRequested()) {
261 invokeOnAppThread([this] {
262 handleIOError(m_parent);
263 });
264 }
265 }
266}
267
268void QPulseAudioSourceStream::readCallbackAudioCallback([[maybe_unused]] size_t bytesToRead)
269{
270 const void *data{};
271 size_t nBytes{};
272 int status = pa_stream_peek(m_stream.get(), &data, &nBytes);
273 if (status < 0) {
274 QMetaObject::invokeMethod(m_parent, [this] {
275 handleIOError(m_parent);
276 });
277 return;
278 }
279
280 QSpan<const std::byte> hostBuffer{
281 reinterpret_cast<const std::byte *>(data),
282 qsizetype(nBytes),
283 };
284
285 runAudioCallback(*m_audioCallback, hostBuffer, m_format, volume());
286
287 status = pa_stream_drop(m_stream.get());
288 if (status < 0) {
289 if (!isStopRequested()) {
290 QMetaObject::invokeMethod(m_parent, [this] {
291 handleIOError(m_parent);
292 });
293 }
294 }
295}
296
297////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
298
299QPulseAudioSource::QPulseAudioSource(QAudioDevice device, const QAudioFormat &format,
300 QObject *parent)
302{
303}
304
305QPulseAudioSource::~QPulseAudioSource()
306 = default;
307
308bool QPulseAudioSource::validatePulseaudio()
309{
310 QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
311 if (!pulseEngine->contextIsGood()) {
312 qWarning() << "Invalid PulseAudio context:" << pulseEngine->getContextState();
313 setError(QtAudio::Error::FatalError);
314 return false;
315 }
316 return true;
317}
318
319void QPulseAudioSource::start(QIODevice *device)
320{
321 if (!validatePulseaudio())
322 return;
323 return BaseClass::start(device);
324}
325
326void QPulseAudioSource::start(AudioCallback &&cb)
327{
328 if (!validatePulseaudio())
329 return;
330 return BaseClass::start(std::move(cb));
331}
332
333QIODevice *QPulseAudioSource::start()
334{
335 if (!validatePulseaudio())
336 return nullptr;
337 return BaseClass::start();
338}
339
340} // namespace QPulseAudioInternal
341
342QT_END_NAMESPACE
QPulseAudioSource(QAudioDevice, const QAudioFormat &, QObject *parent)
void start(QIODevice *device) override
void start(AudioCallback &&) override
QPulseAudioSourceStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QPulseAudioSource *parent, float volume, std::optional< NativePeriodFrames > nativePeriodFrames)