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
17QT_BEGIN_NAMESPACE
18
19static constexpr int SinkPeriodTimeMs = 20;
20
21#define LOW_LATENCY_CATEGORY_NAME "game"
22
23static 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
32static 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
52static 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
60static void outputStreamOverflowCallback(pa_stream *stream, void *userdata)
61{
62 Q_UNUSED(stream);
63 Q_UNUSED(userdata);
64 qCDebug(qLcPulseAudioOut) << "Buffer overflow";
65}
66
67static 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
85static 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
95static 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
105static 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
114QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent)
115 : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this, false)
116{
117}
118
119QPulseAudioSink::~QPulseAudioSink()
120{
121 if (auto notifier = m_stateMachine.stop()) {
122 close();
123 QSignalBlocker blocker(this);
124 notifier.reset();
125 }
126}
127
128QAudio::Error QPulseAudioSink::error() const
129{
130 return m_stateMachine.error();
131}
132
133QAudio::State QPulseAudioSink::state() const
134{
135 return m_stateMachine.state();
136}
137
138void 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
149void QPulseAudioSink::streamDrainedCallback()
150{
151 if (!exchangeDrainOperation(newOperation: nullptr))
152 return;
153
154 m_stateMachine.updateActiveOrIdle(isActive: false);
155}
156
157void 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
179void QPulseAudioSink::startReading()
180{
181 if (!m_tickTimer.isActive())
182 m_tickTimer.start(msec: m_periodTime, obj: this);
183}
184
185QIODevice *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
207bool 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
341void 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
385void QPulseAudioSink::timerEvent(QTimerEvent *event)
386{
387 if (event->timerId() == m_tickTimer.timerId())
388 userFeed();
389
390 QPlatformAudioSink::timerEvent(event);
391}
392
393void 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
444qint64 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
488void QPulseAudioSink::stop()
489{
490 if (auto notifier = m_stateMachine.stop())
491 close();
492}
493
494qsizetype 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
503void QPulseAudioSink::setBufferSize(qsizetype value)
504{
505 m_bufferSize = value;
506}
507
508qsizetype QPulseAudioSink::bufferSize() const
509{
510 return m_bufferSize;
511}
512
513static 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
519qint64 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
580void 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
602void QPulseAudioSink::setFormat(const QAudioFormat &format)
603{
604 m_format = format;
605}
606
607QAudioFormat QPulseAudioSink::format() const
608{
609 return m_format;
610}
611
612void 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
627void QPulseAudioSink::reset()
628{
629 if (auto notifier = m_stateMachine.stopOrUpdateError())
630 close();
631}
632
633PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio)
634{
635 m_audioDevice = qobject_cast<QPulseAudioSink*>(object: audio);
636}
637
638qint64 PulseOutputPrivate::readData(char *data, qint64 len)
639{
640 Q_UNUSED(data);
641 Q_UNUSED(len);
642
643 return 0;
644}
645
646qint64 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
663void 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
671qreal QPulseAudioSink::volume() const
672{
673 return m_volume;
674}
675
676void QPulseAudioSink::onPulseContextFailed()
677{
678 if (auto notifier = m_stateMachine.stop(error: QAudio::FatalError))
679 close();
680}
681
682PAOperationUPtr QPulseAudioSink::exchangeDrainOperation(pa_operation *newOperation)
683{
684 return PAOperationUPtr(m_drainOperation.exchange(p: newOperation));
685}
686
687QT_END_NAMESPACE
688
689#include "moc_qpulseaudiosink_p.cpp"
690

source code of qtmultimedia/src/multimedia/pulseaudio/qpulseaudiosink.cpp