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