summaryrefslogtreecommitdiffstats
path: root/src/multimedia/wasm
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/wasm')
-rw-r--r--src/multimedia/wasm/qwasmaudiodevice.cpp56
-rw-r--r--src/multimedia/wasm/qwasmaudiodevice_p.h31
-rw-r--r--src/multimedia/wasm/qwasmaudiosink.cpp464
-rw-r--r--src/multimedia/wasm/qwasmaudiosink_p.h89
-rw-r--r--src/multimedia/wasm/qwasmaudiosource.cpp315
-rw-r--r--src/multimedia/wasm/qwasmaudiosource_p.h75
-rw-r--r--src/multimedia/wasm/qwasmmediadevices.cpp276
-rw-r--r--src/multimedia/wasm/qwasmmediadevices_p.h89
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