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