diff options
Diffstat (limited to 'src/multimedia/pulseaudio/qpulseaudiosink.cpp')
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink.cpp | 518 |
1 files changed, 246 insertions, 272 deletions
diff --git a/src/multimedia/pulseaudio/qpulseaudiosink.cpp b/src/multimedia/pulseaudio/qpulseaudiosink.cpp index b3e39d746..610677eeb 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosink.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiosink.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QtCore/qcoreapplication.h> #include <QtCore/qdebug.h> @@ -43,21 +7,20 @@ #include <private/qaudiohelpers_p.h> #include "qpulseaudiosink_p.h" -#include "qpulseaudiodevice_p.h" #include "qaudioengine_pulse_p.h" #include "qpulsehelpers_p.h" #include <sys/types.h> #include <unistd.h> - -Q_DECLARE_LOGGING_CATEGORY(qLcPulseAudio) +#include <mutex> // for std::lock_guard QT_BEGIN_NAMESPACE -const int PeriodTimeMs = 20; +static constexpr uint SinkPeriodTimeMs = 20; +static constexpr uint DefaultBufferLengthMs = 100; #define LOW_LATENCY_CATEGORY_NAME "game" -static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) +static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) { Q_UNUSED(stream); Q_UNUSED(userdata); @@ -72,17 +35,19 @@ static void outputStreamStateCallback(pa_stream *stream, void *userdata) pa_stream_state_t state = pa_stream_get_state(stream); qCDebug(qLcPulseAudioOut) << "Stream state callback:" << state; switch (state) { - case PA_STREAM_CREATING: - case PA_STREAM_READY: - case PA_STREAM_TERMINATED: - break; - - case PA_STREAM_FAILED: - default: - qWarning() << QString::fromLatin1("Stream error: %1").arg(QString::fromUtf8(pa_strerror(pa_context_errno(pa_stream_get_context(stream))))); - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); - break; + case PA_STREAM_CREATING: + case PA_STREAM_READY: + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_FAILED: + default: + qWarning() << QStringLiteral("Stream error: %1") + .arg(QString::fromUtf8(pa_strerror( + pa_context_errno(pa_stream_get_context(stream))))); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + break; } } @@ -133,90 +98,73 @@ static void outputStreamDrainComplete(pa_stream *stream, int success, void *user { Q_UNUSED(stream); - qCDebug(qLcPulseAudioOut) << "Stream drained:" << bool(success) << userdata; - if (success && userdata) + qCDebug(qLcPulseAudioOut) << "Stream drained:" << static_cast<bool>(success) << userdata; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + + if (userdata && success) static_cast<QPulseAudioSink *>(userdata)->streamDrainedCallback(); } +static void outputStreamFlushComplete(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + + qCDebug(qLcPulseAudioOut) << "Stream flushed:" << static_cast<bool>(success) << userdata; +} + static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void *userdata) { Q_UNUSED(stream); Q_UNUSED(success); Q_UNUSED(userdata); - qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:" << bool(success); + qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:" << static_cast<bool>(success); } - -QPulseAudioSink::QPulseAudioSink(const QByteArray &device) - : m_device(device) +QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent) + : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this) { } QPulseAudioSink::~QPulseAudioSink() { - close(); - QCoreApplication::processEvents(); -} - -void QPulseAudioSink::setError(QAudio::Error error) -{ - if (m_errorState == error) - return; - - m_errorState = error; - emit errorChanged(error); + if (auto notifier = m_stateMachine.stop()) + close(); } QAudio::Error QPulseAudioSink::error() const { - return m_errorState; -} - -void QPulseAudioSink::setState(QAudio::State state) -{ - if (m_deviceState == state) - return; - - m_deviceState = state; - emit stateChanged(state); + return m_stateMachine.error(); } QAudio::State QPulseAudioSink::state() const { - return m_deviceState; + return m_stateMachine.state(); } void QPulseAudioSink::streamUnderflowCallback() { - if (m_audioSource && m_audioSource->atEnd()) { + bool atEnd = m_audioSource && m_audioSource->atEnd(); + if (atEnd && m_stateMachine.state() != QAudio::StoppedState) { qCDebug(qLcPulseAudioOut) << "Draining stream at end of buffer"; - pa_operation *o = pa_stream_drain(m_stream, outputStreamDrainComplete, this); - if (o) - pa_operation_unref(o); - } else if (m_deviceState != QAudio::IdleState && !m_resuming) { - setError(QAudio::UnderrunError); - setState(QAudio::IdleState); + exchangeDrainOperation(pa_stream_drain(m_stream, outputStreamDrainComplete, this)); } + + m_stateMachine.updateActiveOrIdle( + false, (m_pullMode && atEnd) ? QAudio::NoError : QAudio::UnderrunError); } void QPulseAudioSink::streamDrainedCallback() { - setError(QAudio::NoError); - setState(QAudio::IdleState); + if (!exchangeDrainOperation(nullptr)) + return; } void QPulseAudioSink::start(QIODevice *device) { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - // Handle change of mode - if (m_audioSource && !m_pullMode) - delete m_audioSource; - m_audioSource = nullptr; - - close(); + reset(); m_pullMode = true; m_audioSource = device; @@ -230,27 +178,29 @@ void QPulseAudioSink::start(QIODevice *device) gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; - connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startReading); - setState(QAudio::ActiveState); + connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startPulling); + + m_stateMachine.start(); } -void QPulseAudioSink::startReading() +void QPulseAudioSink::startPulling() { - if (!m_tickTimer.isActive()) - m_tickTimer.start(m_periodTime, this); + Q_ASSERT(m_pullMode); + if (m_tickTimer.isActive()) + return; + + m_tickTimer.start(m_pullingPeriodTime, this); } -QIODevice *QPulseAudioSink::start() +void QPulseAudioSink::stopTimer() { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - // Handle change of mode - if (m_audioSource && !m_pullMode) - delete m_audioSource; - m_audioSource = nullptr; + if (m_tickTimer.isActive()) + m_tickTimer.stop(); +} - close(); +QIODevice *QPulseAudioSink::start() +{ + reset(); m_pullMode = false; @@ -258,13 +208,13 @@ QIODevice *QPulseAudioSink::start() return nullptr; m_audioSource = new PulseOutputPrivate(this); - m_audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + m_audioSource->open(QIODevice::WriteOnly | QIODevice::Unbuffered); // ensure we only process timing infos that are up to date gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; - setState(QAudio::IdleState); + m_stateMachine.start(false); return m_audioSource; } @@ -276,10 +226,9 @@ bool QPulseAudioSink::open() QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { - setError(QAudio::FatalError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + if (!pulseEngine->context() + || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + m_stateMachine.stopOrUpdateError(QAudio::FatalError); return false; } @@ -288,9 +237,7 @@ bool QPulseAudioSink::open() Q_ASSERT(spec.channels == channel_map.channels); if (!pa_sample_spec_valid(&spec)) { - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -298,11 +245,12 @@ bool QPulseAudioSink::open() m_totalTimeValue = 0; if (m_streamName.isNull()) - m_streamName = QString(QLatin1String("QtmPulseStream-%1-%2")).arg(::getpid()).arg(quintptr(this)).toUtf8(); + m_streamName = + QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8(); if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { qCDebug(qLcPulseAudioOut) << "Opening stream with."; - qCDebug(qLcPulseAudioOut) << "\tFormat: " << QPulseAudioInternal::sampleFormatToQString(spec.format); + qCDebug(qLcPulseAudioOut) << "\tFormat: " << spec.format; qCDebug(qLcPulseAudioOut) << "\tRate: " << spec.rate; qCDebug(qLcPulseAudioOut) << "\tChannels: " << spec.channels; qCDebug(qLcPulseAudioOut) << "\tFrame size: " << pa_frame_size(&spec); @@ -310,7 +258,6 @@ bool QPulseAudioSink::open() pulseEngine->lock(); - pa_proplist *propList = pa_proplist_new(); #if 0 qint64 bytesPerSecond = m_format.sampleRate() * m_format.bytesPerFrame(); @@ -332,18 +279,18 @@ bool QPulseAudioSink::open() pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, r); #endif - m_stream = pa_stream_new_with_proplist(pulseEngine->context(), m_streamName.constData(), &m_spec, &channel_map, propList); + m_stream = pa_stream_new_with_proplist(pulseEngine->context(), m_streamName.constData(), + &m_spec, &channel_map, propList); + pa_proplist_free(propList); + if (!m_stream) { qCWarning(qLcPulseAudioOut) << "QAudioSink: pa_stream_new_with_proplist() failed!"; pulseEngine->unlock(); - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } - pa_proplist_free(propList); - pa_stream_set_state_callback(m_stream, outputStreamStateCallback, this); pa_stream_set_write_callback(m_stream, outputStreamWriteCallback, this); @@ -352,21 +299,26 @@ bool QPulseAudioSink::open() pa_stream_set_latency_update_callback(m_stream, outputStreamLatencyCallback, this); pa_buffer_attr requestedBuffer; - requestedBuffer.fragsize = (uint32_t)-1; - requestedBuffer.maxlength = (uint32_t)-1; - requestedBuffer.minreq = (uint32_t)-1; - requestedBuffer.prebuf = (uint32_t)-1; - requestedBuffer.tlength = m_bufferSize; - - pa_stream_flags flags = pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_ADJUST_LATENCY); - if (pa_stream_connect_playback(m_stream, m_device.data(), (m_bufferSize > 0) ? &requestedBuffer : nullptr, flags, nullptr, nullptr) < 0) { + // Request a target buffer size + auto targetBufferSize = m_userBufferSize ? *m_userBufferSize : defaultBufferSize(); + requestedBuffer.tlength = + targetBufferSize ? static_cast<uint32_t>(targetBufferSize) : static_cast<uint32_t>(-1); + // Rest should be determined by PulseAudio + requestedBuffer.fragsize = static_cast<uint32_t>(-1); + requestedBuffer.maxlength = static_cast<uint32_t>(-1); + requestedBuffer.minreq = static_cast<uint32_t>(-1); + requestedBuffer.prebuf = static_cast<uint32_t>(-1); + + pa_stream_flags flags = + pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY); + if (pa_stream_connect_playback(m_stream, m_device.data(), &requestedBuffer, flags, nullptr, + nullptr) + < 0) { qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!"; pa_stream_unref(m_stream); m_stream = nullptr; pulseEngine->unlock(); - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -374,20 +326,25 @@ bool QPulseAudioSink::open() pa_threaded_mainloop_wait(pulseEngine->mainloop()); const pa_buffer_attr *buffer = pa_stream_get_buffer_attr(m_stream); - m_periodTime = PeriodTimeMs; - m_periodSize = pa_usec_to_bytes(m_periodTime * 1000, &m_spec); m_bufferSize = buffer->tlength; - m_maxBufferSize = buffer->maxlength; - m_audioBuffer = new char[m_maxBufferSize]; + + if (m_pullMode) { + // Adjust period time to reduce chance of it being higher than amount of bytes requested by + // PulseAudio server + m_pullingPeriodTime = + qMin(SinkPeriodTimeMs, pa_bytes_to_usec(m_bufferSize, &m_spec) / 1000 / 2); + m_pullingPeriodSize = pa_usec_to_bytes(m_pullingPeriodTime * 1000, &m_spec); + } + + m_audioBuffer.resize(buffer->maxlength); const qint64 streamSize = m_audioSource ? m_audioSource->size() : 0; if (m_pullMode && streamSize > 0 && static_cast<qint64>(buffer->prebuf) > streamSize) { pa_buffer_attr newBufferAttr; newBufferAttr = *buffer; newBufferAttr.prebuf = streamSize; - pa_operation *o = pa_stream_set_buffer_attr(m_stream, &newBufferAttr, streamAdjustPrebufferCallback, nullptr); - if (o) - pa_operation_unref(o); + PAOperationUPtr(pa_stream_set_buffer_attr(m_stream, &newBufferAttr, + streamAdjustPrebufferCallback, nullptr)); } if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { @@ -401,11 +358,13 @@ bool QPulseAudioSink::open() pulseEngine->unlock(); - connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSink::onPulseContextFailed); + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSink::onPulseContextFailed); m_opened = true; - startReading(); + if (m_pullMode) + startPulling(); m_elapsedTimeOffset = 0; @@ -417,12 +376,12 @@ void QPulseAudioSink::close() if (!m_opened) return; - m_tickTimer.stop(); + stopTimer(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); if (m_stream) { - pulseEngine->lock(); + std::lock_guard lock(*pulseEngine); pa_stream_set_state_callback(m_stream, nullptr, nullptr); pa_stream_set_write_callback(m_stream, nullptr, nullptr); @@ -430,37 +389,37 @@ void QPulseAudioSink::close() pa_stream_set_overflow_callback(m_stream, nullptr, nullptr); pa_stream_set_latency_update_callback(m_stream, nullptr, nullptr); - pa_operation *o = pa_stream_drain(m_stream, outputStreamDrainComplete, nullptr); - if (o) - pa_operation_unref(o); + if (auto prevOp = exchangeDrainOperation(nullptr)) + // cancel draining operation to prevent calling draining callback after closing. + pa_operation_cancel(prevOp.get()); + + PAOperationUPtr operation(pa_stream_flush(m_stream, outputStreamFlushComplete, nullptr)); pa_stream_disconnect(m_stream); pa_stream_unref(m_stream); m_stream = nullptr; - - pulseEngine->unlock(); } - disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSink::onPulseContextFailed); + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSink::onPulseContextFailed); if (m_audioSource) { if (m_pullMode) { disconnect(m_audioSource, &QIODevice::readyRead, this, nullptr); + m_audioSource->reset(); } else { delete m_audioSource; m_audioSource = nullptr; } } + m_opened = false; - if (m_audioBuffer) { - delete[] m_audioBuffer; - m_audioBuffer = nullptr; - } + m_audioBuffer.clear(); } void QPulseAudioSink::timerEvent(QTimerEvent *event) { - if (event->timerId() == m_tickTimer.timerId()) + if (event->timerId() == m_tickTimer.timerId() && m_pullMode) userFeed(); QPlatformAudioSink::timerEvent(event); @@ -468,54 +427,48 @@ void QPulseAudioSink::timerEvent(QTimerEvent *event) void QPulseAudioSink::userFeed() { - if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState) + int writableSize = bytesFree(); + + if (writableSize == 0) { + // PulseAudio server doesn't want any more data + m_stateMachine.activateFromIdle(); return; + } - m_resuming = false; + // Write up to writableSize + const int inputSize = + std::min({ m_pullingPeriodSize, static_cast<int>(m_audioBuffer.size()), writableSize }); - if (m_pullMode) { - setError(QAudio::NoError); - setState(QAudio::ActiveState); - int writableSize = bytesFree(); - int chunks = writableSize / m_periodSize; - if (chunks == 0) - return; - - int input = m_periodSize; // always request 1 chunk of data from user - if (input > m_maxBufferSize) - input = m_maxBufferSize; - - Q_ASSERT(m_audioBuffer); - int audioBytesPulled = m_audioSource->read(m_audioBuffer, input); - Q_ASSERT(audioBytesPulled <= input); - if (audioBytesPulled > 0) { - if (audioBytesPulled > input) { - qCWarning(qLcPulseAudioOut) << "Invalid audio data size provided by pull source:" - << audioBytesPulled << "should be less than" << input; - audioBytesPulled = input; - } - qint64 bytesWritten = write(m_audioBuffer, audioBytesPulled); - Q_ASSERT(bytesWritten == audioBytesPulled); //unfinished write should not happen since the data provided is less than writableSize - Q_UNUSED(bytesWritten); - - if (chunks > 1) { - // PulseAudio needs more data. Ask for it immediately. - QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); - } - } else if (audioBytesPulled == 0) { - m_tickTimer.stop(); - qCDebug(qLcPulseAudioOut) << "No more data available, source is done:" << m_audioSource->atEnd(); - setError(m_audioSource->atEnd() ? QAudio::NoError : QAudio::UnderrunError); - setState(QAudio::IdleState); + Q_ASSERT(!m_audioBuffer.empty()); + int audioBytesPulled = m_audioSource->read(m_audioBuffer.data(), inputSize); + Q_ASSERT(audioBytesPulled <= inputSize); + + if (audioBytesPulled > 0) { + if (audioBytesPulled > inputSize) { + qCWarning(qLcPulseAudioOut) + << "Invalid audio data size provided by pull source:" << audioBytesPulled + << "should be less than" << inputSize; + audioBytesPulled = inputSize; } + auto bytesWritten = write(m_audioBuffer.data(), audioBytesPulled); + if (bytesWritten != audioBytesPulled) + qWarning() << "Unfinished write:" << bytesWritten << "vs" << audioBytesPulled; + + m_stateMachine.activateFromIdle(); + + if (inputSize < writableSize) // PulseAudio needs more data. + QMetaObject::invokeMethod(this, &QPulseAudioSink::userFeed, Qt::QueuedConnection); + } else if (audioBytesPulled == 0) { + stopTimer(); + const auto atEnd = m_audioSource->atEnd(); + qCDebug(qLcPulseAudioOut) << "No more data available, source is done:" << atEnd; } - - if (m_deviceState != QAudio::ActiveState) - return; } qint64 QPulseAudioSink::write(const char *data, qint64 len) { + using namespace QPulseAudioInternal; + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); pulseEngine->lock(); @@ -524,9 +477,10 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) void *dest = nullptr; if (pa_stream_begin_write(m_stream, &dest, &nbytes) < 0) { - qCWarning(qLcPulseAudioOut) << "pa_stream_begin_write error:" - << pa_strerror(pa_context_errno(pulseEngine->context())); - setError(QAudio::IOError); + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_begin_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } @@ -543,65 +497,76 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) data = reinterpret_cast<char *>(dest); if ((pa_stream_write(m_stream, data, len, nullptr, 0, PA_SEEK_RELATIVE)) < 0) { - qCWarning(qLcPulseAudioOut) << "pa_stream_write error:" - << pa_strerror(pa_context_errno(pulseEngine->context())); - setError(QAudio::IOError); + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } pulseEngine->unlock(); m_totalTimeValue += len; - setError(QAudio::NoError); - setState(QAudio::ActiveState); - + m_stateMachine.updateActiveOrIdle(true); return len; } void QPulseAudioSink::stop() { - if (m_deviceState == QAudio::StoppedState) - return; + if (auto notifier = m_stateMachine.stop()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + std::lock_guard lock(*pulseEngine); - close(); + if (auto prevOp = exchangeDrainOperation(nullptr)) + // cancel the draining callback that is not relevant already + pa_operation_cancel(prevOp.get()); - setError(QAudio::NoError); - setState(QAudio::StoppedState); + PAOperationUPtr drainOp(pa_stream_drain(m_stream, outputStreamDrainComplete, nullptr)); + pulseEngine->wait(drainOp.get()); + } + + close(); + } } qsizetype QPulseAudioSink::bytesFree() const { - if (m_deviceState != QAudio::ActiveState && m_deviceState != QAudio::IdleState) + if (!m_stateMachine.isActiveOrIdle()) return 0; - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pulseEngine->lock(); - int writableSize = pa_stream_writable_size(m_stream); - pulseEngine->unlock(); - return writableSize; + std::lock_guard lock(*QPulseAudioEngine::instance()); + return pa_stream_writable_size(m_stream); } void QPulseAudioSink::setBufferSize(qsizetype value) { - m_bufferSize = value; + m_userBufferSize = value; } qsizetype QPulseAudioSink::bufferSize() const { - return m_bufferSize; + if (m_bufferSize) + return m_bufferSize; + + if (m_userBufferSize) + return *m_userBufferSize; + + return defaultBufferSize(); } static qint64 operator-(timeval t1, timeval t2) { constexpr qint64 secsToUSecs = 1000000; - return (t1.tv_sec - t2.tv_sec)*secsToUSecs + (t1.tv_usec - t2.tv_usec); + return (t1.tv_sec - t2.tv_sec) * secsToUSecs + (t1.tv_usec - t2.tv_usec); } qint64 QPulseAudioSink::processedUSecs() const { - if (!m_stream || m_deviceState == QAudio::StoppedState) + const auto state = this->state(); + if (!m_stream || state == QAudio::StoppedState) return 0; - if (m_deviceState == QAudio::SuspendedState) + if (state == QAudio::SuspendedState) return lastProcessedUSecs; auto info = pa_stream_get_timing_info(m_stream); @@ -612,13 +577,16 @@ qint64 QPulseAudioSink::processedUSecs() const if (info->timestamp - lastTimingInfo > 0) { lastTimingInfo.tv_sec = info->timestamp.tv_sec; lastTimingInfo.tv_usec = info->timestamp.tv_usec; - averageLatency = 0; // also use that as long as we don't have valid data from the timing info + averageLatency = + 0; // also use that as long as we don't have valid data from the timing info // Only use timing values when playing, otherwise the latency numbers can be way off - if (info->since_underrun >= 0 && pa_bytes_to_usec(info->since_underrun, &m_spec) > info->sink_usec) { + if (info->since_underrun >= 0 + && pa_bytes_to_usec(info->since_underrun, &m_spec) > info->sink_usec) { latencyList.append(info->sink_usec); // Average over the last X timing infos to keep numbers more stable. - // 10 seems to be a decent number that keeps values relatively stable but doesn't make the list too big + // 10 seems to be a decent number that keeps values relatively stable but doesn't make + // the list too big const int latencyListMaxSize = 10; if (latencyList.size() > latencyListMaxSize) latencyList.pop_front(); @@ -631,7 +599,8 @@ qint64 QPulseAudioSink::processedUSecs() const } const qint64 usecsRead = info->read_index < 0 ? 0 : pa_bytes_to_usec(info->read_index, &m_spec); - const qint64 usecsWritten = info->write_index < 0 ? 0 : pa_bytes_to_usec(info->write_index, &m_spec); + const qint64 usecsWritten = + info->write_index < 0 ? 0 : pa_bytes_to_usec(info->write_index, &m_spec); // processed data is the amount read by the server minus its latency qint64 usecs = usecsRead - averageLatency; @@ -659,27 +628,22 @@ qint64 QPulseAudioSink::processedUSecs() const void QPulseAudioSink::resume() { - if (m_deviceState == QAudio::SuspendedState) { - m_resuming = true; - - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - - pulseEngine->lock(); - - pa_operation *operation = pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + if (auto notifier = m_stateMachine.resume()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - operation = pa_stream_trigger(m_stream, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + std::lock_guard lock(*pulseEngine); - pulseEngine->unlock(); + PAOperationUPtr operation( + pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); - m_tickTimer.start(m_periodTime, this); + operation.reset(pa_stream_trigger(m_stream, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } - setState(m_pullMode ? QAudio::ActiveState : QAudio::IdleState); - setError(QAudio::NoError); + if (m_pullMode) + startPulling(); } } @@ -695,33 +659,28 @@ QAudioFormat QPulseAudioSink::format() const void QPulseAudioSink::suspend() { - if (m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState) { - setError(QAudio::NoError); - setState(QAudio::SuspendedState); - - m_tickTimer.stop(); + if (auto notifier = m_stateMachine.suspend()) { + stopTimer(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_operation *operation; - - pulseEngine->lock(); - operation = pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + std::lock_guard lock(*pulseEngine); - pulseEngine->unlock(); + PAOperationUPtr operation( + pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); } } void QPulseAudioSink::reset() { - stop(); + if (auto notifier = m_stateMachine.stopOrUpdateError()) + close(); } PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio) { - m_audioDevice = qobject_cast<QPulseAudioSink*>(audio); + m_audioDevice = qobject_cast<QPulseAudioSink *>(audio); } qint64 PulseOutputPrivate::readData(char *data, qint64 len) @@ -736,13 +695,13 @@ qint64 PulseOutputPrivate::writeData(const char *data, qint64 len) { qint64 written = 0; - if ((m_audioDevice->m_deviceState == QAudio::ActiveState - || m_audioDevice->m_deviceState == QAudio::IdleState)) { - while(written < len) { - int chunk = m_audioDevice->write(data+written, (len-written)); + const auto state = m_audioDevice->state(); + if (state == QAudio::ActiveState || state == QAudio::IdleState) { + while (written < len) { + int chunk = m_audioDevice->write(data + written, (len - written)); if (chunk <= 0) return written; - written+=chunk; + written += chunk; } } @@ -764,10 +723,25 @@ qreal QPulseAudioSink::volume() const void QPulseAudioSink::onPulseContextFailed() { - close(); + if (auto notifier = m_stateMachine.stop(QAudio::FatalError)) + close(); +} - setError(QAudio::FatalError); - setState(QAudio::StoppedState); +PAOperationUPtr QPulseAudioSink::exchangeDrainOperation(pa_operation *newOperation) +{ + return PAOperationUPtr(m_drainOperation.exchange(newOperation)); +} + +qsizetype QPulseAudioSink::defaultBufferSize() const +{ + if (m_spec.rate > 0) + return pa_usec_to_bytes(DefaultBufferLengthMs * 1000, &m_spec); + + auto spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + if (pa_sample_spec_valid(&spec)) + return pa_usec_to_bytes(DefaultBufferLengthMs * 1000, &spec); + + return 0; } QT_END_NAMESPACE |