| 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 <QtCore/qcoreapplication.h> | 
| 5 | #include <QtCore/qdebug.h> | 
| 6 | #include <QtCore/qmath.h> | 
| 7 | #include <private/qaudiohelpers_p.h> | 
| 8 |  | 
| 9 | #include "qpulseaudiosink_p.h" | 
| 10 | #include "qpulseaudio_contextmanager_p.h" | 
| 11 | #include "qpulsehelpers_p.h" | 
| 12 | #include <sys/types.h> | 
| 13 | #include <unistd.h> | 
| 14 | #include <mutex> // for std::lock_guard | 
| 15 |  | 
| 16 | QT_BEGIN_NAMESPACE | 
| 17 |  | 
| 18 | static constexpr uint SinkPeriodTimeMs = 20; | 
| 19 | static constexpr uint DefaultBufferLengthMs = 100; | 
| 20 |  | 
| 21 | static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) | 
| 22 | { | 
| 23 |     Q_UNUSED(stream); | 
| 24 |     Q_UNUSED(userdata); | 
| 25 |     qCDebug(qLcPulseAudioOut) << "Write callback:"  << length; | 
| 26 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 27 |     pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0); | 
| 28 | } | 
| 29 |  | 
| 30 | static void outputStreamStateCallback(pa_stream *stream, void *userdata) | 
| 31 | { | 
| 32 |     Q_UNUSED(userdata); | 
| 33 |     pa_stream_state_t state = pa_stream_get_state(p: stream); | 
| 34 |     qCDebug(qLcPulseAudioOut) << "Stream state callback:"  << state; | 
| 35 |     switch (state) { | 
| 36 |     case PA_STREAM_CREATING: | 
| 37 |     case PA_STREAM_READY: | 
| 38 |     case PA_STREAM_TERMINATED: | 
| 39 |         break; | 
| 40 |  | 
| 41 |     case PA_STREAM_FAILED: | 
| 42 |     default: | 
| 43 |         qWarning() << QStringLiteral("Stream error: %1" ) | 
| 44 |                               .arg(a: QString::fromUtf8(utf8: pa_strerror( | 
| 45 |                                       error: pa_context_errno(c: pa_stream_get_context(p: stream))))); | 
| 46 |         QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 47 |         pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0); | 
| 48 |         break; | 
| 49 |     } | 
| 50 | } | 
| 51 |  | 
| 52 | static void outputStreamUnderflowCallback(pa_stream *stream, void *userdata) | 
| 53 | { | 
| 54 |     Q_UNUSED(stream); | 
| 55 |     qCDebug(qLcPulseAudioOut) << "Buffer underflow" ; | 
| 56 |     if (userdata) | 
| 57 |         static_cast<QPulseAudioSink *>(userdata)->streamUnderflowCallback(); | 
| 58 | } | 
| 59 |  | 
| 60 | static void outputStreamOverflowCallback(pa_stream *stream, void *userdata) | 
| 61 | { | 
| 62 |     Q_UNUSED(stream); | 
| 63 |     Q_UNUSED(userdata); | 
| 64 |     qCDebug(qLcPulseAudioOut) << "Buffer overflow" ; | 
| 65 | } | 
| 66 |  | 
| 67 | static void outputStreamLatencyCallback(pa_stream *stream, void *userdata) | 
| 68 | { | 
| 69 |     Q_UNUSED(stream); | 
| 70 |     Q_UNUSED(userdata); | 
| 71 |  | 
| 72 |     if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { | 
| 73 |         const pa_timing_info *info = pa_stream_get_timing_info(s: stream); | 
| 74 |  | 
| 75 |         qCDebug(qLcPulseAudioOut) << "Latency callback:" ; | 
| 76 |         qCDebug(qLcPulseAudioOut) << "\tWrite index corrupt: "  << info->write_index_corrupt; | 
| 77 |         qCDebug(qLcPulseAudioOut) << "\tWrite index: "  << info->write_index; | 
| 78 |         qCDebug(qLcPulseAudioOut) << "\tRead index corrupt: "  << info->read_index_corrupt; | 
| 79 |         qCDebug(qLcPulseAudioOut) << "\tRead index: "  << info->read_index; | 
| 80 |         qCDebug(qLcPulseAudioOut) << "\tSink usec: "  << info->sink_usec; | 
| 81 |         qCDebug(qLcPulseAudioOut) << "\tConfigured sink usec: "  << info->configured_sink_usec; | 
| 82 |     } | 
| 83 | } | 
| 84 |  | 
| 85 | static void outputStreamSuccessCallback(pa_stream *stream, int success, void *userdata) | 
| 86 | { | 
| 87 |     Q_UNUSED(stream); | 
| 88 |     Q_UNUSED(userdata); | 
| 89 |  | 
| 90 |     qCDebug(qLcPulseAudioOut) << "Stream successful:"  << success; | 
| 91 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 92 |     pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0); | 
| 93 | } | 
| 94 |  | 
| 95 | static void outputStreamDrainComplete(pa_stream *stream, int success, void *userdata) | 
| 96 | { | 
| 97 |     Q_UNUSED(stream); | 
| 98 |  | 
| 99 |     qCDebug(qLcPulseAudioOut) << "Stream drained:"  << static_cast<bool>(success) << userdata; | 
| 100 |  | 
| 101 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 102 |     pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0); | 
| 103 |  | 
| 104 |     if (userdata && success) | 
| 105 |         static_cast<QPulseAudioSink *>(userdata)->streamDrainedCallback(); | 
| 106 | } | 
| 107 |  | 
| 108 | static void outputStreamFlushComplete(pa_stream *stream, int success, void *userdata) | 
| 109 | { | 
| 110 |     Q_UNUSED(stream); | 
| 111 |  | 
| 112 |     qCDebug(qLcPulseAudioOut) << "Stream flushed:"  << static_cast<bool>(success) << userdata; | 
| 113 | } | 
| 114 |  | 
| 115 | static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void *userdata) | 
| 116 | { | 
| 117 |     Q_UNUSED(stream); | 
| 118 |     Q_UNUSED(success); | 
| 119 |     Q_UNUSED(userdata); | 
| 120 |  | 
| 121 |     qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:"  << static_cast<bool>(success); | 
| 122 | } | 
| 123 |  | 
| 124 | QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent) | 
| 125 |     : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this) | 
| 126 | { | 
| 127 | } | 
| 128 |  | 
| 129 | QPulseAudioSink::~QPulseAudioSink() | 
| 130 | { | 
| 131 |     if (auto notifier = m_stateMachine.stop()) | 
| 132 |         close(); | 
| 133 | } | 
| 134 |  | 
| 135 | QAudio::Error QPulseAudioSink::error() const | 
| 136 | { | 
| 137 |     return m_stateMachine.error(); | 
| 138 | } | 
| 139 |  | 
| 140 | QAudio::State QPulseAudioSink::state() const | 
| 141 | { | 
| 142 |     return m_stateMachine.state(); | 
| 143 | } | 
| 144 |  | 
| 145 | void QPulseAudioSink::streamUnderflowCallback() | 
| 146 | { | 
| 147 |     bool atEnd = m_audioSource && m_audioSource->atEnd(); | 
| 148 |     if (atEnd && m_stateMachine.state() != QAudio::StoppedState) { | 
| 149 |         qCDebug(qLcPulseAudioOut) << "Draining stream at end of buffer" ; | 
| 150 |         exchangeDrainOperation(newOperation: pa_stream_drain(s: m_stream.get(), cb: outputStreamDrainComplete, userdata: this)); | 
| 151 |     } | 
| 152 |  | 
| 153 |     m_stateMachine.updateActiveOrIdle(activeOrIdle: QAudioStateMachine::RunningState::Idle, | 
| 154 |                                       error: (m_pullMode && atEnd) ? QAudio::NoError | 
| 155 |                                                             : QAudio::UnderrunError); | 
| 156 | } | 
| 157 |  | 
| 158 | void QPulseAudioSink::streamDrainedCallback() | 
| 159 | { | 
| 160 |     if (!exchangeDrainOperation(newOperation: nullptr)) | 
| 161 |         return; | 
| 162 | } | 
| 163 |  | 
| 164 | void QPulseAudioSink::start(QIODevice *device) | 
| 165 | { | 
| 166 |     reset(); | 
| 167 |  | 
| 168 |     m_pullMode = true; | 
| 169 |     m_audioSource = device; | 
| 170 |  | 
| 171 |     if (!open()) { | 
| 172 |         m_audioSource = nullptr; | 
| 173 |         return; | 
| 174 |     } | 
| 175 |  | 
| 176 |     // ensure we only process timing infos that are up to date | 
| 177 |     gettimeofday(tv: &lastTimingInfo, tz: nullptr); | 
| 178 |     lastProcessedUSecs = 0; | 
| 179 |  | 
| 180 |     connect(sender: m_audioSource, signal: &QIODevice::readyRead, context: this, slot: &QPulseAudioSink::startPulling); | 
| 181 |  | 
| 182 |     m_stateMachine.start(); | 
| 183 | } | 
| 184 |  | 
| 185 | void QPulseAudioSink::startPulling() | 
| 186 | { | 
| 187 |     Q_ASSERT(m_pullMode); | 
| 188 |     if (m_tickTimer.isActive()) | 
| 189 |         return; | 
| 190 |  | 
| 191 |     m_tickTimer.start(msec: m_pullingPeriodTime, obj: this); | 
| 192 | } | 
| 193 |  | 
| 194 | void QPulseAudioSink::stopTimer() | 
| 195 | { | 
| 196 |     if (m_tickTimer.isActive()) | 
| 197 |         m_tickTimer.stop(); | 
| 198 | } | 
| 199 |  | 
| 200 | QIODevice *QPulseAudioSink::start() | 
| 201 | { | 
| 202 |     reset(); | 
| 203 |  | 
| 204 |     m_pullMode = false; | 
| 205 |  | 
| 206 |     if (!open()) | 
| 207 |         return nullptr; | 
| 208 |  | 
| 209 |     m_audioSource = new PulseOutputPrivate(this); | 
| 210 |     m_audioSource->open(mode: QIODevice::WriteOnly | QIODevice::Unbuffered); | 
| 211 |  | 
| 212 |     // ensure we only process timing infos that are up to date | 
| 213 |     gettimeofday(tv: &lastTimingInfo, tz: nullptr); | 
| 214 |     lastProcessedUSecs = 0; | 
| 215 |  | 
| 216 |     m_stateMachine.start(activeOrIdle: QAudioStateMachine::RunningState::Idle); | 
| 217 |  | 
| 218 |     return m_audioSource; | 
| 219 | } | 
| 220 |  | 
| 221 | bool QPulseAudioSink::open() | 
| 222 | { | 
| 223 |     if (m_opened) | 
| 224 |         return true; | 
| 225 |  | 
| 226 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 227 |  | 
| 228 |     if (!pulseEngine->context() | 
| 229 |         || pa_context_get_state(c: pulseEngine->context()) != PA_CONTEXT_READY) { | 
| 230 |         m_stateMachine.stopOrUpdateError(error: QAudio::FatalError); | 
| 231 |         return false; | 
| 232 |     } | 
| 233 |  | 
| 234 |     pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(format: m_format); | 
| 235 |     pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(format: m_format); | 
| 236 |     Q_ASSERT(spec.channels == channel_map.channels); | 
| 237 |  | 
| 238 |     if (!pa_sample_spec_valid(spec: &spec)) { | 
| 239 |         m_stateMachine.stopOrUpdateError(error: QAudio::OpenError); | 
| 240 |         return false; | 
| 241 |     } | 
| 242 |  | 
| 243 |     m_spec = spec; | 
| 244 |     m_totalTimeValue = 0; | 
| 245 |  | 
| 246 |     if (m_streamName.isNull()) | 
| 247 |         m_streamName = | 
| 248 |                 QStringLiteral("QtmPulseStream-%1-%2" ).arg(a: ::getpid()).arg(a: quintptr(this)).toUtf8(); | 
| 249 |  | 
| 250 |     if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { | 
| 251 |         qCDebug(qLcPulseAudioOut) << "Opening stream with." ; | 
| 252 |         qCDebug(qLcPulseAudioOut) << "\tFormat: "  << spec.format; | 
| 253 |         qCDebug(qLcPulseAudioOut) << "\tRate: "  << spec.rate; | 
| 254 |         qCDebug(qLcPulseAudioOut) << "\tChannels: "  << spec.channels; | 
| 255 |         qCDebug(qLcPulseAudioOut) << "\tFrame size: "  << pa_frame_size(spec: &spec); | 
| 256 |     } | 
| 257 |  | 
| 258 |     std::unique_lock engineLock{ *pulseEngine }; | 
| 259 |  | 
| 260 |     pa_proplist *propList = pa_proplist_new(); | 
| 261 | #if 0 | 
| 262 |     qint64 bytesPerSecond = m_format.sampleRate() * m_format.bytesPerFrame(); | 
| 263 |     static const char *mediaRoleFromAudioRole[] = { | 
| 264 |         nullptr, // UnknownRole | 
| 265 |         "music" , // MusicRole | 
| 266 |         "video" , // VideoRole | 
| 267 |         "phone" , // VoiceCommunicationRole | 
| 268 |         "event" , // AlarmRole | 
| 269 |         "event" , // NotificationRole | 
| 270 |         "phone" , // RingtoneRole | 
| 271 |         "a11y" , // AccessibilityRole | 
| 272 |         nullptr, // SonificationRole | 
| 273 |         "game"  // GameRole | 
| 274 |     }; | 
| 275 |  | 
| 276 |     const char *r = mediaRoleFromAudioRole[m_role]; | 
| 277 |     if (r) | 
| 278 |         pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, r); | 
| 279 | #endif | 
| 280 |  | 
| 281 |     m_stream = PAStreamHandle{ | 
| 282 |         pa_stream_new_with_proplist(c: pulseEngine->context(), name: m_streamName.constData(), ss: &m_spec, | 
| 283 |                                     map: &channel_map, p: propList), | 
| 284 |         PAStreamHandle::HasRef, | 
| 285 |     }; | 
| 286 |     pa_proplist_free(p: propList); | 
| 287 |  | 
| 288 |     if (!m_stream) { | 
| 289 |         qCWarning(qLcPulseAudioOut) << "QAudioSink: pa_stream_new_with_proplist() failed!" ; | 
| 290 |         engineLock.unlock(); | 
| 291 |  | 
| 292 |         m_stateMachine.stopOrUpdateError(error: QAudio::OpenError); | 
| 293 |         return false; | 
| 294 |     } | 
| 295 |  | 
| 296 |     pa_stream_set_state_callback(s: m_stream.get(), cb: outputStreamStateCallback, userdata: this); | 
| 297 |     pa_stream_set_write_callback(p: m_stream.get(), cb: outputStreamWriteCallback, userdata: this); | 
| 298 |  | 
| 299 |     pa_stream_set_underflow_callback(p: m_stream.get(), cb: outputStreamUnderflowCallback, userdata: this); | 
| 300 |     pa_stream_set_overflow_callback(p: m_stream.get(), cb: outputStreamOverflowCallback, userdata: this); | 
| 301 |     pa_stream_set_latency_update_callback(p: m_stream.get(), cb: outputStreamLatencyCallback, userdata: this); | 
| 302 |  | 
| 303 |     pa_buffer_attr requestedBuffer; | 
| 304 |     // Request a target buffer size | 
| 305 |     auto targetBufferSize = m_userBufferSize ? *m_userBufferSize : defaultBufferSize(); | 
| 306 |     requestedBuffer.tlength = | 
| 307 |             targetBufferSize ? static_cast<uint32_t>(targetBufferSize) : static_cast<uint32_t>(-1); | 
| 308 |     // Rest should be determined by PulseAudio | 
| 309 |     requestedBuffer.fragsize = static_cast<uint32_t>(-1); | 
| 310 |     requestedBuffer.maxlength = static_cast<uint32_t>(-1); | 
| 311 |     requestedBuffer.minreq = static_cast<uint32_t>(-1); | 
| 312 |     requestedBuffer.prebuf = static_cast<uint32_t>(-1); | 
| 313 |  | 
| 314 |     pa_stream_flags flags = | 
| 315 |             pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY); | 
| 316 |     if (pa_stream_connect_playback(s: m_stream.get(), dev: m_device.data(), attr: &requestedBuffer, flags, | 
| 317 |                                    volume: nullptr, sync_stream: nullptr) | 
| 318 |         < 0) { | 
| 319 |         qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!" ; | 
| 320 |         m_stream = {}; | 
| 321 |         engineLock.unlock(); | 
| 322 |         m_stateMachine.stopOrUpdateError(error: QAudio::OpenError); | 
| 323 |         return false; | 
| 324 |     } | 
| 325 |  | 
| 326 |     while (pa_stream_get_state(p: m_stream.get()) != PA_STREAM_READY) | 
| 327 |         pa_threaded_mainloop_wait(m: pulseEngine->mainloop()); | 
| 328 |  | 
| 329 |     const pa_buffer_attr *buffer = pa_stream_get_buffer_attr(s: m_stream.get()); | 
| 330 |     m_bufferSize = buffer->tlength; | 
| 331 |  | 
| 332 |     if (m_pullMode) { | 
| 333 |         // Adjust period time to reduce chance of it being higher than amount of bytes requested by | 
| 334 |         // PulseAudio server | 
| 335 |         m_pullingPeriodTime = | 
| 336 |                 qMin(a: SinkPeriodTimeMs, b: pa_bytes_to_usec(length: m_bufferSize, spec: &m_spec) / 1000 / 2); | 
| 337 |         m_pullingPeriodSize = pa_usec_to_bytes(t: m_pullingPeriodTime * 1000, spec: &m_spec); | 
| 338 |     } | 
| 339 |  | 
| 340 |     m_audioBuffer.resize(new_size: buffer->maxlength); | 
| 341 |  | 
| 342 |     const qint64 streamSize = m_audioSource ? m_audioSource->size() : 0; | 
| 343 |     if (m_pullMode && streamSize > 0 && static_cast<qint64>(buffer->prebuf) > streamSize) { | 
| 344 |         pa_buffer_attr newBufferAttr; | 
| 345 |         newBufferAttr = *buffer; | 
| 346 |         newBufferAttr.prebuf = streamSize; | 
| 347 |         PAOperationHandle{ | 
| 348 |             pa_stream_set_buffer_attr(s: m_stream.get(), attr: &newBufferAttr, cb: streamAdjustPrebufferCallback, | 
| 349 |                                       userdata: nullptr), | 
| 350 |             PAOperationHandle::HasRef, | 
| 351 |         }; | 
| 352 |     } | 
| 353 |  | 
| 354 |     if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { | 
| 355 |         qCDebug(qLcPulseAudioOut) << "Buffering info:" ; | 
| 356 |         qCDebug(qLcPulseAudioOut) << "\tMax length: "  << buffer->maxlength; | 
| 357 |         qCDebug(qLcPulseAudioOut) << "\tTarget length: "  << buffer->tlength; | 
| 358 |         qCDebug(qLcPulseAudioOut) << "\tPre-buffering: "  << buffer->prebuf; | 
| 359 |         qCDebug(qLcPulseAudioOut) << "\tMinimum request: "  << buffer->minreq; | 
| 360 |         qCDebug(qLcPulseAudioOut) << "\tFragment size: "  << buffer->fragsize; | 
| 361 |     } | 
| 362 |  | 
| 363 |     engineLock.unlock(); | 
| 364 |  | 
| 365 |     connect(sender: pulseEngine, signal: &QPulseAudioContextManager::contextFailed, context: this, | 
| 366 |             slot: &QPulseAudioSink::onPulseContextFailed); | 
| 367 |  | 
| 368 |     m_opened = true; | 
| 369 |  | 
| 370 |     if (m_pullMode) | 
| 371 |         startPulling(); | 
| 372 |  | 
| 373 |     m_elapsedTimeOffset = 0; | 
| 374 |  | 
| 375 |     return true; | 
| 376 | } | 
| 377 |  | 
| 378 | void QPulseAudioSink::close() | 
| 379 | { | 
| 380 |     if (!m_opened) | 
| 381 |         return; | 
| 382 |  | 
| 383 |     stopTimer(); | 
| 384 |  | 
| 385 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 386 |  | 
| 387 |     if (m_stream) { | 
| 388 |         std::lock_guard lock(*pulseEngine); | 
| 389 |  | 
| 390 |         pa_stream_set_state_callback(s: m_stream.get(), cb: nullptr, userdata: nullptr); | 
| 391 |         pa_stream_set_write_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); | 
| 392 |         pa_stream_set_underflow_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); | 
| 393 |         pa_stream_set_overflow_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); | 
| 394 |         pa_stream_set_latency_update_callback(p: m_stream.get(), cb: nullptr, userdata: nullptr); | 
| 395 |  | 
| 396 |         if (auto prevOp = exchangeDrainOperation(newOperation: nullptr)) | 
| 397 |             // cancel draining operation to prevent calling draining callback after closing. | 
| 398 |             pa_operation_cancel(o: prevOp.get()); | 
| 399 |  | 
| 400 |         PAOperationHandle operation{ | 
| 401 |             pa_stream_flush(s: m_stream.get(), cb: outputStreamFlushComplete, userdata: nullptr), | 
| 402 |             PAOperationHandle::HasRef, | 
| 403 |         }; | 
| 404 |  | 
| 405 |         pa_stream_disconnect(s: m_stream.get()); | 
| 406 |         m_stream = {}; | 
| 407 |     } | 
| 408 |  | 
| 409 |     disconnect(sender: pulseEngine, signal: &QPulseAudioContextManager::contextFailed, receiver: this, | 
| 410 |                slot: &QPulseAudioSink::onPulseContextFailed); | 
| 411 |  | 
| 412 |     if (m_audioSource) { | 
| 413 |         if (m_pullMode) { | 
| 414 |             disconnect(sender: m_audioSource, signal: &QIODevice::readyRead, receiver: this, zero: nullptr); | 
| 415 |             m_audioSource->reset(); | 
| 416 |         } else { | 
| 417 |             delete m_audioSource; | 
| 418 |             m_audioSource = nullptr; | 
| 419 |         } | 
| 420 |     } | 
| 421 |  | 
| 422 |     m_opened = false; | 
| 423 |     m_audioBuffer.clear(); | 
| 424 | } | 
| 425 |  | 
| 426 | void QPulseAudioSink::timerEvent(QTimerEvent *event) | 
| 427 | { | 
| 428 |     if (event->timerId() == m_tickTimer.timerId() && m_pullMode) | 
| 429 |         userFeed(); | 
| 430 |  | 
| 431 |     QPlatformAudioSink::timerEvent(event); | 
| 432 | } | 
| 433 |  | 
| 434 | void QPulseAudioSink::userFeed() | 
| 435 | { | 
| 436 |     int writableSize = bytesFree(); | 
| 437 |  | 
| 438 |     if (writableSize == 0) { | 
| 439 |         // PulseAudio server doesn't want any more data | 
| 440 |         m_stateMachine.activateFromIdle(); | 
| 441 |         return; | 
| 442 |     } | 
| 443 |  | 
| 444 |     // Write up to writableSize | 
| 445 |     const int inputSize = | 
| 446 |             std::min(l: { m_pullingPeriodSize, static_cast<int>(m_audioBuffer.size()), writableSize }); | 
| 447 |  | 
| 448 |     Q_ASSERT(!m_audioBuffer.empty()); | 
| 449 |     int audioBytesPulled = m_audioSource->read(data: m_audioBuffer.data(), maxlen: inputSize); | 
| 450 |     Q_ASSERT(audioBytesPulled <= inputSize); | 
| 451 |  | 
| 452 |     if (audioBytesPulled > 0) { | 
| 453 |         if (audioBytesPulled > inputSize) { | 
| 454 |             qCWarning(qLcPulseAudioOut) | 
| 455 |                     << "Invalid audio data size provided by pull source:"  << audioBytesPulled | 
| 456 |                     << "should be less than"  << inputSize; | 
| 457 |             audioBytesPulled = inputSize; | 
| 458 |         } | 
| 459 |         auto bytesWritten = write(data: m_audioBuffer.data(), len: audioBytesPulled); | 
| 460 |         if (bytesWritten != audioBytesPulled) | 
| 461 |             qWarning() << "Unfinished write:"  << bytesWritten << "vs"  << audioBytesPulled; | 
| 462 |  | 
| 463 |         m_stateMachine.activateFromIdle(); | 
| 464 |  | 
| 465 |         if (inputSize < writableSize) // PulseAudio needs more data. | 
| 466 |             QMetaObject::invokeMethod(object: this, function: &QPulseAudioSink::userFeed, type: Qt::QueuedConnection); | 
| 467 |     } else if (audioBytesPulled == 0) { | 
| 468 |         stopTimer(); | 
| 469 |         const auto atEnd = m_audioSource->atEnd(); | 
| 470 |         qCDebug(qLcPulseAudioOut) << "No more data available, source is done:"  << atEnd; | 
| 471 |     } | 
| 472 | } | 
| 473 |  | 
| 474 | qint64 QPulseAudioSink::write(const char *data, qint64 len) | 
| 475 | { | 
| 476 |     using namespace QPulseAudioInternal; | 
| 477 |  | 
| 478 |     QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 479 |  | 
| 480 |     std::unique_lock engineLock{ *pulseEngine }; | 
| 481 |  | 
| 482 |     size_t nbytes = len; | 
| 483 |     void *dest = nullptr; | 
| 484 |  | 
| 485 |     if (pa_stream_begin_write(p: m_stream.get(), data: &dest, nbytes: &nbytes) < 0) { | 
| 486 |         engineLock.unlock(); | 
| 487 |  | 
| 488 |         qCWarning(qLcPulseAudioOut) | 
| 489 |                 << "pa_stream_begin_write error:"  << currentError(pulseEngine->context()); | 
| 490 |         m_stateMachine.updateActiveOrIdle(activeOrIdle: QAudioStateMachine::RunningState::Idle, error: QAudio::IOError); | 
| 491 |         return 0; | 
| 492 |     } | 
| 493 |  | 
| 494 |     len = qMin(a: len, b: qint64(nbytes)); | 
| 495 |     // Don't use PulseAudio volume, as it might affect all other streams of the same category | 
| 496 |     // or even affect the system volume if flat volumes are enabled | 
| 497 |  | 
| 498 |     QAudioHelperInternal::applyVolume(volume: m_volume, m_format, | 
| 499 |                                       source: QSpan{ reinterpret_cast<const std::byte *>(data), len }, | 
| 500 |                                       destination: QSpan{ reinterpret_cast<std::byte *>(dest), len }); | 
| 501 |  | 
| 502 |     if ((pa_stream_write(p: m_stream.get(), data: dest, nbytes: len, free_cb: nullptr, offset: 0, PA_SEEK_RELATIVE)) < 0) { | 
| 503 |         engineLock.unlock(); | 
| 504 |         qCWarning(qLcPulseAudioOut) | 
| 505 |                 << "pa_stream_write error:"  << currentError(pulseEngine->context()); | 
| 506 |         m_stateMachine.updateActiveOrIdle(activeOrIdle: QAudioStateMachine::RunningState::Idle, error: QAudio::IOError); | 
| 507 |         return 0; | 
| 508 |     } | 
| 509 |  | 
| 510 |     engineLock.unlock(); | 
| 511 |     m_totalTimeValue += len; | 
| 512 |  | 
| 513 |     m_stateMachine.updateActiveOrIdle(activeOrIdle: QAudioStateMachine::RunningState::Active); | 
| 514 |     return len; | 
| 515 | } | 
| 516 |  | 
| 517 | void QPulseAudioSink::stop() | 
| 518 | { | 
| 519 |     if (auto notifier = m_stateMachine.stop()) { | 
| 520 |         { | 
| 521 |             QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 522 |             std::lock_guard lock(*pulseEngine); | 
| 523 |  | 
| 524 |             if (auto prevOp = exchangeDrainOperation(newOperation: nullptr)) | 
| 525 |                 // cancel the draining callback that is not relevant already | 
| 526 |                 pa_operation_cancel(o: prevOp.get()); | 
| 527 |  | 
| 528 |             PAOperationHandle drainOp{ | 
| 529 |                 pa_stream_drain(s: m_stream.get(), cb: outputStreamDrainComplete, userdata: nullptr), | 
| 530 |                 PAOperationHandle::HasRef, | 
| 531 |             }; | 
| 532 |             pulseEngine->wait(op: drainOp); | 
| 533 |         } | 
| 534 |  | 
| 535 |         close(); | 
| 536 |     } | 
| 537 | } | 
| 538 |  | 
| 539 | qsizetype QPulseAudioSink::bytesFree() const | 
| 540 | { | 
| 541 |     if (!m_stateMachine.isActiveOrIdle()) | 
| 542 |         return 0; | 
| 543 |  | 
| 544 |     std::lock_guard lock(*QPulseAudioContextManager::instance()); | 
| 545 |     return pa_stream_writable_size(p: m_stream.get()); | 
| 546 | } | 
| 547 |  | 
| 548 | void QPulseAudioSink::setBufferSize(qsizetype value) | 
| 549 | { | 
| 550 |     m_userBufferSize = value; | 
| 551 | } | 
| 552 |  | 
| 553 | qsizetype QPulseAudioSink::bufferSize() const | 
| 554 | { | 
| 555 |     if (m_bufferSize) | 
| 556 |         return m_bufferSize; | 
| 557 |  | 
| 558 |     if (m_userBufferSize) | 
| 559 |         return *m_userBufferSize; | 
| 560 |  | 
| 561 |     return defaultBufferSize(); | 
| 562 | } | 
| 563 |  | 
| 564 | static qint64 operator-(timeval t1, timeval t2) | 
| 565 | { | 
| 566 |     constexpr qint64 secsToUSecs = 1000000; | 
| 567 |     return (t1.tv_sec - t2.tv_sec) * secsToUSecs + (t1.tv_usec - t2.tv_usec); | 
| 568 | } | 
| 569 |  | 
| 570 | qint64 QPulseAudioSink::processedUSecs() const | 
| 571 | { | 
| 572 |     const auto state = this->state(); | 
| 573 |     if (!m_stream || state == QAudio::StoppedState) | 
| 574 |         return 0; | 
| 575 |     if (state == QAudio::SuspendedState) | 
| 576 |         return lastProcessedUSecs; | 
| 577 |  | 
| 578 |     auto info = pa_stream_get_timing_info(s: m_stream.get()); | 
| 579 |     if (!info) | 
| 580 |         return lastProcessedUSecs; | 
| 581 |  | 
| 582 |     // if the info changed, update our cached data, and recalculate the average latency | 
| 583 |     if (info->timestamp - lastTimingInfo > 0) { | 
| 584 |         lastTimingInfo.tv_sec = info->timestamp.tv_sec; | 
| 585 |         lastTimingInfo.tv_usec = info->timestamp.tv_usec; | 
| 586 |         averageLatency = | 
| 587 |                 0; // also use that as long as we don't have valid data from the timing info | 
| 588 |  | 
| 589 |         // Only use timing values when playing, otherwise the latency numbers can be way off | 
| 590 |         if (info->since_underrun >= 0 | 
| 591 |             && pa_bytes_to_usec(length: info->since_underrun, spec: &m_spec) > info->sink_usec) { | 
| 592 |             latencyList.append(t: info->sink_usec); | 
| 593 |             // Average over the last X timing infos to keep numbers more stable. | 
| 594 |             // 10 seems to be a decent number that keeps values relatively stable but doesn't make | 
| 595 |             // the list too big | 
| 596 |             const int latencyListMaxSize = 10; | 
| 597 |             if (latencyList.size() > latencyListMaxSize) | 
| 598 |                 latencyList.pop_front(); | 
| 599 |             for (const auto l : latencyList) | 
| 600 |                 averageLatency += l; | 
| 601 |             averageLatency /= latencyList.size(); | 
| 602 |             if (averageLatency < 0) | 
| 603 |                 averageLatency = 0; | 
| 604 |         } | 
| 605 |     } | 
| 606 |  | 
| 607 |     const qint64 usecsRead = info->read_index < 0 ? 0 : pa_bytes_to_usec(length: info->read_index, spec: &m_spec); | 
| 608 |     const qint64 usecsWritten = | 
| 609 |             info->write_index < 0 ? 0 : pa_bytes_to_usec(length: info->write_index, spec: &m_spec); | 
| 610 |  | 
| 611 |     // processed data is the amount read by the server minus its latency | 
| 612 |     qint64 usecs = usecsRead - averageLatency; | 
| 613 |  | 
| 614 |     timeval tv; | 
| 615 |     gettimeofday(tv: &tv, tz: nullptr); | 
| 616 |  | 
| 617 |     // and now adjust for the time since the last update | 
| 618 |     qint64 timeSinceUpdate = tv - info->timestamp; | 
| 619 |     if (timeSinceUpdate > 0) | 
| 620 |         usecs += timeSinceUpdate; | 
| 621 |  | 
| 622 |     // We can never have processed more than we've written to the sink | 
| 623 |     if (usecs > usecsWritten) | 
| 624 |         usecs = usecsWritten; | 
| 625 |  | 
| 626 |     // make sure timing is monotonic | 
| 627 |     if (usecs < lastProcessedUSecs) | 
| 628 |         usecs = lastProcessedUSecs; | 
| 629 |     else | 
| 630 |         lastProcessedUSecs = usecs; | 
| 631 |  | 
| 632 |     return usecs; | 
| 633 | } | 
| 634 |  | 
| 635 | void QPulseAudioSink::resume() | 
| 636 | { | 
| 637 |     if (auto notifier = m_stateMachine.resume()) { | 
| 638 |         { | 
| 639 |             QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 640 |  | 
| 641 |             std::lock_guard lock(*pulseEngine); | 
| 642 |  | 
| 643 |             PAOperationHandle operation{ | 
| 644 |                 pa_stream_cork(s: m_stream.get(), b: 0, cb: outputStreamSuccessCallback, userdata: nullptr), | 
| 645 |                 PAOperationHandle::HasRef, | 
| 646 |             }; | 
| 647 |  | 
| 648 |             pulseEngine->wait(op: operation); | 
| 649 |  | 
| 650 |             operation = PAOperationHandle{ | 
| 651 |                 pa_stream_trigger(s: m_stream.get(), cb: outputStreamSuccessCallback, userdata: nullptr), | 
| 652 |                 PAOperationHandle::HasRef, | 
| 653 |             }; | 
| 654 |             pulseEngine->wait(op: operation); | 
| 655 |         } | 
| 656 |  | 
| 657 |         if (m_pullMode) | 
| 658 |             startPulling(); | 
| 659 |     } | 
| 660 | } | 
| 661 |  | 
| 662 | void QPulseAudioSink::setFormat(const QAudioFormat &format) | 
| 663 | { | 
| 664 |     m_format = format; | 
| 665 | } | 
| 666 |  | 
| 667 | QAudioFormat QPulseAudioSink::format() const | 
| 668 | { | 
| 669 |     return m_format; | 
| 670 | } | 
| 671 |  | 
| 672 | void QPulseAudioSink::suspend() | 
| 673 | { | 
| 674 |     if (auto notifier = m_stateMachine.suspend()) { | 
| 675 |         stopTimer(); | 
| 676 |  | 
| 677 |         QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance(); | 
| 678 |  | 
| 679 |         std::lock_guard lock(*pulseEngine); | 
| 680 |  | 
| 681 |         PAOperationHandle operation{ | 
| 682 |             pa_stream_cork(s: m_stream.get(), b: 1, cb: outputStreamSuccessCallback, userdata: nullptr), | 
| 683 |             PAOperationHandle::HasRef, | 
| 684 |         }; | 
| 685 |         pulseEngine->wait(op: operation); | 
| 686 |     } | 
| 687 | } | 
| 688 |  | 
| 689 | void QPulseAudioSink::reset() | 
| 690 | { | 
| 691 |     if (auto notifier = m_stateMachine.stopOrUpdateError()) | 
| 692 |         close(); | 
| 693 | } | 
| 694 |  | 
| 695 | PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio) | 
| 696 | { | 
| 697 |     m_audioDevice = qobject_cast<QPulseAudioSink *>(object: audio); | 
| 698 | } | 
| 699 |  | 
| 700 | qint64 PulseOutputPrivate::readData(char *data, qint64 len) | 
| 701 | { | 
| 702 |     Q_UNUSED(data); | 
| 703 |     Q_UNUSED(len); | 
| 704 |  | 
| 705 |     return 0; | 
| 706 | } | 
| 707 |  | 
| 708 | qint64 PulseOutputPrivate::writeData(const char *data, qint64 len) | 
| 709 | { | 
| 710 |     qint64 written = 0; | 
| 711 |  | 
| 712 |     const auto state = m_audioDevice->state(); | 
| 713 |     if (state == QAudio::ActiveState || state == QAudio::IdleState) { | 
| 714 |         while (written < len) { | 
| 715 |             int chunk = m_audioDevice->write(data: data + written, len: (len - written)); | 
| 716 |             if (chunk <= 0) | 
| 717 |                 return written; | 
| 718 |             written += chunk; | 
| 719 |         } | 
| 720 |     } | 
| 721 |  | 
| 722 |     return written; | 
| 723 | } | 
| 724 |  | 
| 725 | void QPulseAudioSink::setVolume(qreal vol) | 
| 726 | { | 
| 727 |     if (qFuzzyCompare(p1: m_volume, p2: vol)) | 
| 728 |         return; | 
| 729 |  | 
| 730 |     m_volume = qBound(min: qreal(0), val: vol, max: qreal(1)); | 
| 731 | } | 
| 732 |  | 
| 733 | qreal QPulseAudioSink::volume() const | 
| 734 | { | 
| 735 |     return m_volume; | 
| 736 | } | 
| 737 |  | 
| 738 | void QPulseAudioSink::onPulseContextFailed() | 
| 739 | { | 
| 740 |     if (auto notifier = m_stateMachine.stop(error: QAudio::FatalError)) | 
| 741 |         close(); | 
| 742 | } | 
| 743 |  | 
| 744 | auto QPulseAudioSink::exchangeDrainOperation(pa_operation *newOperation) -> PAOperationHandle | 
| 745 | { | 
| 746 |     return PAOperationHandle{ | 
| 747 |         m_drainOperation.exchange(p: newOperation), | 
| 748 |         PAOperationHandle::HasRef, | 
| 749 |     }; | 
| 750 | } | 
| 751 |  | 
| 752 | qsizetype QPulseAudioSink::defaultBufferSize() const | 
| 753 | { | 
| 754 |     if (m_spec.rate > 0) | 
| 755 |         return pa_usec_to_bytes(t: DefaultBufferLengthMs * 1000, spec: &m_spec); | 
| 756 |  | 
| 757 |     auto spec = QPulseAudioInternal::audioFormatToSampleSpec(format: m_format); | 
| 758 |     if (pa_sample_spec_valid(spec: &spec)) | 
| 759 |         return pa_usec_to_bytes(t: DefaultBufferLengthMs * 1000, spec: &spec); | 
| 760 |  | 
| 761 |     return 0; | 
| 762 | } | 
| 763 |  | 
| 764 | QT_END_NAMESPACE | 
| 765 |  | 
| 766 | #include "moc_qpulseaudiosink_p.cpp" | 
| 767 |  |