Revision control
++ // 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 "qpulseaudiosink_p.h"
++
++ #include <QtMultimedia/private/qaudiohelpers_p.h>
++ #include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
++ #include <QtMultimedia/private/qpulseaudio_contextmanager_p.h>
++ #include <QtMultimedia/private/qpulsehelpers_p.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qsemaphore.h>
++
++ #include <mutex> // for std::lock_guard
++ #include <unistd.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QPulseAudioInternal {
++
++ QPulseAudioSinkStream::QPulseAudioSinkStream(QAudioDevice device, const QAudioFormat &format,
++ std::optional<qsizetype> ringbufferSize, QPulseAudioSink *parent,
++ float volume,
++ std::optional<NativePeriodFrames> nativePeriodFrames,
++ AudioEndpointRole role)
++ : QPlatformAudioSinkStream{
++ std::move(device), format, ringbufferSize, nativePeriodFrames, volume,
++ },
++ m_parent{
++ parent,
++ }
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++
++ pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(format);
++ pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(format);
++
++ if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) {
++ qCDebug(qLcPulseAudioOut) << "Opening stream with.";
++ qCDebug(qLcPulseAudioOut) << "\tFormat: " << spec.format;
++ qCDebug(qLcPulseAudioOut) << "\tRate: " << spec.rate;
++ qCDebug(qLcPulseAudioOut) << "\tChannels: " << spec.channels;
++ qCDebug(qLcPulseAudioOut) << "\tFrame size: " << pa_frame_size(&spec);
++ }
++
++ const QByteArray streamName =
++ QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8();
++
++ PAProplistHandle propList{
++ pa_proplist_new(),
++ };
++ const char *roleString = [&]() -> const char * {
++ switch (role) {
++ case AudioEndpointRole::MediaPlayback:
++ return "music";
++ case AudioEndpointRole::SoundEffect:
++ return "event";
++ case AudioEndpointRole::Accessibility:
++ return "a11y";
++ case AudioEndpointRole::Other:
++ return nullptr;
++ default:
++ Q_UNREACHABLE_RETURN(nullptr);
++ }
++ }();
++
++ if (roleString)
++ pa_proplist_sets(propList.get(), PA_PROP_MEDIA_ROLE, roleString);
++
++ std::lock_guard engineLock{ *pulseEngine };
++
++ m_stream = PAStreamHandle{
++ pa_stream_new_with_proplist(pulseEngine->context(), streamName.constData(), &spec,
++ &channel_map, propList.get()),
++ PAStreamHandle::HasRef,
++ };
++
++ if (!m_stream) {
++ qWarning() << "Failed to create PulseAudio stream";
++ return;
++ }
++ }
++
++ QPulseAudioSinkStream::~QPulseAudioSinkStream()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++ m_stream = {};
++ }
++
++ bool QPulseAudioSinkStream::start(QIODevice *device)
++ {
++ setQIODevice(device);
++ pullFromQIODevice();
++
++ createQIODeviceConnections(device);
++
++ bool streamStarted = startStream(StreamType::Ringbuffer);
++ return streamStarted;
++ }
++
++ bool QPulseAudioSinkStream::start(AudioCallback &&callback)
++ {
++ m_audioCallback = std::move(callback);
++
++ bool streamStarted = startStream(StreamType::Callback);
++ return streamStarted;
++ }
++
++ QIODevice *QPulseAudioSinkStream::start()
++ {
++ QIODevice *device = createRingbufferWriterDevice();
++
++ setIdleState(true);
++ bool started = start(device);
++ if (!started)
++ return nullptr;
++
++ return device;
++ }
++
++ void QPulseAudioSinkStream::stop(ShutdownPolicy policy)
++ {
++ requestStop();
++
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ uninstallCallbacks();
++ // Note: we need to cork to ensure that the stream is stopped immediately
++ auto op = streamCork(m_stream, true);
++ pulseEngine->waitForAsyncOperation(op);
++
++ if (m_audioCallback) {
++ switch (policy) {
++ case ShutdownPolicy::DrainRingbuffer:
++ case ShutdownPolicy::DiscardRingbuffer:
++ break;
++ default:
++ Q_UNREACHABLE_RETURN();
++ }
++ } else {
++ switch (policy) {
++ case ShutdownPolicy::DrainRingbuffer: {
++ bool writeFailed = false;
++
++ visitRingbuffer([&](auto &ringbuffer) {
++ ringbuffer.consumeAll([&](auto region) {
++ if (writeFailed)
++ return;
++
++ QSpan<const std::byte> writeRegion = as_bytes(region);
++ int status =
++ pa_stream_write(m_stream.get(), writeRegion.data(), writeRegion.size(),
++ /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
++ if (status != 0)
++ writeFailed = true;
++ });
++ });
++
++ break;
++ }
++ case ShutdownPolicy::DiscardRingbuffer: {
++ break;
++ }
++ default:
++ Q_UNREACHABLE_RETURN();
++ }
++ }
++ pa_stream_disconnect(m_stream.get());
++ }
++
++ void QPulseAudioSinkStream::suspend()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ std::ignore = streamCork(m_stream, true);
++ }
++
++ void QPulseAudioSinkStream::resume()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ std::ignore = streamCork(m_stream, false);
++ }
++
++ bool QPulseAudioSinkStream::open() const
++ {
++ return m_stream.isValid();
++ }
++
++ void QPulseAudioSinkStream::installCallbacks(StreamType streamType)
++ {
++ switch (streamType) {
++ case StreamType::Ringbuffer:
++ pa_stream_set_write_callback(m_stream.get(),
++ [](pa_stream *stream, size_t nbytes, void *data) {
++ auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
++ Q_ASSERT(stream == self->m_stream.get());
++ self->writeCallbackRingbuffer(nbytes);
++ }, this);
++ break;
++ case StreamType::Callback:
++ pa_stream_set_write_callback(m_stream.get(),
++ [](pa_stream *stream, size_t nbytes, void *data) {
++ auto *self = reinterpret_cast<QPulseAudioSinkStream *>(data);
++ Q_ASSERT(stream == self->m_stream.get());
++ self->writeCallbackAudioCallback(nbytes);
++ }, this);
++ break;
++
++ default:
++ Q_UNREACHABLE_RETURN();
++ }
++ }
++
++ void QPulseAudioSinkStream::uninstallCallbacks()
++ {
++ pa_stream_set_write_callback(m_stream.get(), nullptr, nullptr);
++ }
++
++ bool QPulseAudioSinkStream::startStream(StreamType streamType)
++ {
++ pa_buffer_attr attr{
++ .maxlength = uint32_t(m_format.bytesForFrames(
++ m_nativePeriodFrames ? qToUnderlying(*m_nativePeriodFrames) : 1024)),
++ .tlength = uint32_t(-1),
++ .prebuf = uint32_t(-1),
++ .minreq = uint32_t(-1),
++ .fragsize = uint32_t(-1),
++ };
++
++ constexpr pa_stream_flags flags =
++ pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
++
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::unique_lock engineLock{ *pulseEngine };
++ installCallbacks(streamType);
++
++ struct state_t
++ {
++ pa_stream_state state{};
++ QSemaphore sem;
++ };
++
++ state_t state;
++
++ pa_stream_set_state_callback(m_stream.get(), [](pa_stream *stream, void *data) {
++ auto *state = reinterpret_cast<state_t *>(data);
++ switch (pa_stream_get_state(stream)) {
++ case PA_STREAM_READY:
++ case PA_STREAM_FAILED:
++ case PA_STREAM_TERMINATED:
++ state->state = pa_stream_get_state(stream);
++ state->sem.release();
++ break;
++ default:
++ break;
++ }
++ }, &state);
++
++ const auto id = m_audioDevice.id();
++ int status =
++ pa_stream_connect_playback(m_stream.get(), id.data(), &attr, flags, nullptr, nullptr);
++
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!";
++ m_stream = {};
++ return false;
++ }
++ engineLock.unlock();
++ state.sem.acquire();
++ if (state.state != PA_STREAM_READY) {
++ qCWarning(qLcPulseAudioOut) << "Stream failed to connect:" << state.state;
++ m_stream = {};
++ return false;
++ }
++
++ engineLock.lock();
++ pa_stream_set_state_callback(m_stream.get(), nullptr, nullptr);
++
++ return true;
++ }
++
++ void QPulseAudioSinkStream::updateStreamIdle(bool idle)
++ {
++ m_parent->updateStreamIdle(idle);
++ }
++
++ void QPulseAudioSinkStream::writeCallbackRingbuffer(size_t requestedBytes)
++ {
++ // ensure round down to number of requested frames
++ uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
++ size_t nbytes = m_format.bytesForFrames(requestedFrames);
++
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ Q_ASSERT(pulseEngine->isInMainLoop());
++
++ void *dest = nullptr;
++
++ int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut)
++ << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
++
++ QMetaObject::invokeMethod(m_parent, [this] {
++ handleIOError(m_parent);
++ });
++ }
++ QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
++
++ const uint64_t consumedFrames = process(hostBuffer, requestedFrames);
++ if (consumedFrames != requestedFrames) {
++ auto remainder = drop(hostBuffer, m_format.bytesForFrames(consumedFrames));
++ QAudioHelperInternal::fillSilence(remainder, m_format);
++ }
++ status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
++ /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut)
++ << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
++
++ QMetaObject::invokeMethod(m_parent, [this] {
++ handleIOError(m_parent);
++ });
++ }
++ }
++
++ void QPulseAudioSinkStream::writeCallbackAudioCallback(size_t requestedBytes)
++ {
++ // ensure round down to number of requested frames
++ uint32_t requestedFrames = m_format.framesForBytes(requestedBytes);
++ size_t nbytes = m_format.bytesForFrames(requestedFrames);
++
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ Q_ASSERT(pulseEngine->isInMainLoop());
++
++ void *dest = nullptr;
++
++ int status = pa_stream_begin_write(m_stream.get(), &dest, &nbytes);
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut)
++ << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
++
++ invokeOnAppThread([this] {
++ handleIOError(m_parent);
++ });
++ }
++ QSpan<std::byte> hostBuffer{ reinterpret_cast<std::byte *>(dest), qsizetype(nbytes) };
++ runAudioCallback(*m_audioCallback, hostBuffer, m_format, volume());
++
++ status = pa_stream_write(m_stream.get(), hostBuffer.data(), nbytes,
++ /*free_cb= */ nullptr, /*offset=*/0, PA_SEEK_RELATIVE);
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut)
++ << "pa_stream_begin_write error:" << currentError(pulseEngine->context());
++
++ invokeOnAppThread([this] {
++ handleIOError(m_parent);
++ });
++ }
++ }
++
++ QPulseAudioSink::QPulseAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
++ : BaseClass(std::move(device), format, parent)
++ {
++ }
++
++ QPulseAudioSink::~QPulseAudioSink() = default;
++
++ bool QPulseAudioSink::validatePulseaudio()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ if (!pulseEngine->contextIsGood()) {
++ qWarning() << "Invalid PulseAudio context:" << pulseEngine->getContextState();
++ setError(QtAudio::Error::FatalError);
++ return false;
++ }
++ return true;
++ }
++
++ void QPulseAudioSink::start(QIODevice *device)
++ {
++ if (!validatePulseaudio())
++ return;
++ return BaseClass::start(device);
++ }
++
++ void QPulseAudioSink::start(AudioCallback &&callback)
++ {
++ if (!validatePulseaudio())
++ return;
++ return BaseClass::start(std::forward<AudioCallback>(callback));
++ }
++
++ QIODevice *QPulseAudioSink::start()
++ {
++ if (!validatePulseaudio())
++ return nullptr;
++ return BaseClass::start();
++ }
++
++ } // namespace QPulseAudioInternal
++
++ QT_END_NAMESPACE