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 |
Definitions
- SinkPeriodTimeMs
- DefaultBufferLengthMs
- outputStreamWriteCallback
- outputStreamStateCallback
- outputStreamUnderflowCallback
- outputStreamOverflowCallback
- outputStreamLatencyCallback
- outputStreamSuccessCallback
- outputStreamDrainComplete
- outputStreamFlushComplete
- streamAdjustPrebufferCallback
- QPulseAudioSink
- ~QPulseAudioSink
- error
- state
- streamUnderflowCallback
- streamDrainedCallback
- start
- startPulling
- stopTimer
- start
- open
- close
- timerEvent
- userFeed
- write
- stop
- bytesFree
- setBufferSize
- bufferSize
- operator-
- processedUSecs
- resume
- setFormat
- format
- suspend
- reset
- PulseOutputPrivate
- readData
- writeData
- setVolume
- volume
- onPulseContextFailed
- exchangeDrainOperation
Learn to use CMake with our Intro Training
Find out more