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