diff options
Diffstat (limited to 'src/multimedia/wasm')
-rw-r--r-- | src/multimedia/wasm/qwasmaudiodevice.cpp | 56 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmaudiodevice_p.h | 31 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmaudiosink.cpp | 464 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmaudiosink_p.h | 89 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmaudiosource.cpp | 315 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmaudiosource_p.h | 75 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmmediadevices.cpp | 276 | ||||
-rw-r--r-- | src/multimedia/wasm/qwasmmediadevices_p.h | 89 |
8 files changed, 1395 insertions, 0 deletions
diff --git a/src/multimedia/wasm/qwasmaudiodevice.cpp b/src/multimedia/wasm/qwasmaudiodevice.cpp new file mode 100644 index 000000000..c87a0ad54 --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiodevice.cpp @@ -0,0 +1,56 @@ +// 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 "qwasmaudiodevice_p.h" +#include <emscripten.h> +#include <emscripten/val.h> +#include <emscripten/bind.h> + +#include <AL/al.h> +#include <AL/alc.h> + +QT_BEGIN_NAMESPACE + +QWasmAudioDevice::QWasmAudioDevice(const char *device, const char *desc, bool isDef, QAudioDevice::Mode mode) + : QAudioDevicePrivate(device, mode) +{ + description = QString::fromUtf8(desc); + + isDefault = isDef; + minimumChannelCount = 1; + maximumChannelCount = 2; + minimumSampleRate = 8000; + maximumSampleRate = 96000; // js AudioContext max according to docs + + // native openAL formats + supportedSampleFormats.append(QAudioFormat::UInt8); + supportedSampleFormats.append(QAudioFormat::Int16); + + // Browsers use 32bit floats as native, but emscripten reccomends checking for the exension. + if (alIsExtensionPresent("AL_EXT_float32")) + supportedSampleFormats.append(QAudioFormat::Float); + + preferredFormat.setChannelCount(2); + + // FIXME: firefox + // An AudioContext was prevented from starting automatically. + // It must be created or resumed after a user gesture on the page. + emscripten::val audioContext = emscripten::val::global("window")["AudioContext"].new_(); + if (audioContext == emscripten::val::undefined()) + audioContext = emscripten::val::global("window")["webkitAudioContext"].new_(); + + if (audioContext != emscripten::val::undefined()) { + audioContext.call<void>("resume"); + int sRate = audioContext["sampleRate"].as<int>(); + audioContext.call<void>("close"); + preferredFormat.setSampleRate(sRate); + } + + auto f = QAudioFormat::Float; + + if (!supportedSampleFormats.contains(f)) + f = QAudioFormat::Int16; + preferredFormat.setSampleFormat(f); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/wasm/qwasmaudiodevice_p.h b/src/multimedia/wasm/qwasmaudiodevice_p.h new file mode 100644 index 000000000..cc86c1575 --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiodevice_p.h @@ -0,0 +1,31 @@ +// 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 + +#ifndef QWASMAUDIODEVICEINFO_H +#define QWASMAUDIODEVICEINFO_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "private/qaudiodevice_p.h" + +QT_BEGIN_NAMESPACE + +class QWasmAudioDevice : public QAudioDevicePrivate +{ +public: + QWasmAudioDevice(const char *device, const char *description, bool isDefault, QAudioDevice::Mode mode); + +}; + +QT_END_NAMESPACE + +#endif // QWASMAUDIODEVICEINFO_H diff --git a/src/multimedia/wasm/qwasmaudiosink.cpp b/src/multimedia/wasm/qwasmaudiosink.cpp new file mode 100644 index 000000000..d1068e744 --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiosink.cpp @@ -0,0 +1,464 @@ +// 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 "qwasmaudiosink_p.h" + + +#include <emscripten.h> +#include <AL/al.h> +#include <AL/alc.h> +#include <QDebug> +#include <QtMath> +#include <QIODevice> + +// non native al formats (AL_EXT_float32) +#define AL_FORMAT_MONO_FLOAT32 0x10010 +#define AL_FORMAT_STEREO_FLOAT32 0x10011 + +constexpr unsigned int DEFAULT_BUFFER_DURATION = 50'000; + +class ALData { +public: + ALCcontext *context = nullptr; + ALCdevice *device = nullptr; + ALuint source; + ALuint *buffers = nullptr; + ALuint *buffer = nullptr; + ALenum format; +}; + +QT_BEGIN_NAMESPACE + +class QWasmAudioSinkDevice : public QIODevice { + + QWasmAudioSink *m_out; + +public: + QWasmAudioSinkDevice(QWasmAudioSink *parent); + +protected: + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; +}; + +QWasmAudioSink::QWasmAudioSink(const QByteArray &device, QObject *parent) + : QPlatformAudioSink(parent), + m_name(device), + m_timer(new QTimer(this)) +{ + m_timer->setSingleShot(false); + aldata = new ALData(); + connect(m_timer, &QTimer::timeout, this, [this](){ + if (m_pullMode) + nextALBuffers(); + else { + unloadALBuffers(); + m_device->write(nullptr, 0); + updateState(); + } + }); +} + +QWasmAudioSink::~QWasmAudioSink() +{ + delete aldata; + if (m_tmpData) + delete[] m_tmpData; +} + +void QWasmAudioSink::start(QIODevice *device) +{ + Q_ASSERT(device); + Q_ASSERT(device->openMode().testFlag(QIODevice::ReadOnly)); + m_device = device; + start(true); +} + +QIODevice *QWasmAudioSink::start() +{ + m_device = new QWasmAudioSinkDevice(this); + m_device->open(QIODevice::WriteOnly); + start(false); + return m_device; +} + +void QWasmAudioSink::start(bool mode) +{ + auto formatError = [this](){ + qWarning() << "Unsupported audio format " << m_format; + setError(QAudio::OpenError); + }; + switch (m_format.sampleFormat()) { + case QAudioFormat::UInt8: + switch (m_format.channelCount()) { + case 1: + aldata->format = AL_FORMAT_MONO8; + break; + case 2: + aldata->format = AL_FORMAT_STEREO8; + break; + default: + return formatError(); + } + break; + case QAudioFormat::Int16: + switch (m_format.channelCount()) { + case 1: + aldata->format = AL_FORMAT_MONO16; + break; + case 2: + aldata->format = AL_FORMAT_STEREO16; + break; + default: + return formatError(); + } + break; + case QAudioFormat::Float: + switch (m_format.channelCount()) { + case 1: + aldata->format = AL_FORMAT_MONO_FLOAT32; + break; + case 2: + aldata->format = AL_FORMAT_STEREO_FLOAT32; + break; + default: + return formatError(); + } + break; + default: + return formatError(); + } + + alGetError(); + aldata->device = alcOpenDevice(m_name.data()); + if (!aldata->device) { + qWarning() << "Failed to open audio device" << alGetString(alGetError()); + return setError(QAudio::OpenError); + } + ALint attrlist[] = {ALC_FREQUENCY, m_format.sampleRate(), 0}; + aldata->context = alcCreateContext(aldata->device, attrlist); + + if (!aldata->context) { + qWarning() << "Failed to create audio context" << alGetString(alGetError()); + return setError(QAudio::OpenError); + } + alcMakeContextCurrent(aldata->context); + + alGenSources(1, &aldata->source); + + if (m_bufferSize > 0) + m_bufferFragmentsCount = qMax(2,qCeil((qreal)m_bufferSize/(m_bufferFragmentSize))); + m_bufferSize = m_bufferFragmentsCount * m_bufferFragmentSize; + aldata->buffers = new ALuint[m_bufferFragmentsCount]; + aldata->buffer = aldata->buffers; + alGenBuffers(m_bufferFragmentsCount, aldata->buffers); + m_processed = 0; + m_tmpDataOffset = 0; + m_pullMode = mode; + alSourcef(aldata->source, AL_GAIN, m_volume); + if (m_pullMode) + loadALBuffers(); + m_timer->setInterval(DEFAULT_BUFFER_DURATION / 3000); + m_timer->start(); + if (m_pullMode) + alSourcePlay(aldata->source); + m_running = true; + m_elapsedTimer.start(); + updateState(); +} + +void QWasmAudioSink::stop() +{ + if (!m_running) + return; + m_elapsedTimer.invalidate(); + alSourceStop(aldata->source); + alSourceRewind(aldata->source); + m_timer->stop(); + m_bufferFragmentsBusyCount = 0; + alDeleteSources(1, &aldata->source); + alDeleteBuffers(m_bufferFragmentsCount, aldata->buffers); + delete[] aldata->buffers; + alcMakeContextCurrent(nullptr); + alcDestroyContext(aldata->context); + alcCloseDevice(aldata->device); + m_running = false; + m_processed = 0; + if (!m_pullMode) + m_device->deleteLater(); + updateState(); +} + +void QWasmAudioSink::reset() +{ + stop(); + m_error = QAudio::NoError; +} + +void QWasmAudioSink::suspend() +{ + if (!m_running) + return; + + m_suspendedInState = m_state; + alSourcePause(aldata->source); +} + +void QWasmAudioSink::resume() +{ + if (!m_running) + return; + + alSourcePlay(aldata->source); +} + +qsizetype QWasmAudioSink::bytesFree() const +{ + int processed; + alGetSourcei(aldata->source, AL_BUFFERS_PROCESSED, &processed); + return m_running ? m_bufferFragmentSize * (m_bufferFragmentsCount - m_bufferFragmentsBusyCount + + (qsizetype)processed) : 0; +} + +void QWasmAudioSink::setBufferSize(qsizetype value) +{ + if (m_running) + return; + + m_bufferSize = value; +} + +qsizetype QWasmAudioSink::bufferSize() const +{ + return m_bufferSize; +} + +qint64 QWasmAudioSink::processedUSecs() const +{ + int processed; + alGetSourcei(aldata->source, AL_BUFFERS_PROCESSED, &processed); + return m_format.durationForBytes(m_processed + m_format.bytesForDuration( + DEFAULT_BUFFER_DURATION * processed)); +} + +QAudio::Error QWasmAudioSink::error() const +{ + return m_error; +} + +QAudio::State QWasmAudioSink::state() const +{ + if (!m_running) + return QAudio::StoppedState; + ALint state; + alGetSourcei(aldata->source, AL_SOURCE_STATE, &state); + switch (state) { + case AL_INITIAL: + return QAudio::IdleState; + case AL_PLAYING: + return QAudio::ActiveState; + case AL_PAUSED: + return QAudio::SuspendedState; + case AL_STOPPED: + return m_running ? QAudio::IdleState : QAudio::StoppedState; + } + return QAudio::StoppedState; +} + +void QWasmAudioSink::setFormat(const QAudioFormat &fmt) +{ + if (m_running) + return; + m_format = fmt; + if (m_tmpData) + delete[] m_tmpData; + m_bufferFragmentSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION); + m_bufferSize = m_bufferFragmentSize * m_bufferFragmentsCount; + m_tmpData = new char[m_bufferFragmentSize]; +} + +QAudioFormat QWasmAudioSink::format() const +{ + return m_format; +} + +void QWasmAudioSink::setVolume(qreal volume) +{ + if (m_volume == volume) + return; + m_volume = volume; + if (m_running) + alSourcef(aldata->source, AL_GAIN, volume); +} + +qreal QWasmAudioSink::volume() const +{ + return m_volume; +} + +void QWasmAudioSink::loadALBuffers() +{ + if (m_bufferFragmentsBusyCount == m_bufferFragmentsCount) + return; + + if (m_device->bytesAvailable() == 0) { + return; + } + + auto size = m_device->read(m_tmpData + m_tmpDataOffset, m_bufferFragmentSize - + m_tmpDataOffset); + m_tmpDataOffset += size; + if (!m_tmpDataOffset || (m_tmpDataOffset != m_bufferFragmentSize && + m_bufferFragmentsBusyCount >= m_bufferFragmentsCount * 2 / 3)) + return; + + alBufferData(*aldata->buffer, aldata->format, m_tmpData, m_tmpDataOffset, + m_format.sampleRate()); + m_tmpDataOffset = 0; + alGetError(); + alSourceQueueBuffers(aldata->source, 1, aldata->buffer); + if (alGetError()) + return; + + m_bufferFragmentsBusyCount++; + m_processed += size; + if (++aldata->buffer == aldata->buffers + m_bufferFragmentsCount) + aldata->buffer = aldata->buffers; +} + +void QWasmAudioSink::unloadALBuffers() +{ + int processed; + alGetSourcei(aldata->source, AL_BUFFERS_PROCESSED, &processed); + + if (processed) { + auto head = aldata->buffer - m_bufferFragmentsBusyCount; + if (head < aldata->buffers) { + if (head + processed > aldata->buffers) { + auto batch = m_bufferFragmentsBusyCount - (aldata->buffer - aldata->buffers); + alGetError(); + alSourceUnqueueBuffers(aldata->source, batch, head + m_bufferFragmentsCount); + if (!alGetError()) { + m_bufferFragmentsBusyCount -= batch; + m_processed += m_bufferFragmentSize*batch; + } + processed -= batch; + if (!processed) + return; + head = aldata->buffers; + } else { + head += m_bufferFragmentsCount; + } + } + alGetError(); + alSourceUnqueueBuffers(aldata->source, processed, head); + if (!alGetError()) + m_bufferFragmentsBusyCount -= processed; + } +} + +void QWasmAudioSink::nextALBuffers() +{ + updateState(); + unloadALBuffers(); + loadALBuffers(); + ALint state; + alGetSourcei(aldata->source, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING && m_error == QAudio::NoError) + alSourcePlay(aldata->source); + updateState(); +} + +void QWasmAudioSink::updateState() +{ + auto current = state(); + if (m_state == current) + return; + m_state = current; + + if (m_state == QAudio::IdleState && m_running && m_device->bytesAvailable() == 0) + setError(QAudio::UnderrunError); + + emit stateChanged(m_state); + +} + +void QWasmAudioSink::setError(QAudio::Error error) +{ + if (error == m_error) + return; + m_error = error; + if (error != QAudio::NoError) { + m_timer->stop(); + alSourceRewind(aldata->source); + } + + emit errorChanged(error); +} + +QWasmAudioSinkDevice::QWasmAudioSinkDevice(QWasmAudioSink *parent) + : QIODevice(parent), + m_out(parent) +{ +} + +qint64 QWasmAudioSinkDevice::readData(char *data, qint64 maxlen) +{ + Q_UNUSED(data) + Q_UNUSED(maxlen) + return 0; +} + + +qint64 QWasmAudioSinkDevice::writeData(const char *data, qint64 len) +{ + ALint state; + alGetSourcei(m_out->aldata->source, AL_SOURCE_STATE, &state); + if (state != AL_INITIAL) + m_out->unloadALBuffers(); + if (m_out->m_bufferFragmentsBusyCount < m_out->m_bufferFragmentsCount) { + bool exceeds = m_out->m_tmpDataOffset + len > m_out->m_bufferFragmentSize; + bool flush = m_out->m_bufferFragmentsBusyCount < m_out->m_bufferFragmentsCount * 2 / 3 || + m_out->m_tmpDataOffset + len >= m_out->m_bufferFragmentSize; + const char *read; + char *tmp = m_out->m_tmpData; + int size = 0; + if (m_out->m_tmpDataOffset && exceeds) { + size = m_out->m_tmpDataOffset + len; + tmp = new char[m_out->m_tmpDataOffset + len]; + std::memcpy(tmp, m_out->m_tmpData, m_out->m_tmpDataOffset); + } + if (flush && !m_out->m_tmpDataOffset) { + read = data; + size = len; + } else { + std::memcpy(tmp + m_out->m_tmpDataOffset, data, len); + read = tmp; + if (!exceeds) { + m_out->m_tmpDataOffset += len; + size = m_out->m_tmpDataOffset; + } + } + m_out->m_processed += size; + if (size && flush) { + alBufferData(*m_out->aldata->buffer, m_out->aldata->format, read, size, + m_out->m_format.sampleRate()); + if (tmp && tmp != m_out->m_tmpData) + delete[] tmp; + m_out->m_tmpDataOffset = 0; + alGetError(); + alSourceQueueBuffers(m_out->aldata->source, 1, m_out->aldata->buffer); + if (alGetError()) + return 0; + m_out->m_bufferFragmentsBusyCount++; + if (++m_out->aldata->buffer == m_out->aldata->buffers + m_out->m_bufferFragmentsCount) + m_out->aldata->buffer = m_out->aldata->buffers; + if (state != AL_PLAYING) + alSourcePlay(m_out->aldata->source); + } + return len; + } + return 0; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/wasm/qwasmaudiosink_p.h b/src/multimedia/wasm/qwasmaudiosink_p.h new file mode 100644 index 000000000..975b7f6cc --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiosink_p.h @@ -0,0 +1,89 @@ +// 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 + +#ifndef QWASMAUDIOSINK_H +#define QWASMAUDIOSINK_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosystem_p.h> +#include <QTimer> +#include <QElapsedTimer> + +class ALData; +class QIODevice; + +QT_BEGIN_NAMESPACE + +class QWasmAudioSink : public QPlatformAudioSink +{ + Q_OBJECT + + QByteArray m_name; + ALData *aldata = nullptr; + QTimer *m_timer = nullptr; + QIODevice *m_device = nullptr; + QAudioFormat m_format; + QAudio::Error m_error = QAudio::NoError; + bool m_running = false; + QAudio::State m_state = QAudio::StoppedState; + QAudio::State m_suspendedInState = QAudio::SuspendedState; + int m_bufferSize = 0; + quint64 m_processed = 0; + QElapsedTimer m_elapsedTimer; + int m_bufferFragmentsCount = 10; + int m_notifyInterval = 0; + char *m_tmpData = nullptr; + int m_bufferFragmentSize = 0; + int m_lastNotified = 0; + int m_tmpDataOffset = 0; + int m_bufferFragmentsBusyCount = 0; + bool m_pullMode; + qreal m_volume = 1; + + void loadALBuffers(); + void unloadALBuffers(); + void nextALBuffers(); + +private slots: + void updateState(); + void setError(QAudio::Error); + +public: + QWasmAudioSink(const QByteArray &device, QObject *parent); + ~QWasmAudioSink(); + +public: + void start(QIODevice *device) override; + QIODevice *start() override; + void start(bool mode); + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesFree() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &fmt) override; + QAudioFormat format() const override; + void setVolume(qreal volume) override; + qreal volume() const override; + + friend class QWasmAudioSinkDevice; +}; + +QT_END_NAMESPACE + +#endif // QWASMAUDIOSINK_H diff --git a/src/multimedia/wasm/qwasmaudiosource.cpp b/src/multimedia/wasm/qwasmaudiosource.cpp new file mode 100644 index 000000000..81f222c4b --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiosource.cpp @@ -0,0 +1,315 @@ +// 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 "qwasmaudiosource_p.h" + +#include <emscripten.h> +#include <AL/al.h> +#include <AL/alc.h> +#include <QDataStream> +#include <QDebug> +#include <QtMath> +#include <private/qaudiohelpers_p.h> +#include <QIODevice> + +QT_BEGIN_NAMESPACE + +#define AL_FORMAT_MONO_FLOAT32 0x10010 +#define AL_FORMAT_STEREO_FLOAT32 0x10011 + +constexpr unsigned int DEFAULT_BUFFER_DURATION = 50'000; + +class QWasmAudioSourceDevice : public QIODevice +{ + QWasmAudioSource *m_in; + +public: + explicit QWasmAudioSourceDevice(QWasmAudioSource *in); + +protected: + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; +}; + +class ALData { +public: + ALCdevice *device = nullptr; + ALCcontext *context = nullptr; +}; + +void QWasmAudioSource::setError(const QAudio::Error &error) +{ + if (m_error == error) + return; + m_error = error; + emit errorChanged(error); +} + +void QWasmAudioSource::writeBuffer() +{ + int samples = 0; + alcGetIntegerv(aldata->device, ALC_CAPTURE_SAMPLES, 1, &samples); + samples = qMin(samples, m_format.framesForBytes(m_bufferSize)); + auto bytes = m_format.bytesForFrames(samples); + auto err = alcGetError(aldata->device); + alcCaptureSamples(aldata->device, m_tmpData, samples); + err = alcGetError(aldata->device); + if (err) { + qWarning() << alcGetString(aldata->device, err); + return setError(QAudio::FatalError); + } + if (m_volume < 1) + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, m_tmpData, m_tmpData, bytes); + m_processed += bytes; + m_device->write(m_tmpData,bytes); +} + +QWasmAudioSource::QWasmAudioSource(const QByteArray &device , QObject *parent) + : QPlatformAudioSource(parent), + m_name(device), + m_timer(new QTimer(this)) +{ + if (device.contains("Emscripten")) { + aldata = new ALData(); + connect(m_timer, &QTimer::timeout, this, [this](){ + Q_ASSERT(m_running); + if (m_pullMode) + writeBuffer(); + else if (bytesReady() > 0) + emit m_device->readyRead(); + }); + } +} + +void QWasmAudioSource::start(QIODevice *device) +{ + m_device = device; + start(true); +} + +QIODevice *QWasmAudioSource::start() +{ + m_device = new QWasmAudioSourceDevice(this); + m_device->open(QIODevice::ReadOnly); + start(false); + return m_device; +} + +void QWasmAudioSource::start(bool mode) +{ + m_pullMode = mode; + auto formatError = [this](){ + qWarning() << "Unsupported audio format " << m_format; + setError(QAudio::OpenError); + }; + ALCenum format; + switch (m_format.sampleFormat()) { + case QAudioFormat::UInt8: + switch (m_format.channelCount()) { + case 1: + format = AL_FORMAT_MONO8; + break; + case 2: + format = AL_FORMAT_STEREO8; + break; + default: + return formatError(); + } + break; + case QAudioFormat::Int16: + switch (m_format.channelCount()) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + default: + return formatError(); + } + break; + case QAudioFormat::Float: + switch (m_format.channelCount()) { + case 1: + format = AL_FORMAT_MONO_FLOAT32; + break; + case 2: + format = AL_FORMAT_STEREO_FLOAT32; + break; + default: + return formatError(); + } + break; + default: + return formatError(); + } + if (m_tmpData) + delete[] m_tmpData; + if (m_pullMode) + m_tmpData = new char[m_bufferSize]; + else + m_tmpData = nullptr; + m_timer->setInterval(m_format.durationForBytes(m_bufferSize) / 3000); + m_timer->start(); + + alcGetError(aldata->device); // clear error state + aldata->device = alcCaptureOpenDevice(m_name.data(), m_format.sampleRate(), format, + m_format.framesForBytes(m_bufferSize)); + + auto err = alcGetError(aldata->device); + if (err) { + qWarning() << "alcCaptureOpenDevice" << alcGetString(aldata->device, err); + return setError(QAudio::OpenError); + } + alcCaptureStart(aldata->device); + m_elapsedTimer.start(); + auto cerr = alcGetError(aldata->device); + if (cerr) { + qWarning() << "alcCaptureStart" << alcGetString(aldata->device, cerr); + return setError(QAudio::OpenError); + } + m_processed = 0; + m_running = true; +} + +void QWasmAudioSource::stop() +{ + if (m_pullMode) + writeBuffer(); + alcCaptureStop(aldata->device); + alcCaptureCloseDevice(aldata->device); + m_elapsedTimer.invalidate(); + if (m_tmpData) { + delete[] m_tmpData; + m_tmpData = nullptr; + } + if (!m_pullMode) + m_device->deleteLater(); + m_timer->stop(); + m_running = false; +} + +void QWasmAudioSource::reset() +{ + stop(); + if (m_tmpData) { + delete[] m_tmpData; + m_tmpData = nullptr; + } + m_running = false; + m_processed = 0; + m_error = QAudio::NoError; +} + +void QWasmAudioSource::suspend() +{ + if (!m_running) + return; + + m_suspended = true; + alcCaptureStop(aldata->device); +} + +void QWasmAudioSource::resume() +{ + if (!m_running) + return; + + m_suspended = false; + alcCaptureStart(aldata->device); +} + +qsizetype QWasmAudioSource::bytesReady() const +{ + if (!m_running) + return 0; + int samples; + alcGetIntegerv(aldata->device, ALC_CAPTURE_SAMPLES, 1, &samples); + return m_format.bytesForFrames(samples); +} + +void QWasmAudioSource::setBufferSize(qsizetype value) +{ + if (!m_running) + return; + m_bufferSize = value; +} + +qsizetype QWasmAudioSource::bufferSize() const +{ + return m_bufferSize; +} + +qint64 QWasmAudioSource::processedUSecs() const +{ + return m_format.durationForBytes(m_processed); +} + +QAudio::Error QWasmAudioSource::error() const +{ + return m_error; +} + +QAudio::State QWasmAudioSource::state() const +{ + if (m_running) + return QAudio::ActiveState; + else + return QAudio::StoppedState; +} + +void QWasmAudioSource::setFormat(const QAudioFormat &fmt) +{ + m_format = fmt; + m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION); +} + +QAudioFormat QWasmAudioSource::format() const +{ + return m_format; +} + +void QWasmAudioSource::setVolume(qreal volume) +{ + m_volume = volume; +} + +qreal QWasmAudioSource::volume() const +{ + return m_volume; +} + +QWasmAudioSourceDevice::QWasmAudioSourceDevice(QWasmAudioSource *in) : QIODevice(in), m_in(in) +{ + +} + +qint64 QWasmAudioSourceDevice::readData(char *data, qint64 maxlen) +{ + int samples; + alcGetIntegerv(m_in->aldata->device, ALC_CAPTURE_SAMPLES, 1, &samples); + samples = qMin(samples, m_in->m_format.framesForBytes(maxlen)); + auto bytes = m_in->m_format.bytesForFrames(samples); + alcGetError(m_in->aldata->device); + alcCaptureSamples(m_in->aldata->device, data, samples); + if (m_in->m_volume < 1) + QAudioHelperInternal::qMultiplySamples(m_in->m_volume, m_in->m_format, data, data, bytes); + auto err = alcGetError(m_in->aldata->device); + if (err) { + qWarning() << alcGetString(m_in->aldata->device, err); + m_in->setError(QAudio::FatalError); + return 0; + } + m_in->m_processed += bytes; + return bytes; +} + +qint64 QWasmAudioSourceDevice::writeData(const char *data, qint64 len) +{ + Q_UNREACHABLE(); + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/wasm/qwasmaudiosource_p.h b/src/multimedia/wasm/qwasmaudiosource_p.h new file mode 100644 index 000000000..97b4ec52a --- /dev/null +++ b/src/multimedia/wasm/qwasmaudiosource_p.h @@ -0,0 +1,75 @@ +// 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 + +#ifndef QWASMAUDIOSOURCE_H +#define QWASMAUDIOSOURCE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosystem_p.h> +#include <QTimer> +#include <QElapsedTimer> + +QT_BEGIN_NAMESPACE + +class ALData; + +class QWasmAudioSource : public QPlatformAudioSource +{ + Q_OBJECT + + QByteArray m_name; + ALData *aldata = nullptr; + QTimer *m_timer = nullptr; + QIODevice *m_device = nullptr; + QAudioFormat m_format; + qreal m_volume = 1; + qsizetype m_bufferSize; + bool m_running = false; + bool m_suspended = false; + QAudio::Error m_error; + bool m_pullMode; + char *m_tmpData = nullptr; + QElapsedTimer m_elapsedTimer; + int m_notifyInterval = 0; + quint64 m_processed = 0; + + void writeBuffer(); +public: + QWasmAudioSource(const QByteArray &device, QObject *parent); + +public: + void start(QIODevice *device) override; + QIODevice *start() override; + void start(bool mode); + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesReady() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &fmt) override; + QAudioFormat format() const override; + void setVolume(qreal volume) override; + qreal volume() const override; + + friend class QWasmAudioSourceDevice; + void setError(const QAudio::Error &error); +}; + +QT_END_NAMESPACE + +#endif // QWASMAUDIOSOURCE_H diff --git a/src/multimedia/wasm/qwasmmediadevices.cpp b/src/multimedia/wasm/qwasmmediadevices.cpp new file mode 100644 index 000000000..4e59fd161 --- /dev/null +++ b/src/multimedia/wasm/qwasmmediadevices.cpp @@ -0,0 +1,276 @@ +// 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 "qwasmmediadevices_p.h" +#include "private/qcameradevice_p.h" +#include "private/qplatformmediaintegration_p.h" +#include "qwasmaudiosource_p.h" +#include "qwasmaudiosink_p.h" +#include "qwasmaudiodevice_p.h" +#include <AL/al.h> +#include <AL/alc.h> + +#include <QMap> +#include <QDebug> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qWasmMediaDevices, "qt.multimedia.wasm.mediadevices") + +QWasmCameraDevices::QWasmCameraDevices(QPlatformMediaIntegration *integration) + : QPlatformVideoDevices(integration) +{ + m_mediaDevices = QPlatformMediaIntegration::instance()->mediaDevices(); +} + +QList<QCameraDevice> QWasmCameraDevices::videoDevices() const +{ + QWasmMediaDevices *wasmMediaDevices = reinterpret_cast<QWasmMediaDevices *>(m_mediaDevices); + return wasmMediaDevices ? wasmMediaDevices->videoInputs() : QList<QCameraDevice>(); +} + +QWasmMediaDevices::QWasmMediaDevices() +{ + initDevices(); +} + +void QWasmMediaDevices::initDevices() +{ + if (m_initDone) + return; + + m_initDone = true; + getOpenALAudioDevices(); + getMediaDevices(); // asynchronous +} + +QList<QAudioDevice> QWasmMediaDevices::audioInputs() const +{ + return m_audioInputs.values(); +} + +QList<QAudioDevice> QWasmMediaDevices::audioOutputs() const +{ + return m_audioOutputs.values(); +} + +QList<QCameraDevice> QWasmMediaDevices::videoInputs() const +{ + return m_cameraDevices.values(); +} + +QPlatformAudioSource *QWasmMediaDevices::createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QWasmAudioSource(deviceInfo.id(), parent); +} + +QPlatformAudioSink *QWasmMediaDevices::createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QWasmAudioSink(deviceInfo.id(), parent); +} + +void QWasmMediaDevices::parseDevices(emscripten::val devices) +{ + if (devices.isNull() || devices.isUndefined()) { + qWarning() << "Something went wrong enumerating devices"; + return; + } + + QList<std::string> cameraDevicesToRemove = m_cameraDevices.keys(); + QList<std::string> audioOutputsToRemove; + QList<std::string> audioInputsToRemove; + + if (m_firstInit) { + m_firstInit = false; + qWarning() << "m_audioInputs count" << m_audioInputs.count(); + + } else { + audioOutputsToRemove = m_audioOutputs.keys(); + audioInputsToRemove = m_audioInputs.keys(); + m_audioInputsAdded = false; + m_audioOutputsAdded = false; + } + m_videoInputsAdded = false; + + bool m_videoInputsRemoved = false; + bool m_audioInputsRemoved = false; + bool m_audioOutputsRemoved = false; + + for (int i = 0; i < devices["length"].as<int>(); i++) { + + emscripten::val mediaDevice = devices[i]; + + std::string defaultDeviceLabel = ""; + + const std::string deviceKind = mediaDevice["kind"].as<std::string>(); + const std::string label = mediaDevice["label"].as<std::string>(); + const std::string deviceId = mediaDevice["deviceId"].as<std::string>(); + + qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind) + << QString::fromStdString(deviceId) + << QString::fromStdString(label); + + if (deviceKind.empty()) + continue; + + if (deviceId == std::string("default")) { + // chrome specifies the default device with this as deviceId + // and then prepends "Default - " with the name of the device + // in the label + if (label.empty()) + continue; + + defaultDeviceLabel = label; + continue; + } + + const bool isDefault = false; // FIXME + // (defaultDeviceLabel.find(label) != std::string::npos); + + if (deviceKind == std::string("videoinput")) { + if (!m_cameraDevices.contains(deviceId)) { + QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData + camera->id = QString::fromStdString(deviceId).toUtf8(); + camera->description = QString::fromUtf8(label.c_str()); + camera->isDefault = isDefault; + + m_cameraDevices.insert(deviceId, camera->create()); + m_videoInputsAdded = true; + } + cameraDevicesToRemove.removeOne(deviceId); + } else if (deviceKind == std::string("audioinput")) { + if (!m_audioInputs.contains(deviceId)) { + m_audioInputs.insert(deviceId, + (new QWasmAudioDevice(deviceId.c_str(), label.c_str(), + isDefault, QAudioDevice::Input)) + ->create()); + + m_audioInputsAdded = true; + } + audioInputsToRemove.removeOne(deviceId); + } else if (deviceKind == std::string("audiooutput")) { + if (!m_audioOutputs.contains(deviceId)) { + m_audioOutputs.insert(deviceId, + (new QWasmAudioDevice(deviceId.c_str(), label.c_str(), + isDefault, QAudioDevice::Input)) + ->create()); + + m_audioOutputsAdded = true; + } + audioOutputsToRemove.removeOne(deviceId); + } + // if permissions are given label will hold the actual + // camera name, such as "Live! Cam Sync 1080p (041e:409d)" + } + if (!m_firstInit) + getOpenALAudioDevices(); + + // any left here were removed + int j = 0; + for (; j < cameraDevicesToRemove.count(); j++) { + m_cameraDevices.remove(cameraDevicesToRemove.at(j)); + } + m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty(); + + for (j = 0; j < audioInputsToRemove.count(); j++) { + m_audioInputs.remove(audioInputsToRemove.at(j)); + } + m_audioInputsRemoved = !audioInputsToRemove.isEmpty(); + + for (j = 0; j < audioOutputsToRemove.count(); j++) { + m_audioOutputs.remove(audioOutputsToRemove.at(j)); + } + m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty(); + + if (m_videoInputsAdded || m_videoInputsRemoved) + emit videoInputsChanged(); + if (m_audioInputsAdded || m_audioInputsRemoved) + emit audioInputsChanged(); + if (m_audioOutputsAdded || m_audioOutputsRemoved) + emit audioOutputsChanged(); + + m_firstInit = false; + +} + +void QWasmMediaDevices::getMediaDevices() +{ + emscripten::val navigator = emscripten::val::global("navigator"); + m_jsMediaDevicesInterface = navigator["mediaDevices"]; + + if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) { + qWarning() << "No media devices found"; + return; + } + + if (qstdweb::haveAsyncify()) { +#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY + emscripten::val devicesList = m_jsMediaDevicesInterface.call<emscripten::val>("enumerateDevices").await(); + if (devicesList.isNull() || devicesList.isUndefined()) { + qWarning() << "devices list error"; + return; + } + + parseDevices(devicesList); +#endif + } else { + qstdweb::PromiseCallbacks enumerateDevicesCallback{ + .thenFunc = + [&](emscripten::val devices) { + parseDevices(devices); + }, + .catchFunc = + [this](emscripten::val error) { + qWarning() << "mediadevices enumerateDevices fail" + << QString::fromStdString(error["name"].as<std::string>()) + << QString::fromStdString(error["message"].as<std::string>()); + m_initDone = false; + } + }; + + qstdweb::Promise::make(m_jsMediaDevicesInterface, + QStringLiteral("enumerateDevices"), + std::move(enumerateDevicesCallback)); + + // setup devicechange monitor + m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>( + m_jsMediaDevicesInterface, "devicechange", + [this, enumerateDevicesCallback](emscripten::val) { + qstdweb::Promise::make(m_jsMediaDevicesInterface, + QStringLiteral("enumerateDevices"), + std::move(enumerateDevicesCallback)); + }); + } + +} + +void QWasmMediaDevices::getOpenALAudioDevices() +{ + // VM3959:4 The AudioContext was not allowed to start. + // It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu + auto capture = alcGetString(nullptr, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER); + // present even if there is no capture device + if (capture && !m_audioOutputs.contains(capture)) { + m_audioInputs.insert(capture, + (new QWasmAudioDevice(capture, "WebAssembly audio capture device", + true, QAudioDevice::Input)) + ->create()); + m_audioInputsAdded = true; + emit audioInputsChanged(); + } + + auto playback = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); + // present even if there is no playback device + if (playback && !m_audioOutputs.contains(capture)) { + m_audioOutputs.insert(playback, + (new QWasmAudioDevice(playback, "WebAssembly audio playback device", + true, QAudioDevice::Output)) + ->create()); + emit audioOutputsChanged(); + } + m_firstInit = true; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/wasm/qwasmmediadevices_p.h b/src/multimedia/wasm/qwasmmediadevices_p.h new file mode 100644 index 000000000..b97036f97 --- /dev/null +++ b/src/multimedia/wasm/qwasmmediadevices_p.h @@ -0,0 +1,89 @@ +// 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 + +#ifndef QWASMMEDIADEVICES_H +#define QWASMMEDIADEVICES_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformmediadevices_p.h> + +#include <private/qplatformvideodevices_p.h> + +#include <QtCore/private/qstdweb_p.h> +#include <qaudio.h> +#include <qaudiodevice.h> +#include <qcameradevice.h> +#include <qset.h> +#include <QtCore/qloggingcategory.h> + +#include <emscripten.h> +#include <emscripten/val.h> +#include <emscripten/bind.h> +#include <QMapIterator> +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(qWasmMediaDevices) + +class QWasmAudioEngine; + +class QWasmCameraDevices : public QPlatformVideoDevices +{ + Q_OBJECT +public: + QWasmCameraDevices(QPlatformMediaIntegration *integration); + + QList<QCameraDevice> videoDevices() const override; +private: + // weak + QPlatformMediaDevices *m_mediaDevices; +}; + +class QWasmMediaDevices : public QPlatformMediaDevices +{ +public: + QWasmMediaDevices(); + + QList<QAudioDevice> audioInputs() const override; + QList<QAudioDevice> audioOutputs() const override; + QList<QCameraDevice> videoInputs() const; + + QPlatformAudioSource *createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) override; + QPlatformAudioSink *createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) override; + void initDevices(); + +private: + void updateCameraDevices(); + void getMediaDevices(); + void getOpenALAudioDevices(); + void parseDevices(emscripten::val devices); + + QMap <std::string, QAudioDevice> m_audioOutputs; + QMap <std::string, QAudioDevice> m_audioInputs; + QMap <std::string, QCameraDevice> m_cameraDevices; + + + std::unique_ptr<qstdweb::EventCallback> m_deviceChangedCallback; + + bool m_videoInputsAdded = false; + bool m_audioInputsAdded = false; + bool m_audioOutputsAdded = false; + emscripten::val m_jsMediaDevicesInterface = emscripten::val::undefined(); + bool m_initDone = false; + bool m_firstInit = false; +}; + +QT_END_NAMESPACE + +#endif |