// 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 #include #include #include #include #include // 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) : m_name(device) { 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; alSourcePause(aldata->source); } void QWasmAudioSink::resume() { if (!m_running) return; alSourcePlay(aldata->source); } int QWasmAudioSink::bytesFree() const { int processed; alGetSourcei(aldata->source, AL_BUFFERS_PROCESSED, &processed); return m_running ? m_bufferFragmentSize * (m_bufferFragmentsCount - m_bufferFragmentsBusyCount + processed) : 0; } void QWasmAudioSink::setBufferSize(int value) { if (m_running) return; m_bufferSize = value; } int 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