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 "qpulseaudiosource_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/qsemaphore.h>
++
++ #include <mutex> // for std::lock_guard
++ #include <unistd.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QPulseAudioInternal {
++
++ using namespace QtMultimediaPrivate;
++
++ QPulseAudioSourceStream::QPulseAudioSourceStream(QAudioDevice device, const QAudioFormat &format,
++ std::optional<qsizetype> ringbufferSize,
++ QPulseAudioSource *parent,
++ float volume,
++ std::optional<NativePeriodFrames> nativePeriodFrames)
++ : QPlatformAudioSourceStream{
++ 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 (!pa_sample_spec_valid(&spec))
++ return;
++
++ const QByteArray streamName =
++ QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8();
++
++ if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) {
++ qCDebug(qLcPulseAudioIn) << "Format: " << spec.format;
++ qCDebug(qLcPulseAudioIn) << "Rate: " << spec.rate;
++ qCDebug(qLcPulseAudioIn) << "Channels: " << spec.channels;
++ qCDebug(qLcPulseAudioIn) << "Frame size: " << pa_frame_size(&spec);
++ }
++
++ std::lock_guard engineLock{ *pulseEngine };
++
++ m_stream = PAStreamHandle{
++ pa_stream_new(pulseEngine->context(), streamName.constData(), &spec, &channel_map),
++ PAStreamHandle::HasRef,
++ };
++ }
++
++ QPulseAudioSourceStream::~QPulseAudioSourceStream() = default;
++
++ bool QPulseAudioSourceStream::start(QIODevice *device)
++ {
++ setQIODevice(device);
++
++ createQIODeviceConnections(device);
++
++ return startStream(StreamType::Ringbuffer);
++ }
++
++ bool QPulseAudioSourceStream::start(AudioCallback &&audioCallback)
++ {
++ m_audioCallback = std::move(audioCallback);
++ return startStream(StreamType::Callback);
++ }
++
++ QIODevice *QPulseAudioSourceStream::start()
++ {
++ QIODevice *device = createRingbufferReaderDevice();
++ bool started = start(device);
++ if (!started)
++ return nullptr;
++
++ return device;
++ }
++
++ void QPulseAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
++ {
++ requestStop();
++
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ uninstallCallbacks();
++ disconnectQIODeviceConnections();
++
++ if (shutdownPolicy == ShutdownPolicy::DrainRingbuffer) {
++ size_t bytesToRead = pa_stream_readable_size(m_stream.get());
++ if (bytesToRead != size_t(-1))
++ readCallbackRingbuffer(bytesToRead);
++ }
++
++ // Note: we need to cork the stream before disconnecting to prevent pulseaudio from deadlocking
++ auto op = streamCork(m_stream, true);
++ pulseEngine->waitForAsyncOperation(op);
++
++ pa_stream_disconnect(m_stream.get());
++
++ finalizeQIODevice(shutdownPolicy);
++ if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
++ emptyRingbuffer();
++ }
++
++ void QPulseAudioSourceStream::suspend()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ std::ignore = streamCork(m_stream, true);
++ }
++
++ void QPulseAudioSourceStream::resume()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ std::lock_guard engineLock{ *pulseEngine };
++
++ std::ignore = streamCork(m_stream, false);
++ }
++
++ bool QPulseAudioSourceStream::open() const
++ {
++ return bool(m_stream);
++ }
++
++ void QPulseAudioSourceStream::updateStreamIdle(bool idle)
++ {
++ m_parent->updateStreamIdle(idle);
++ }
++
++ bool QPulseAudioSourceStream::startStream(StreamType streamType)
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ static const bool serverIsPipewire = [&] {
++ return pulseEngine->serverName().contains(u"PulseAudio (on PipeWire");
++ }();
++
++ 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),
++
++ // pulseaudio's vanilla implementation requires us to set a fragment size, otherwise we only
++ // get a single callback every 2-ish seconds.
++ .fragsize = serverIsPipewire
++ ? uint32_t(-1)
++ : uint32_t(m_format.bytesForFrames(
++ m_nativePeriodFrames ? qToUnderlying(*m_nativePeriodFrames)
++ : 1024)),
++ };
++
++ constexpr pa_stream_flags flags =
++ pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
++
++ 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_record(m_stream.get(), id.data(), &attr, flags);
++ if (status != 0) {
++ qCWarning(qLcPulseAudioOut) << "pa_stream_connect_record() 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 QPulseAudioSourceStream::installCallbacks(StreamType streamType)
++ {
++ switch (streamType) {
++ case StreamType::Ringbuffer: {
++ pa_stream_set_read_callback(m_stream.get(),
++ [](pa_stream *stream, size_t nbytes, void *data) {
++ auto *self = reinterpret_cast<QPulseAudioSourceStream *>(data);
++ Q_ASSERT(stream == self->m_stream.get());
++ self->readCallbackRingbuffer(nbytes);
++ }, this);
++ break;
++ }
++ case StreamType::Callback: {
++ pa_stream_set_read_callback(m_stream.get(),
++ [](pa_stream *stream, size_t nbytes, void *data) {
++ auto *self = reinterpret_cast<QPulseAudioSourceStream *>(data);
++ Q_ASSERT(stream == self->m_stream.get());
++ self->readCallbackAudioCallback(nbytes);
++ }, this);
++ break;
++ }
++ }
++ }
++
++ void QPulseAudioSourceStream::uninstallCallbacks()
++ {
++ pa_stream_set_read_callback(m_stream.get(), nullptr, nullptr);
++ }
++
++ void QPulseAudioSourceStream::readCallbackRingbuffer([[maybe_unused]] size_t bytesToRead)
++ {
++ const void *data{};
++ size_t nBytes{};
++ int status = pa_stream_peek(m_stream.get(), &data, &nBytes);
++ if (status < 0) {
++ invokeOnAppThread([this] {
++ handleIOError(m_parent);
++ });
++ return;
++ }
++
++ QSpan<const std::byte> hostBuffer{
++ reinterpret_cast<const std::byte *>(data),
++ qsizetype(nBytes),
++ };
++
++ uint32_t numberOfFrames = m_format.framesForBytes(nBytes);
++
++ [[maybe_unused]] uint64_t framesWritten =
++ QPlatformAudioSourceStream::process(hostBuffer, numberOfFrames);
++ status = pa_stream_drop(m_stream.get());
++ if (status < 0) {
++ if (!isStopRequested()) {
++ invokeOnAppThread([this] {
++ handleIOError(m_parent);
++ });
++ }
++ }
++ }
++
++ void QPulseAudioSourceStream::readCallbackAudioCallback([[maybe_unused]] size_t bytesToRead)
++ {
++ const void *data{};
++ size_t nBytes{};
++ int status = pa_stream_peek(m_stream.get(), &data, &nBytes);
++ if (status < 0) {
++ QMetaObject::invokeMethod(m_parent, [this] {
++ handleIOError(m_parent);
++ });
++ return;
++ }
++
++ QSpan<const std::byte> hostBuffer{
++ reinterpret_cast<const std::byte *>(data),
++ qsizetype(nBytes),
++ };
++
++ runAudioCallback(*m_audioCallback, hostBuffer, m_format, volume());
++
++ status = pa_stream_drop(m_stream.get());
++ if (status < 0) {
++ if (!isStopRequested()) {
++ QMetaObject::invokeMethod(m_parent, [this] {
++ handleIOError(m_parent);
++ });
++ }
++ }
++ }
++
++ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
++
++ QPulseAudioSource::QPulseAudioSource(QAudioDevice device, const QAudioFormat &format,
++ QObject *parent)
++ : BaseClass(std::move(device), format, parent)
++ {
++ }
++
++ QPulseAudioSource::~QPulseAudioSource()
++ = default;
++
++ bool QPulseAudioSource::validatePulseaudio()
++ {
++ QPulseAudioContextManager *pulseEngine = QPulseAudioContextManager::instance();
++ if (!pulseEngine->contextIsGood()) {
++ qWarning() << "Invalid PulseAudio context:" << pulseEngine->getContextState();
++ setError(QtAudio::Error::FatalError);
++ return false;
++ }
++ return true;
++ }
++
++ void QPulseAudioSource::start(QIODevice *device)
++ {
++ if (!validatePulseaudio())
++ return;
++ return BaseClass::start(device);
++ }
++
++ void QPulseAudioSource::start(AudioCallback &&cb)
++ {
++ if (!validatePulseaudio())
++ return;
++ return BaseClass::start(std::move(cb));
++ }
++
++ QIODevice *QPulseAudioSource::start()
++ {
++ if (!validatePulseaudio())
++ return nullptr;
++ return BaseClass::start();
++ }
++
++ } // namespace QPulseAudioInternal
++
++ QT_END_NAMESPACE