Revision control
++ // Copyright (C) 2025 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 "qaudiosystem_platform_stream_support_p.h"
++
++ #include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
++ #include <QtMultimedia/private/qaudiohelpers_p.h>
++ #include <QtCore/qdebug.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtMultimediaPrivate {
++
++ using namespace std::chrono_literals;
++
++ QPlatformAudioIOStream::QPlatformAudioIOStream(QAudioDevice m_audioDevice, QAudioFormat m_format,
++ std::optional<int> ringbufferSize,
++ std::optional<NativePeriodFrames> nativePeriodFrames,
++ float volume)
++ : m_audioDevice{
++ std::move(m_audioDevice),
++ },
++ m_format{
++ m_format,
++ },
++ m_nativePeriodFrames{
++ nativePeriodFrames,
++ },
++ m_volume{
++ volume,
++ }
++ {
++ prepareRingbuffer(ringbufferSize);
++ }
++
++ QPlatformAudioIOStream::~QPlatformAudioIOStream()
++ {
++ Q_ASSERT(m_stopRequested);
++ }
++
++ void QPlatformAudioIOStream::setVolume(float volume)
++ {
++ m_volume.store(volume, std::memory_order_relaxed);
++ }
++
++ void QPlatformAudioIOStream::prepareRingbuffer(std::optional<int> ringbufferSize)
++ {
++ using SampleFormat = QAudioFormat::SampleFormat;
++
++ // Warning: QAudioSink::setBufferSize is measured in *bytes* not in *frames*
++ int ringbufferElements = inferRingbufferFrames(ringbufferSize, m_nativePeriodFrames, m_format)
++ * m_format.channelCount();
++
++ switch (m_format.sampleFormat()) {
++ case SampleFormat::Float:
++ m_ringbuffer.emplace<QAudioRingBuffer<float>>(ringbufferElements);
++ break;
++ case SampleFormat::Int16:
++ m_ringbuffer.emplace<QAudioRingBuffer<int16_t>>(ringbufferElements);
++ break;
++ case SampleFormat::Int32:
++ m_ringbuffer.emplace<QAudioRingBuffer<int32_t>>(ringbufferElements);
++ break;
++ case SampleFormat::UInt8:
++ m_ringbuffer.emplace<QAudioRingBuffer<uint8_t>>(ringbufferElements);
++ break;
++
++ default:
++ qCritical() << "invalid sample format";
++ Q_UNREACHABLE_RETURN();
++ }
++ }
++
++ void QPlatformAudioIOStream::requestStop()
++ {
++ m_stopRequested.store(true, std::memory_order_release);
++ }
++
++ qsizetype
++ QPlatformAudioIOStream::inferRingbufferFrames(const std::optional<int> &ringbufferSize,
++ const std::optional<NativePeriodFrames> &nativePeriodFrames,
++ const QAudioFormat &format)
++ {
++ int bytesPerFrame = format.bytesPerFrame();
++ Q_PRESUME(bytesPerFrame > 0);
++
++ return inferRingbufferBytes(ringbufferSize, nativePeriodFrames, format) / bytesPerFrame;
++ }
++
++ qsizetype
++ QPlatformAudioIOStream::inferRingbufferBytes(const std::optional<int> &ringbufferSize,
++ const std::optional<NativePeriodFrames> &nativePeriodFrames,
++ const QAudioFormat &format)
++ {
++ // ensure to a sane minimum ringbuffer size of twice the hw buffer size or 32 frames
++ const int minimumRingbufferFrames = nativePeriodFrames
++ ? qToUnderlying(*nativePeriodFrames) * 2
++ : 32;
++ const int minimumRingbufferBytes = format.bytesForFrames(minimumRingbufferFrames);
++ if (ringbufferSize)
++ return ringbufferSize >= minimumRingbufferBytes ? *ringbufferSize : minimumRingbufferBytes;
++
++ using namespace std::chrono;
++ static constexpr auto defaultBufferDuration = 250ms;
++
++ return format.bytesForDuration(microseconds(defaultBufferDuration).count());
++ }
++
++ int QPlatformAudioIOStream::ringbufferSizeInBytes()
++ {
++ return visitRingbuffer([](auto &ringbuffer) {
++ using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
++ return ringbuffer.size() * sizeof(SampleType);
++ });
++ }
++
++ ////////////////////////////////////////////////////////////////////////////////////////////////////
++
++ QPlatformAudioSinkStream::QPlatformAudioSinkStream(QAudioDevice audioDevice,
++ const QAudioFormat &format,
++ std::optional<int> ringbufferSize,
++ std::optional<NativePeriodFrames> nativePeriodFrames,
++ float volume)
++ : QPlatformAudioIOStream{
++ std::move(audioDevice), format, ringbufferSize, nativePeriodFrames, volume,
++ }
++ {
++ m_streamIdleDetectionConnection = m_streamIdleDetectionNotifier.callOnActivated([this] {
++ if (isStopRequested())
++ return;
++
++ bool sinkIsIdle = m_streamIsIdle.load();
++
++ if (sinkIsIdle) {
++ // data has been pushed to the ringbuffer, while the stream is
++ // still idle, this will change during the next audio callback
++ bool ringbufferIsEmpty = visitRingbuffer([&](auto &ringbuffer) {
++ return ringbuffer.free() == ringbuffer.size();
++ });
++
++ sinkIsIdle = ringbufferIsEmpty;
++ }
++
++ updateStreamIdle(sinkIsIdle);
++ });
++ }
++
++ QPlatformAudioSinkStream::~QPlatformAudioSinkStream() = default;
++
++ uint64_t
++ QPlatformAudioSinkStream::process(QSpan<std::byte> hostBuffer, qsizetype totalNumberOfFrames,
++ std::optional<NativeSampleFormat> nativeFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ qsizetype totalNumberOfSamples = totalNumberOfFrames * m_format.channelCount();
++
++ const float vol = volume();
++
++ int samplesConsumedFromRingbuffer = visitRingbuffer([&](auto &ringbuffer) {
++ return ringbuffer.consume(totalNumberOfSamples, [&](auto ringbufferRange) {
++ if (nativeFormat) {
++ // Amount of bytes in output range differ from ringbuffer range
++ const qsizetype samplesInChunk = ringbufferRange.size();
++ const qsizetype bytesInChunk = samplesInChunk * bytesPerSample(*nativeFormat);
++
++ QSpan<std::byte> outputByteRange = take(hostBuffer, bytesInChunk);
++ hostBuffer = drop(hostBuffer, bytesInChunk);
++ convertToNative(as_bytes(ringbufferRange), outputByteRange, vol, *nativeFormat);
++ } else {
++ QSpan<std::byte> outputByteRange = take(hostBuffer, ringbufferRange.size_bytes());
++ hostBuffer = drop(hostBuffer, ringbufferRange.size_bytes());
++ QAudioHelperInternal::applyVolume(vol, m_format, as_bytes(ringbufferRange),
++ outputByteRange);
++ }
++ });
++ });
++
++ if (m_ringbufferWriterDevice) {
++ qint64 bytes = samplesConsumedFromRingbuffer * m_format.bytesPerSample();
++ m_ringbufferWriterDevice->bytesConsumedFromRingbuffer(bytes);
++ }
++
++ if (!isStopRequested()) {
++ if (notificationThresholdBytes == 0 || bytesFree() > notificationThresholdBytes)
++ m_ringbufferHasSpace.set();
++
++ bool streamIsIdle = m_streamIsIdle.load(std::memory_order_relaxed);
++ if (streamIsIdle && samplesConsumedFromRingbuffer) {
++ m_streamIsIdle.store(false);
++ m_streamIdleDetectionNotifier.set();
++ } else if (!streamIsIdle && !samplesConsumedFromRingbuffer) {
++ m_streamIsIdle.store(true);
++ m_streamIdleDetectionNotifier.set();
++ }
++ }
++ if (!hostBuffer.empty()) {
++ if (nativeFormat)
++ QAudioHelperInternal::fillSilence(hostBuffer, *nativeFormat);
++ else
++ QAudioHelperInternal::fillSilence(hostBuffer, m_format);
++ }
++
++ uint64_t consumedFrames = samplesConsumedFromRingbuffer / m_format.channelCount();
++ m_processedFrameCount += consumedFrames;
++ m_totalFrameCount += totalNumberOfFrames;
++
++ return consumedFrames;
++ }
++
++ quint64 QPlatformAudioSinkStream::bytesFree() const
++ {
++ return visitRingbuffer([](auto &ringbuffer) {
++ using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
++ return ringbuffer.free() * sizeof(SampleType);
++ });
++ }
++
++ std::chrono::microseconds QPlatformAudioSinkStream::processedDuration() const
++ {
++ return std::chrono::microseconds{
++ m_processedFrameCount * 1'000'000 / m_format.sampleRate(),
++ };
++ }
++
++ void QPlatformAudioSinkStream::pullFromQIODevice()
++ {
++ withPullIODeviceReentrancyGuard([this] {
++ pullFromQIODeviceImpl();
++ });
++ }
++
++ void QPlatformAudioSinkStream::pullFromQIODeviceImpl()
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ Q_ASSERT(m_device);
++ Q_ASSERT(m_pullIODeviceReentrancyGuard);
++
++ visitRingbuffer([&](auto &ringbuffer) {
++ int elementsPulled = pullFromQIODeviceToRingbuffer(*m_device, ringbuffer);
++ if (elementsPulled)
++ updateStreamIdle(false);
++ });
++ }
++
++ void QPlatformAudioSinkStream::createQIODeviceConnections(QIODevice *device)
++ {
++ // consumed from audio thread
++ m_ringbufferHasSpaceConnection = m_ringbufferHasSpace.callOnActivated(device, [this] {
++ pullFromQIODevice();
++ });
++
++ // data has been pushed to device
++ m_iodeviceHasNewDataConnection =
++ QObject::connect(device, &QIODevice::readyRead, device, [this] {
++ withPullIODeviceReentrancyGuard([this] {
++ pullFromQIODeviceImpl();
++ updateStreamIdle(false);
++ });
++ });
++ }
++
++ void QPlatformAudioSinkStream::disconnectQIODeviceConnections()
++ {
++ QObject::disconnect(m_ringbufferHasSpaceConnection);
++ QObject::disconnect(m_iodeviceHasNewDataConnection);
++ }
++
++ QIODevice *QPlatformAudioSinkStream::createRingbufferWriterDevice()
++ {
++ m_ringbufferWriterDevice = visitRingbuffer(
++ [&](auto &ringbuffer) -> std::unique_ptr<QtPrivate::QIODeviceRingBufferWriterBase> {
++ using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
++ return std::make_unique<QtPrivate::QIODeviceRingBufferWriter<SampleType>>(&ringbuffer);
++ });
++
++ return m_ringbufferWriterDevice.get();
++ }
++
++ void QPlatformAudioSinkStream::setQIODevice(QIODevice *device)
++ {
++ m_device = device;
++ }
++
++ void QPlatformAudioSinkStream::setIdleState(bool x)
++ {
++ m_streamIsIdle.store(x);
++ }
++
++ void QPlatformAudioSinkStream::stopIdleDetection()
++ {
++ QObject::disconnect(m_streamIdleDetectionConnection);
++ }
++
++ QThread *QPlatformAudioSinkStream::thread() const
++ {
++ // QPlatformAudioSinkStream is not a QObject, but still has a notion of an application thread
++ // where it lives on.
++ return m_streamIdleDetectionNotifier.thread();
++ }
++
++ // we limit alloca calls to 0.5MB. it's good enough for virtually all use cases (i.e. buffers
++ // of 4092 frames / 32 channels) and well in the reasonable range of available stack memory on linux
++ // (8MB)
++ static constexpr qsizetype scratchpadBufferSizeLimit = 512 * 1024;
++ static_assert(scratchpadBufferSizeLimit > 4092 * 32 * sizeof(float));
++
++ void QPlatformAudioSinkStream::convertToNative(QSpan<const std::byte> internal,
++ QSpan<std::byte> native, float volume,
++ NativeSampleFormat nativeFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ using namespace QAudioHelperInternal;
++
++ if (volume == 1.f) {
++ convertSampleFormat(internal, toNativeSampleFormat(m_format.sampleFormat()), native,
++ nativeFormat);
++ return;
++ }
++
++ withTemporaryBuffer<scratchpadBufferSizeLimit>(internal.size(),
++ [&](QSpan<std::byte> scratchpadBuffer) {
++ applyVolume(volume, m_format, internal, scratchpadBuffer);
++ convertSampleFormat(scratchpadBuffer, toNativeSampleFormat(m_format.sampleFormat()), native,
++ nativeFormat);
++ });
++ }
++
++ ////////////////////////////////////////////////////////////////////////////////////////////////////
++
++ QPlatformAudioSourceStream::QPlatformAudioSourceStream(QAudioDevice audioDevice,
++ const QAudioFormat &format,
++ std::optional<int> ringbufferSize,
++ std::optional<NativePeriodFrames> nativePeriodFrames,
++ float volume)
++ : QPlatformAudioIOStream{
++ std::move(audioDevice), format, ringbufferSize, nativePeriodFrames, volume,
++ }
++ {
++ }
++
++ QPlatformAudioSourceStream::~QPlatformAudioSourceStream() = default;
++
++ uint64_t QPlatformAudioSourceStream::process(
++ QSpan<const std::byte> hostBuffer, qsizetype numberOfFrames,
++ std::optional<NativeSampleFormat> nativeFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ qsizetype remainingNumberOfSamples = numberOfFrames * m_format.channelCount();
++
++ const float vol = volume();
++ using namespace QtMultimediaPrivate;
++
++ uint64_t totalSamplesWritten = visitRingbuffer([&](auto &rb) {
++ using SampleType = typename std::decay_t<decltype(rb)>::ValueType;
++
++ // clang-format off
++ return rb.produceSome([&](QSpan<SampleType> ringbufferRange) {
++ if (nativeFormat) {
++ // Amount of bytes in input range differ from ringbuffer range
++ const qsizetype samplesInChunk = ringbufferRange.size();
++ const qsizetype bytesInChunk = samplesInChunk * bytesPerSample(*nativeFormat);
++
++ QSpan<const std::byte> inputByteRange = take(hostBuffer, bytesInChunk);
++ hostBuffer = drop(hostBuffer, bytesInChunk);
++ convertFromNative(inputByteRange, as_writable_bytes(ringbufferRange), vol,
++ *nativeFormat);
++ } else {
++ QSpan<const std::byte> inputByteRange =
++ take(hostBuffer, ringbufferRange.size_bytes());
++ hostBuffer = drop(hostBuffer, ringbufferRange.size_bytes());
++ QAudioHelperInternal::applyVolume(vol, m_format, inputByteRange,
++ as_writable_bytes(ringbufferRange));
++ }
++ return ringbufferRange;
++ }, remainingNumberOfSamples);
++ // clang-format on
++ });
++
++ if (totalSamplesWritten)
++ m_ringbufferHasData.set();
++
++ uint64_t framesWritten = totalSamplesWritten / m_format.channelCount();
++ m_totalNumberOfFramesPushedToRingbuffer += framesWritten;
++ return framesWritten;
++ }
++
++ void QPlatformAudioSourceStream::pushToIODevice()
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++
++ qsizetype bytesPushed = visitRingbuffer([&](auto &ringbuffer) {
++ return QtPrivate::pushToQIODeviceFromRingbuffer(*m_device, ringbuffer);
++ });
++
++ if (bytesPushed)
++ Q_EMIT m_device->readyRead();
++ }
++
++ bool QPlatformAudioSourceStream::deviceIsRingbufferReader() const
++ {
++ return m_device == m_ringbufferReaderDevice.get();
++ }
++
++ void QPlatformAudioSourceStream::finalizeQIODevice(ShutdownPolicy shutdownPolicy)
++ {
++ switch (shutdownPolicy) {
++ case ShutdownPolicy::DiscardRingbuffer:
++ return;
++ case ShutdownPolicy::DrainRingbuffer:
++ if (!deviceIsRingbufferReader())
++ pushToIODevice();
++ return;
++
++ default:
++ Q_UNREACHABLE_RETURN();
++ }
++ }
++
++ void QPlatformAudioSourceStream::emptyRingbuffer()
++ {
++ visitRingbuffer([](auto &ringbuffer) {
++ ringbuffer.consumeAll([](auto &) {
++ });
++ });
++ }
++
++ QThread *QPlatformAudioSourceStream::thread() const
++ {
++ // QPlatformAudioSourceStream is not a QObject, but still has a notion of an application thread
++ // where it lives on.
++ return m_ringbufferHasData.thread();
++ }
++
++ qsizetype QPlatformAudioSourceStream::bytesReady() const
++ {
++ return visitRingbuffer([](const auto &ringbuffer) {
++ return ringbuffer.used() * sizeof(typename std::decay_t<decltype(ringbuffer)>::ValueType);
++ });
++ }
++
++ std::chrono::microseconds QPlatformAudioSourceStream::processedDuration() const
++ {
++ return std::chrono::microseconds{
++ m_format.durationForFrames(
++ qint32(m_totalNumberOfFramesPushedToRingbuffer.load(std::memory_order_relaxed))),
++ };
++ }
++
++ void QPlatformAudioSourceStream::setQIODevice(QIODevice *device)
++ {
++ m_device = device;
++ }
++
++ void QPlatformAudioSourceStream::createQIODeviceConnections(QIODevice *device)
++ {
++ bool pushToDevice = !deviceIsRingbufferReader();
++
++ if (pushToDevice) {
++ m_ringbufferHasDataConnection = m_ringbufferHasData.callOnActivated(device, [this] {
++ if (!isStopRequested())
++ updateStreamIdle(false);
++ pushToIODevice();
++ });
++ } else {
++ m_ringbufferHasDataConnection = m_ringbufferHasData.callOnActivated(device, [this] {
++ if (!isStopRequested())
++ updateStreamIdle(false);
++ Q_EMIT m_device->readyRead();
++ });
++ }
++
++ m_ringbufferIsFullConnection = m_ringbufferHasData.callOnActivated(device, [this] {
++ if (!isStopRequested())
++ updateStreamIdle(false);
++ });
++ }
++
++ void QPlatformAudioSourceStream::disconnectQIODeviceConnections()
++ {
++ QObject::disconnect(m_ringbufferHasDataConnection);
++ QObject::disconnect(m_ringbufferIsFullConnection);
++ }
++
++ QIODevice *QPlatformAudioSourceStream::createRingbufferReaderDevice()
++ {
++ using namespace QtPrivate;
++
++ m_ringbufferReaderDevice = visitRingbuffer([&](auto &rb) -> std::unique_ptr<QIODevice> {
++ using SampleType = typename std::decay_t<decltype(rb)>::ValueType;
++ return std::make_unique<QIODeviceRingBufferReader<SampleType>>(&rb);
++ });
++
++ m_ringbufferReaderDevice->open(QIODevice::ReadOnly | QIODevice::Unbuffered);
++
++ return m_ringbufferReaderDevice.get();
++ }
++
++ void QPlatformAudioSourceStream::convertFromNative(
++ QSpan<const std::byte> native, QSpan<std::byte> internal, float volume,
++ NativeSampleFormat nativeFormat) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ using namespace QAudioHelperInternal;
++ if (volume == 1.f) {
++ convertSampleFormat(native, nativeFormat, internal,
++ QAudioHelperInternal::toNativeSampleFormat(m_format.sampleFormat()));
++ return;
++ }
++
++ withTemporaryBuffer<scratchpadBufferSizeLimit>(internal.size(),
++ [&](QSpan<std::byte> scratchpadBuffer) {
++ convertSampleFormat(native, nativeFormat, scratchpadBuffer,
++ QAudioHelperInternal::toNativeSampleFormat(m_format.sampleFormat()));
++ applyVolume(volume, m_format, scratchpadBuffer, internal);
++ });
++ }
++
++ } // namespace QtMultimediaPrivate
++
++ QT_END_NAMESPACE
++
++ #ifdef Q_CC_MSVC
++ # undef alloca
++ #endif