| 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 | |
| 4 | #include "qpulseaudiosink_p.h" |
| 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 | |
| 15 | QT_BEGIN_NAMESPACE |
| 16 | |
| 17 | namespace QPulseAudioInternal { |
| 18 | |
| 19 | QPulseAudioSinkStream::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) |
| 24 | : QPlatformAudioSinkStream{ |
| 25 | std::move(device), format, ringbufferSize, hardwareBufferSize, volume, |
| 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: &spec); |
| 42 | } |
| 43 | |
| 44 | const QByteArray streamName = |
| 45 | QStringLiteral("QtmPulseStream-%1-%2" ).arg(a: ::getpid()).arg(a: 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(p: propList.get(), PA_PROP_MEDIA_ROLE, value: roleString); |
| 67 | |
| 68 | std::lock_guard engineLock{ *pulseEngine }; |
| 69 | |
| 70 | m_stream = PAStreamHandle{ |
| 71 | pa_stream_new_with_proplist(c: pulseEngine->context(), name: streamName.constData(), ss: &spec, |
| 72 | map: &channel_map, p: propList.get()), |
| 73 | PAStreamHandle::HasRef, |
| 74 | }; |
| 75 | |
| 76 | if (!m_stream) { |
| 77 | qWarning() << "Failed to create PulseAudio stream" ; |
| 78 | return; |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | QPulseAudioSinkStream::~QPulseAudioSinkStream() = default; |
| 83 | |
| 84 | bool 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 | |
| 95 | bool QPulseAudioSinkStream::start(AudioCallback &&callback) |
| 96 | { |
| 97 | m_audioCallback = std::move(callback); |
| 98 | |
| 99 | bool streamStarted = startStream(StreamType::Callback); |
| 100 | return streamStarted; |
| 101 | } |
| 102 | |
| 103 | QIODevice *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 | |
| 115 | void 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 | pa_stream_cork(s: m_stream.get(), b: 1, cb: nullptr, userdata: nullptr); |
| 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(f: [&](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(p: m_stream.get(), data: writeRegion.data(), nbytes: 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(s: m_stream.get()); |
| 163 | } |
| 164 | |
| 165 | void QPulseAudioSinkStream::suspend() |
| 166 | { |
| 167 | QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); |
| 168 | std::lock_guard engineLock{ *pulseEngine }; |
| 169 | |
| 170 | pa_stream_cork(s: m_stream.get(), b: 1, cb: nullptr, userdata: nullptr); |
| 171 | } |
| 172 | |
| 173 | void QPulseAudioSinkStream::resume() |
| 174 | { |
| 175 | QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); |
| 176 | std::lock_guard engineLock{ *pulseEngine }; |
| 177 | |
| 178 | pa_stream_cork(s: m_stream.get(), b: 0, cb: nullptr, userdata: nullptr); |
| 179 | } |
| 180 | |
| 181 | bool QPulseAudioSinkStream::open() const |
| 182 | { |
| 183 | return m_stream.isValid(); |
| 184 | } |
| 185 | |
| 186 | void QPulseAudioSinkStream::installCallbacks(StreamType streamType) |
| 187 | { |
| 188 | pa_stream_set_overflow_callback(p: m_stream.get(), cb: [](pa_stream *stream, void *data) { |
| 189 | auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data); |
| 190 | Q_ASSERT(stream == self->m_stream.get()); |
| 191 | self->underflowCallback(); |
| 192 | }, userdata: this); |
| 193 | |
| 194 | pa_stream_set_underflow_callback(p: m_stream.get(), cb: [](pa_stream *stream, void *data) { |
| 195 | auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data); |
| 196 | Q_ASSERT(stream == self->m_stream.get()); |
| 197 | self->overflowCallback(); |
| 198 | }, userdata: this); |
| 199 | |
| 200 | pa_stream_set_state_callback(s: m_stream.get(), cb: [](pa_stream *stream, void *data) { |
| 201 | auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data); |
| 202 | Q_ASSERT(stream == self->m_stream.get()); |
| 203 | self->stateCallback(); |
| 204 | }, userdata: this); |
| 205 | |
| 206 | switch (streamType) { |
| 207 | case StreamType::Ringbuffer: |
| 208 | pa_stream_set_write_callback(p: m_stream.get(), |
| 209 | cb: [](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(requestedBytes: nbytes); |
| 213 | }, userdata: this); |
| 214 | break; |
| 215 | case StreamType::Callback: |
| 216 | pa_stream_set_write_callback(p: m_stream.get(), |
| 217 | cb: [](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(requestedBytes: nbytes); |
| 221 | }, userdata: this); |
| 222 | break; |
| 223 | |
| 224 | default: |
| 225 | Q_UNREACHABLE_RETURN(); |
| 226 | } |
| 227 | |
| 228 | pa_stream_set_latency_update_callback(p: m_stream.get(), cb: [](pa_stream *stream, void *data) { |
| 229 | auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data); |
| 230 | Q_ASSERT(stream == self->m_stream.get()); |
| 231 | self->latencyUpdateCallback(); |
| 232 | }, userdata: this); |
| 233 | } |
| 234 | |
| 235 | void QPulseAudioSinkStream::uninstallCallbacks() |
| 236 | { |
| 237 | pa_stream_set_overflow_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); |
| 238 | pa_stream_set_underflow_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); |
| 239 | pa_stream_set_state_callback(s: m_stream.get(), cb: nullptr, userdata: nullptr); |
| 240 | pa_stream_set_write_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); |
| 241 | pa_stream_set_latency_update_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); |
| 242 | } |
| 243 | |
| 244 | bool QPulseAudioSinkStream::startStream(StreamType streamType) |
| 245 | { |
| 246 | pa_buffer_attr attr{ |
| 247 | .maxlength = uint32_t(m_format.bytesForFrames(frameCount: m_hardwareBufferFrames.value_or(u: 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(s: m_stream.get(), dev: m_audioDevice.id().data(), attr: &attr, flags, |
| 263 | volume: nullptr, sync_stream: 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 | |
| 273 | void QPulseAudioSinkStream::updateStreamIdle(bool idle) |
| 274 | { |
| 275 | m_parent->updateStreamIdle(idle); |
| 276 | } |
| 277 | |
| 278 | void QPulseAudioSinkStream::writeCallbackRingbuffer(size_t requestedBytes) |
| 279 | { |
| 280 | // ensure round down to number of requested frames |
| 281 | uint32_t requestedFrames = m_format.framesForBytes(byteCount: requestedBytes); |
| 282 | size_t nbytes = m_format.bytesForFrames(frameCount: 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(p: m_stream.get(), data: &dest, nbytes: &nbytes); |
| 290 | if (status != 0) { |
| 291 | qCWarning(qLcPulseAudioOut) |
| 292 | << "pa_stream_begin_write error:" << currentError(pulseEngine->context()); |
| 293 | |
| 294 | QMetaObject::invokeMethod(object: m_parent, function: [this] { |
| 295 | handleIOError(parent: m_parent); |
| 296 | }); |
| 297 | } |
| 298 | QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) }; |
| 299 | |
| 300 | const uint64_t consumedFrames = process(hostBuffer, totalNumberOfFrames: requestedFrames); |
| 301 | if (consumedFrames != requestedFrames) { |
| 302 | auto remainder = drop(span: hostBuffer, n: m_format.bytesForFrames(frameCount: consumedFrames)); |
| 303 | std::fill(first: remainder.begin(), last: remainder.end(), value: std::byte{}); |
| 304 | } |
| 305 | status = pa_stream_write(p: m_stream.get(), data: 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(object: m_parent, function: [this] { |
| 312 | handleIOError(parent: m_parent); |
| 313 | }); |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | void QPulseAudioSinkStream::writeCallbackAudioCallback(size_t requestedBytes) |
| 318 | { |
| 319 | // ensure round down to number of requested frames |
| 320 | uint32_t requestedFrames = m_format.framesForBytes(byteCount: requestedBytes); |
| 321 | size_t nbytes = m_format.bytesForFrames(frameCount: 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(p: m_stream.get(), data: &dest, nbytes: &nbytes); |
| 329 | if (status != 0) { |
| 330 | qCWarning(qLcPulseAudioOut) |
| 331 | << "pa_stream_begin_write error:" << currentError(pulseEngine->context()); |
| 332 | |
| 333 | invokeOnAppThread(f: [this] { |
| 334 | handleIOError(parent: m_parent); |
| 335 | }); |
| 336 | } |
| 337 | QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) }; |
| 338 | runAudioCallback(audioCallback&: *m_audioCallback, hostBuffer, format: m_format, volume: volume()); |
| 339 | |
| 340 | status = pa_stream_write(p: m_stream.get(), data: 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(f: [this] { |
| 347 | handleIOError(parent: m_parent); |
| 348 | }); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | QPulseAudioSink::QPulseAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent) |
| 353 | : BaseClass(std::move(device), format, parent) |
| 354 | { |
| 355 | } |
| 356 | |
| 357 | bool 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 | |
| 368 | void QPulseAudioSink::start(QIODevice *device) |
| 369 | { |
| 370 | if (!validatePulseaudio()) |
| 371 | return; |
| 372 | return BaseClass::start(device); |
| 373 | } |
| 374 | |
| 375 | void QPulseAudioSink::start(AudioCallback &&callback) |
| 376 | { |
| 377 | if (!validatePulseaudio()) |
| 378 | return; |
| 379 | return BaseClass::start(audioCallback: std::forward<AudioCallback>(t&: callback)); |
| 380 | } |
| 381 | |
| 382 | QIODevice *QPulseAudioSink::start() |
| 383 | { |
| 384 | if (!validatePulseaudio()) |
| 385 | return nullptr; |
| 386 | return BaseClass::start(); |
| 387 | } |
| 388 | |
| 389 | } // namespace QPulseAudioInternal |
| 390 | |
| 391 | QT_END_NAMESPACE |
| 392 | |