Revision control

++ // Copyright (C) 2021 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 "qffmpegaudioinput_p.h"
++
++ #include <QtCore/qatomic.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qiodevice.h>
++ #include <QtCore/qmetaobject.h>
++ #include <QtMultimedia/qaudiobuffer.h>
++ #include <QtMultimedia/qaudiosource.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QFFmpeg {
++
++ class AudioSourceIO : public QIODevice
++ {
++ Q_OBJECT
++ public:
++ AudioSourceIO(QFFmpegAudioInput *audioInput) : m_input(audioInput)
++ {
++ m_muted = m_input->muted;
++ m_volume = m_input->volume;
++ updateVolume();
++ open(QIODevice::WriteOnly);
++ }
++
++ ~AudioSourceIO() override
++ {
++ // QAudioSource may invoke QIODevice::writeData in the destructor.
++ // Let's reset the audio source to get around the case.
++ if (m_audioSource)
++ m_audioSource->reset();
++ }
++
++ void setDevice(const QAudioDevice &device)
++ {
++ Q_ASSERT(!thread()->isCurrentThread());
++ QMutexLocker locker(&m_mutex);
++ if (m_device == device)
++ return;
++ m_device = device;
++ QMetaObject::invokeMethod(this, [this] {
++ QMutexLocker locker(&m_mutex);
++ updateSource(locker);
++ });
++ }
++ void setBufferSize(int bufferSize)
++ {
++ m_bufferSize.storeRelease((bufferSize > 0 && m_format.isValid())
++ ? m_format.bytesForFrames(bufferSize)
++ : DefaultAudioInputBufferSize);
++ }
++ void setRunning(bool r) {
++ Q_ASSERT(!thread()->isCurrentThread());
++ QMutexLocker locker(&m_mutex);
++ if (m_running == r)
++ return;
++ m_running = r;
++ QMetaObject::invokeMethod(this, &AudioSourceIO::updateRunning);
++ }
++
++ void setVolume(float vol) {
++ Q_ASSERT(!thread()->isCurrentThread());
++ QMutexLocker locker(&m_mutex);
++ m_volume = vol;
++ QMetaObject::invokeMethod(this, &AudioSourceIO::updateVolume);
++ }
++ void setMuted(bool muted) {
++ Q_ASSERT(!thread()->isCurrentThread());
++ QMutexLocker locker(&m_mutex);
++ m_muted = muted;
++ QMetaObject::invokeMethod(this, &AudioSourceIO::updateVolume);
++ }
++
++ int bufferSize() const { return m_bufferSize.loadAcquire(); }
++
++ protected:
++ qint64 readData(char *, qint64) override
++ {
++ return 0;
++ }
++ qint64 writeData(const char *data, qint64 len) override
++ {
++ Q_ASSERT(m_audioSource);
++
++ int l = len;
++ while (len > 0) {
++ const auto bufferSize = m_bufferSize.loadAcquire();
++
++ while (m_pcm.size() > bufferSize) {
++ // bufferSize has been reduced. Send data until m_pcm
++ // can hold more data.
++ sendBuffer(m_pcm.first(bufferSize));
++ m_pcm.remove(0, bufferSize);
++ }
++
++ // Size of m_pcm is always <= bufferSize
++ int toAppend = qMin(len, bufferSize - m_pcm.size());
++ m_pcm.append(data, toAppend);
++ data += toAppend;
++ len -= toAppend;
++ if (m_pcm.size() == bufferSize) {
++ sendBuffer(m_pcm);
++ m_pcm.clear();
++ }
++ }
++
++ return l;
++ }
++
++ private Q_SLOTS:
++ void updateVolume()
++ {
++ if (m_audioSource)
++ m_audioSource->setVolume(m_muted ? 0. : m_volume);
++ }
++ void updateRunning()
++ {
++ QMutexLocker locker(&m_mutex);
++ if (m_running) {
++ if (!m_audioSource)
++ updateSource(locker);
++ else
++ m_audioSource->start(this);
++ } else {
++ m_audioSource->stop();
++ }
++ }
++
++ private:
++ void updateSource(const QMutexLocker<QMutex> &guard)
++ {
++ Q_ASSERT(guard.mutex() == &m_mutex);
++ m_format = m_device.preferredFormat();
++ if (std::exchange(m_audioSource, nullptr))
++ m_pcm.clear();
++
++ m_audioSource = std::make_unique<QAudioSource>(m_device, m_format);
++ updateVolume();
++ if (m_running)
++ m_audioSource->start(this);
++ }
++
++ void sendBuffer(const QByteArray &pcmData)
++ {
++ QAudioFormat fmt = m_audioSource->format();
++ qint64 time = fmt.durationForBytes(m_processed);
++ QAudioBuffer buffer(pcmData, fmt, time);
++ Q_EMIT m_input->newAudioBuffer(buffer);
++ m_processed += pcmData.size();
++ }
++
++ QMutex m_mutex;
++ QAudioDevice m_device;
++ float m_volume = 1.;
++ bool m_muted = false;
++ bool m_running = false;
++
++ QFFmpegAudioInput *m_input = nullptr;
++ std::unique_ptr<QAudioSource> m_audioSource;
++ QAudioFormat m_format;
++ QAtomicInt m_bufferSize = DefaultAudioInputBufferSize;
++ qint64 m_processed = 0;
++ QByteArray m_pcm;
++ };
++
++ } // namespace QFFmpeg
++
++ QFFmpegAudioInput::QFFmpegAudioInput(QAudioInput *qq)
++ : QPlatformAudioInput(qq)
++ {
++ qRegisterMetaType<QAudioBuffer>();
++
++ m_inputThread = std::make_unique<QThread>();
++ m_inputThread->setObjectName(QStringLiteral("QFFmpegAudioInputThread"));
++ m_audioIO = new QFFmpeg::AudioSourceIO(this);
++ m_audioIO->moveToThread(m_inputThread.get());
++ m_inputThread->start();
++ }
++
++ QFFmpegAudioInput::~QFFmpegAudioInput()
++ {
++ // Ensure that COM is uninitialized by nested QWindowsResampler
++ // on the same thread that initialized it.
++ m_audioIO->deleteLater();
++ m_inputThread->exit();
++ m_inputThread->wait();
++ }
++
++ void QFFmpegAudioInput::setAudioDevice(const QAudioDevice &device)
++ {
++ m_audioIO->setDevice(device);
++ }
++
++ void QFFmpegAudioInput::setMuted(bool muted)
++ {
++ m_audioIO->setMuted(muted);
++ }
++
++ void QFFmpegAudioInput::setVolume(float volume)
++ {
++ m_audioIO->setVolume(volume);
++ }
++
++ void QFFmpegAudioInput::setBufferSize(int bufferSize)
++ {
++ m_audioIO->setBufferSize(bufferSize);
++ }
++
++ int QFFmpegAudioInput::bufferSize() const
++ {
++ return m_audioIO->bufferSize();
++ }
++
++ void QFFmpegAudioInput::connectNotify(const QMetaMethod &signal)
++ {
++ // threading considerations:
++ // AudioSourceIO::setRunning doesn't reenter
++ // the internal QObject's mutex of the audio input instance
++ if (signal == QMetaMethod::fromSignal(&QFFmpegAudioInput::newAudioBuffer))
++ m_audioIO->setRunning(true);
++ }
++
++ void QFFmpegAudioInput::disconnectNotify(const QMetaMethod &signal)
++ {
++ if (!signal.isValid()
++ || signal == QMetaMethod::fromSignal(&QFFmpegAudioInput::newAudioBuffer)) {
++ auto stopIOifNeeded = [this]() {
++ // if the signal disconnectNotify is not
++ if (!isSignalConnected(QMetaMethod::fromSignal(&QFFmpegAudioInput::newAudioBuffer)))
++ m_audioIO->setRunning(false);
++ };
++
++ // threading considerations:
++ // QMetaObject::invokeMethod doesn't reenter
++ // the internal QObject's mutex of the audio input instance.
++ // Instead, QMetaObject::invokeMethod locks ThreadData::postEventMutex
++
++ // postpone update to avoid redundant QAudioSource restarts upon reconnection
++ QMetaObject::invokeMethod(this, stopIOifNeeded, Qt::QueuedConnection);
++ }
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qffmpegaudioinput_p.cpp"
++
++ #include "qffmpegaudioinput.moc"