summaryrefslogtreecommitdiffstats
path: root/src/multimedia/wasm/qwasmaudiosink.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/wasm/qwasmaudiosink.cpp')
-rw-r--r--src/multimedia/wasm/qwasmaudiosink.cpp464
1 files changed, 464 insertions, 0 deletions
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