Revision control

Copy as Markdown

// Copyright (C) 2026 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 "qwasmwebaudiosink_p.h"
#include <emscripten.h>
#include <emscripten/val.h>
#include <QDebug>
#include <QIODevice>
#include <QtMath>
using emscripten::EM_VAL;
QT_BEGIN_NAMESPACE
constexpr unsigned int DEFAULT_BUFFER_DURATION = 250'000; // µs
constexpr int RING_BUFFER_DURATION = 250'000; // µs
int QWasmAudioSink::s_nextId = 0;
static QHash<int, QWasmAudioSink *> s_sinkRegistry;
void QWasmAudioSink::workletReadyCallback(int callbackId)
{
if (auto *sink = s_sinkRegistry.value(callbackId))
sink->connectWorklet();
}
void QWasmAudioSink::deliverDataCallback(int callbackId)
{
if (auto *sink = s_sinkRegistry.value(callbackId))
sink->deliverData();
}
extern "C" {
EMSCRIPTEN_KEEPALIVE void qt_sinkWorkletReady(int id) { QWasmAudioSink::workletReadyCallback(id); }
EMSCRIPTEN_KEEPALIVE void qt_sinkDeliverData(int id) { QWasmAudioSink::deliverDataCallback(id); }
}
// ---------------------------------------------------------------------------
// Shared EM_JS helpers (both paths)
// ---------------------------------------------------------------------------
EM_JS(void, qt_sink_clearWorklet, (int callbackId), {
if (Module._qtSinkWorkletParams) Module._qtSinkWorkletParams[callbackId] = undefined;
});
// ---------------------------------------------------------------------------
// Threaded-path EM_JS helpers (SharedArrayBuffer/Atomics ring buffer)
// ---------------------------------------------------------------------------
#if QT_CONFIG(thread)
// Load the AudioWorklet processor via Blob URL.
// The worklet reads PCM from the WASM heap ring buffer (SharedArrayBuffer),
// converts sample-by-sample to planar Float32, and writes to outputs[0].
// rposPtr is updated by the worklet with Atomics.store after each render quantum;
// the C++ side reads it with m_readPos.load(std::memory_order_acquire).
EM_JS(void, qt_sink_loadWorkletModule,
(EM_VAL ctxHandle,
int ringBufferPtr, int ringBufferSize,
int readPositionPtr, int writePositionPtr, int volumePtr,
int activeIdPtr,
int channels, int sampleFormat, int bytesPerSample,
int callbackId), {
if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
Module._qtSinkWorkletParams[callbackId] = {
heap: HEAP8.buffer,
ringPtr: ringBufferPtr,
ringSize: ringBufferSize,
rposPtr: readPositionPtr,
wposPtr: writePositionPtr,
volPtr: volumePtr,
activeIdPtr: activeIdPtr,
callbackId: callbackId,
channels: channels,
fmt: sampleFormat,
bps: bytesPerSample
};
var code = [
'class QtSink extends AudioWorkletProcessor {',
' constructor(opts) {',
' super(opts);',
' var processorOptions = opts.processorOptions;',
' this._heap8 = new Int8Array(processorOptions.heap);',
' this._heap16 = new Int16Array(processorOptions.heap);',
' this._heap32 = new Int32Array(processorOptions.heap);',
' this._readPositionIdx = (processorOptions.rposPtr >> 2) | 0;',
' this._writePositionIdx = (processorOptions.wposPtr >> 2) | 0;',
' this._volumeIdx = (processorOptions.volPtr >> 2) | 0;',
' this._activeIdIdx = (processorOptions.activeIdPtr >> 2) | 0;',
' this._myId = processorOptions.callbackId | 0;',
' this._ringBufferPtr = processorOptions.ringPtr | 0;',
' this._ringBufferSize = processorOptions.ringSize | 0;',
' this._numChannels = processorOptions.channels | 0;',
' this._sampleFormat = processorOptions.fmt | 0;',
' this._bytesPerSample = processorOptions.bps | 0;',
' this._volConvI = new Int32Array(1);',
' this._volConvF = new Float32Array(this._volConvI.buffer);',
' this._fltConvI = new Int32Array(1);',
' this._fltConvF = new Float32Array(this._fltConvI.buffer);',
' }',
' process(inputs, outputs) {',
' if (Atomics.load(this._heap32, this._activeIdIdx) !== this._myId) return false;',
' var out = outputs[0];',
' if (!out || !out.length) return true;',
' var numChannels = out.length;',
' var samplesPerChannel = out[0].length;',
' this._volConvI[0] = Atomics.load(this._heap32, this._volumeIdx);',
' var volume = this._volConvF[0];',
' var writePosition = Atomics.load(this._heap32, this._writePositionIdx);',
' var readPosition = Atomics.load(this._heap32, this._readPositionIdx);',
' var ringBufferSize = this._ringBufferSize;',
' var availableBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;',
' var needed = samplesPerChannel * this._numChannels * this._bytesPerSample;',
' var canRead = availableBytes < needed ? availableBytes : needed;',
' var currentReadPosition = readPosition;',
' var ringBufferPtr = this._ringBufferPtr, sampleFormat = this._sampleFormat, bytesPerSample = this._bytesPerSample;',
' for (var i = 0; i < samplesPerChannel; i++) {',
' for (var channel = 0; channel < this._numChannels; channel++) {',
' var sample = 0.0;',
' if (canRead > 0) {',
' var offset = ringBufferPtr + currentReadPosition;',
' if (sampleFormat === 1) { sample = (this._heap8[offset] / 127.5) - 1.0; }',
' else if (sampleFormat === 2) { sample = this._heap16[offset>>1] / 32767.0; }',
' else if (sampleFormat === 3) { sample = this._heap32[offset>>2] / 2147483647.0; }',
' else { this._fltConvI[0] = this._heap32[offset>>2]; sample = this._fltConvF[0]; }',
' currentReadPosition = (currentReadPosition + bytesPerSample) % ringBufferSize;',
' canRead -= bytesPerSample;',
' }',
' if (channel < numChannels) {',
' sample = sample < -1.0 ? -1.0 : sample > 1.0 ? 1.0 : sample;',
' out[channel][i] = volume < 1.0 ? sample * volume : sample;',
' }',
' }',
' }',
' Atomics.store(this._heap32, this._readPositionIdx, currentReadPosition);',
' this.port.postMessage(null);',
' return true;',
' }',
'}',
'registerProcessor("qt-audio-sink", QtSink);'
].join('\n');
var blob = new Blob([code], { type: 'application/javascript' });
var url = URL.createObjectURL(blob);
Emval.toValue(ctxHandle).audioWorklet.addModule(url).then(function() {
URL.revokeObjectURL(url);
Module._qt_sinkWorkletReady(callbackId);
});
});
EM_JS(void, qt_mt_sink_setupWorkletPort, (EM_VAL nodeHandle, int callbackId), {
Emval.toValue(nodeHandle).port.onmessage = function() {
Module._qt_sinkDeliverData(callbackId);
};
});
EM_JS(EM_VAL, qt_sink_createWorkletNode,
(EM_VAL ctxHandle, int callbackId, int channels), {
var ctx = Emval.toValue(ctxHandle);
var params = Module._qtSinkWorkletParams[callbackId];
var node = new AudioWorkletNode(ctx, 'qt-audio-sink', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [channels],
processorOptions: params
});
return Emval.toHandle(node);
});
// Store updated ring buffer params and signal ready without reloading the module.
// Used on subsequent start() calls when the processor is already registered.
EM_JS(void, qt_sink_storeWorkletParams,
(int ringBufferPtr, int ringBufferSize,
int readPositionPtr, int writePositionPtr, int volumePtr,
int activeIdPtr,
int channels, int sampleFormat, int bytesPerSample,
int callbackId), {
if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
Module._qtSinkWorkletParams[callbackId] = {
heap: HEAP8.buffer,
ringPtr: ringBufferPtr,
ringSize: ringBufferSize,
rposPtr: readPositionPtr,
wposPtr: writePositionPtr,
volPtr: volumePtr,
activeIdPtr: activeIdPtr,
callbackId: callbackId,
channels: channels,
fmt: sampleFormat,
bps: bytesPerSample
};
});
#else // QT_CONFIG(thread)
// Single-threaded path: load a JS worklet that dequeues planar Float32 frames
// posted from the main thread via MessagePort.
EM_JS(void, qt_st_sink_loadWorkletModule,
(EM_VAL ctxHandle, int callbackId, int channels), {
var ctx = Emval.toValue(ctxHandle);
var code = [
'class QtSink extends AudioWorkletProcessor {',
' constructor(opts) {',
' super(opts);',
' this._numChannels = opts.processorOptions.channels | 0;',
' this._queue = [];',
' this._pos = 0;',
' this.port.onmessage = (e) => { this._queue.push(e.data); };',
' }',
' process(inputs, outputs) {',
' var out = outputs[0];',
' if (!out || !out.length) return true;',
' var samplesPerChannel = out[0].length;',
' for (var i = 0; i < samplesPerChannel; i++) {',
' while (this._queue.length > 0 && this._pos >= this._queue[0].samplesPerChannel) {',
' this._queue.shift();',
' this._pos = 0;',
' }',
' if (this._queue.length === 0) break;',
' var frame = this._queue[0];',
' for (var channel = 0; channel < out.length && channel < frame.numChannels; channel++)',
' out[channel][i] = frame.data[channel * frame.samplesPerChannel + this._pos];',
' this._pos++;',
' }',
' this.port.postMessage(null);',
' return true;',
' }',
'}',
'registerProcessor("qt-audio-sink", QtSink);'
].join('\n');
var blob = new Blob([code], { type: 'application/javascript' });
var url = URL.createObjectURL(blob);
ctx.audioWorklet.addModule(url).then(function() {
URL.revokeObjectURL(url);
Module._qt_sinkWorkletReady(callbackId);
});
});
EM_JS(EM_VAL, qt_st_sink_createWorkletNode,
(EM_VAL ctxHandle, int callbackId, int channels), {
var node = new AudioWorkletNode(Emval.toValue(ctxHandle), 'qt-audio-sink', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [channels],
processorOptions: { channels: channels }
});
node.port.onmessage = function() { Module._qt_sinkDeliverData(callbackId); };
return Emval.toHandle(node);
});
// Post one render quantum (planar Float32, ch × spch floats) to the worklet.
// WASM heap is not a SharedArrayBuffer in single-threaded builds, so we copy
// the data before transferring ownership of the buffer to the worklet thread.
EM_JS(void, qt_st_sink_postFrame,
(EM_VAL nodeHandle, int channels, int samplesPerChannel, const float *dataPtr), {
var copy = HEAPF32.slice(dataPtr >> 2, (dataPtr >> 2) + channels * samplesPerChannel);
Emval.toValue(nodeHandle).port.postMessage(
{ numChannels: channels, samplesPerChannel: samplesPerChannel, data: copy }, [copy.buffer]);
});
#endif // QT_CONFIG(thread)
// ---------------------------------------------------------------------------
// QWasmAudioSinkDevice — returned to caller in push mode
// ---------------------------------------------------------------------------
class QWasmAudioSinkDevice : public QIODevice
{
QWasmAudioSink *m_sink;
public:
explicit QWasmAudioSinkDevice(QWasmAudioSink *sink) : QIODevice(sink), m_sink(sink) {}
bool isSequential() const override { return true; }
protected:
qint64 readData(char *, qint64) override { return 0; }
qint64 writeData(const char *data, qint64 maxLength) override
{
const int freeBytes = static_cast<int>(m_sink->bytesFree());
const int toWrite = static_cast<int>(qMin(maxLength, static_cast<qint64>(freeBytes)));
if (toWrite == 0)
return 0;
m_sink->writeToRingBuffer(data, toWrite);
return toWrite;
}
};
// ---------------------------------------------------------------------------
// QWasmAudioSink
// ---------------------------------------------------------------------------
QWasmAudioSink::QWasmAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
: QPlatformAudioSink(std::move(device), format, parent)
{
m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
m_deviceId = m_audioDevice.id().toStdString();
// "System audiooutput/audioinput" is a Qt-internal fallback ID used when the
// browser has not granted device permissions. It is not a valid browser sinkId,
// so map it to "" to request the default audio output device.
if (m_deviceId.compare(0, 7, "System ") == 0)
m_deviceId.clear();
emscripten::val options = emscripten::val::object();
options.set("latencyHint", emscripten::val("interactive"));
options.set("sampleRate", m_format.sampleRate());
if (!m_deviceId.empty())
options.set("sinkId", emscripten::val(m_deviceId));
m_audioContext = emscripten::val::global("AudioContext").new_(options);
}
QWasmAudioSink::~QWasmAudioSink()
{
teardownPipeline();
if (!m_audioContext.isUndefined() && !m_audioContext.isNull()
&& m_audioContext["state"].as<std::string>() != "closed") {
m_audioContext.call<emscripten::val>("close");
}
}
void QWasmAudioSink::start(QIODevice *device)
{
Q_ASSERT(device);
Q_ASSERT(device->openMode().testFlag(QIODevice::ReadOnly));
m_device = device;
start(true); // pullMode
}
QIODevice *QWasmAudioSink::start()
{
auto *sinkDevice = new QWasmAudioSinkDevice(this);
sinkDevice->open(QIODevice::WriteOnly);
m_device = sinkDevice;
start(false /*pullMode*/);
return sinkDevice;
}
void QWasmAudioSink::start(AudioCallback &&callback)
{
m_audioCallback = std::move(callback);
start(false); // pushMode
}
void QWasmAudioSink::start(bool pullMode)
{
if (m_format.sampleFormat() == QAudioFormat::Unknown
|| m_format.channelCount() < 1
|| m_format.channelCount() > 8) {
qWarning() << "QWasmAudioSink: unsupported format" << m_format;
setError(QAudio::OpenError);
return;
}
m_pullMode = pullMode;
m_callbackId = ++s_nextId;
s_sinkRegistry.insert(m_callbackId, this);
m_ringBuffer.resize(m_format.bytesForDuration(RING_BUFFER_DURATION));
m_writePos.store(0, std::memory_order_relaxed);
m_readPos.store(0, std::memory_order_relaxed);
m_processed.store(0, std::memory_order_relaxed);
#if QT_CONFIG(thread)
m_activeCallbackId.store(m_callbackId, std::memory_order_relaxed);
// m_audioContext is created once in the constructor and reused across mode changes.
if (!m_workletModuleLoaded) {
qt_sink_loadWorkletModule(
m_audioContext.as_handle(),
static_cast<int>(reinterpret_cast<intptr_t>(m_ringBuffer.data())),
static_cast<int>(m_ringBuffer.size()),
static_cast<int>(reinterpret_cast<intptr_t>(&m_readPos)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_writePos)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_volumeAtomic)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_activeCallbackId)),
m_format.channelCount(),
static_cast<int>(m_format.sampleFormat()),
m_format.bytesPerSample(),
m_callbackId);
} else {
qt_sink_storeWorkletParams(
static_cast<int>(reinterpret_cast<intptr_t>(m_ringBuffer.data())),
static_cast<int>(m_ringBuffer.size()),
static_cast<int>(reinterpret_cast<intptr_t>(&m_readPos)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_writePos)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_volumeAtomic)),
static_cast<int>(reinterpret_cast<intptr_t>(&m_activeCallbackId)),
m_format.channelCount(),
static_cast<int>(m_format.sampleFormat()),
m_format.bytesPerSample(),
m_callbackId);
connectWorklet();
}
#else
if (!m_workletModuleLoaded) {
qt_st_sink_loadWorkletModule(m_audioContext.as_handle(), m_callbackId,
m_format.channelCount());
} else {
connectWorklet();
}
#endif
m_audioContext.call<emscripten::val>("resume");
}
// ---------------------------------------------------------------------------
// Connect worklet once the module has loaded
// ---------------------------------------------------------------------------
void QWasmAudioSink::connectWorklet()
{
m_workletModuleLoaded = true;
m_running = true;
#if QT_CONFIG(thread)
deliverData();
m_workletNode = emscripten::val::take_ownership(
qt_sink_createWorkletNode(m_audioContext.as_handle(), m_callbackId,
m_format.channelCount()));
qt_mt_sink_setupWorkletPort(m_workletNode.as_handle(), m_callbackId);
m_workletNode.call<void>("connect", m_audioContext["destination"]);
#else
m_workletNode = emscripten::val::take_ownership(
qt_st_sink_createWorkletNode(m_audioContext.as_handle(), m_callbackId,
m_format.channelCount()));
m_workletNode.call<void>("connect", m_audioContext["destination"]);
deliverData();
#endif
Q_EMIT stateChanged(m_suspended ? QAudio::SuspendedState : QAudio::ActiveState);
}
// ---------------------------------------------------------------------------
// Fill ring buffer from device/callback and deliver frames to worklet (ST only)
// ---------------------------------------------------------------------------
void QWasmAudioSink::deliverData()
{
if (!m_running || m_suspended)
return;
if (m_audioContext["state"] == emscripten::val("suspended"))
m_audioContext.call<emscripten::val>("resume");
if (m_pullMode && m_device) {
const int freeBytes = static_cast<int>(bytesFree());
if (freeBytes > 0) {
QByteArray pullBuffer(freeBytes, Qt::Uninitialized);
const qint64 bytesRead = m_device->read(pullBuffer.data(), freeBytes);
if (bytesRead > 0)
writeToRingBuffer(pullBuffer.constData(), static_cast<int>(bytesRead));
}
} else if (m_audioCallback) {
int freeBytes = static_cast<int>(bytesFree());
freeBytes -= freeBytes % m_format.bytesPerFrame(); // keep m_writePos frame-aligned
if (freeBytes > 0) {
QByteArray callbackBuffer(freeBytes, Qt::Uninitialized);
// Pass volume=1.0f here: volume is applied by the worklet (threaded)
// or by deliverToWorklet() (single-threaded), not pre-applied here.
QtMultimediaPrivate::runAudioCallback(
*m_audioCallback,
QSpan<std::byte>(reinterpret_cast<std::byte *>(callbackBuffer.data()), freeBytes),
m_format,
1.0f);
writeToRingBuffer(callbackBuffer.constData(), freeBytes);
}
}
#if !QT_CONFIG(thread)
deliverToWorklet();
#endif
}
// ---------------------------------------------------------------------------
// Single-threaded: drain ring buffer → convert PCM → post quanta to worklet
// ---------------------------------------------------------------------------
#if !QT_CONFIG(thread)
void QWasmAudioSink::deliverToWorklet()
{
const int numChannels = m_format.channelCount();
const int bytesPerSample = m_format.bytesPerSample();
const int renderBlockFrames = 128;
const int bytesPerFrame = renderBlockFrames * numChannels * bytesPerSample;
const int ringBufferSize = m_ringBuffer.size();
const float volume = m_volumeAtomic.load(std::memory_order_relaxed);
const QAudioFormat::SampleFormat sampleFormat = m_format.sampleFormat();
while (true) {
const int writePosition = m_writePos.load(std::memory_order_acquire);
const int readPosition = m_readPos.load(std::memory_order_relaxed);
const int availableBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;
if (availableBytes < bytesPerFrame)
break;
// Copy one quantum from the ring buffer into a local stack buffer.
char quantumBuffer[128 * 8 * sizeof(float)];
const int tailBytes = ringBufferSize - readPosition;
if (bytesPerFrame <= tailBytes) {
memcpy(quantumBuffer, m_ringBuffer.constData() + readPosition, bytesPerFrame);
} else {
memcpy(quantumBuffer, m_ringBuffer.constData() + readPosition, tailBytes);
memcpy(quantumBuffer + tailBytes, m_ringBuffer.constData(), bytesPerFrame - tailBytes);
}
m_readPos.store((readPosition + bytesPerFrame) % ringBufferSize, std::memory_order_release);
m_processed += bytesPerFrame;
// Convert interleaved PCM to planar Float32 with volume applied.
float planar[128 * 8];
for (int i = 0; i < renderBlockFrames; ++i) {
for (int channel = 0; channel < numChannels; ++channel) {
float sample = 0.0f;
const int sampleIndex = i * numChannels + channel;
switch (sampleFormat) {
case QAudioFormat::UInt8:
sample = (reinterpret_cast<const quint8 *>(quantumBuffer)[sampleIndex] / 127.5f) - 1.0f;
break;
case QAudioFormat::Int16:
sample = reinterpret_cast<const qint16 *>(quantumBuffer)[sampleIndex] / 32767.0f;
break;
case QAudioFormat::Int32:
sample = reinterpret_cast<const qint32 *>(quantumBuffer)[sampleIndex] / 2147483647.0f;
break;
case QAudioFormat::Float:
sample = reinterpret_cast<const float *>(quantumBuffer)[sampleIndex];
break;
default:
break;
}
planar[channel * renderBlockFrames + i] = qBound(-1.0f, sample * volume, 1.0f);
}
}
qt_st_sink_postFrame(m_workletNode.as_handle(), numChannels, renderBlockFrames, planar);
}
}
#endif
// ---------------------------------------------------------------------------
// Ring buffer write helper (shared by push device, pull mode, callback mode)
// ---------------------------------------------------------------------------
void QWasmAudioSink::writeToRingBuffer(const char *data, int bytes)
{
const int ringBufferSize = m_ringBuffer.size();
const int writePosition = m_writePos.load(std::memory_order_relaxed);
const int tailBytes = ringBufferSize - writePosition;
if (bytes <= tailBytes) {
memcpy(m_ringBuffer.data() + writePosition, data, bytes);
} else {
memcpy(m_ringBuffer.data() + writePosition, data, tailBytes);
memcpy(m_ringBuffer.data(), data + tailBytes, bytes - tailBytes);
}
m_writePos.store((writePosition + bytes) % ringBufferSize, std::memory_order_release);
#if QT_CONFIG(thread)
// Threaded: track bytes sent to ring buffer as a proxy for bytes played.
// The error is bounded by the ring buffer occupancy (~100ms).
m_processed.fetch_add(static_cast<quint64>(bytes), std::memory_order_relaxed);
#endif
}
// ---------------------------------------------------------------------------
// Control
// ---------------------------------------------------------------------------
void QWasmAudioSink::stop()
{
if (!m_running)
return;
teardownPipeline();
if (!m_pullMode && m_device)
delete m_device;
m_device = nullptr;
Q_EMIT stateChanged(QAudio::StoppedState);
}
void QWasmAudioSink::reset()
{
if (!m_running)
return;
teardownPipeline();
m_processed.store(0, std::memory_order_relaxed);
setError(QAudio::NoError);
Q_EMIT stateChanged(QAudio::StoppedState);
}
void QWasmAudioSink::suspend()
{
if (m_suspended)
return;
m_suspended = true;
m_audioContext.call<emscripten::val>("suspend");
Q_EMIT stateChanged(QAudio::SuspendedState);
}
void QWasmAudioSink::resume()
{
if (!m_suspended)
return;
m_suspended = false;
m_audioContext.call<emscripten::val>("resume");
Q_EMIT stateChanged(QAudio::ActiveState);
}
qsizetype QWasmAudioSink::bytesFree() const
{
const int ringBufferSize = m_ringBuffer.size();
if (ringBufferSize == 0)
return 0;
const int writePosition = m_writePos.load(std::memory_order_relaxed);
const int readPosition = m_readPos.load(std::memory_order_acquire);
const int usedBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;
return static_cast<qsizetype>(ringBufferSize - usedBytes - 1); // -1 distinguishes full from empty
}
void QWasmAudioSink::setBufferSize(qsizetype value)
{
if (!m_running)
m_bufferSize = value;
}
qsizetype QWasmAudioSink::bufferSize() const
{
return m_bufferSize;
}
qint64 QWasmAudioSink::processedUSecs() const
{
return m_format.durationForBytes(
static_cast<qint64>(m_processed.load(std::memory_order_relaxed)));
}
QAudio::State QWasmAudioSink::state() const
{
if (!m_running)
return QAudio::StoppedState;
if (m_suspended)
return QAudio::SuspendedState;
return QAudio::ActiveState;
}
void QWasmAudioSink::setVolume(float volume)
{
QPlatformAudioEndpointBase::setVolume(volume);
m_volumeAtomic.store(volume, std::memory_order_relaxed);
}
void QWasmAudioSink::setError(QAudio::Error error)
{
QPlatformAudioEndpointBase::setError(error);
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
void QWasmAudioSink::teardownPipeline()
{
if (m_callbackId) {
s_sinkRegistry.remove(m_callbackId);
qt_sink_clearWorklet(m_callbackId);
m_callbackId = 0;
}
m_running = false;
m_suspended = false;
m_audioCallback.reset();
#if QT_CONFIG(thread)
// Invalidate the active ID so the current worklet sees a generation mismatch
// on its next process() call and returns false, deactivating itself.
m_activeCallbackId.store(0, std::memory_order_release);
#endif
// Disconnect the node so audio stops immediately.
// The AudioContext stays alive and is reused on the next start().
// Do not suspend the threaded AudioContext here — the worklet deactivates
// itself via the activeCallbackId mismatch, and suspending causes an audible
// gap when teardown is immediately followed by a restart (e.g. channel change).
if (!m_workletNode.isUndefined() && !m_workletNode.isNull())
m_workletNode.call<void>("disconnect");
#if !QT_CONFIG(thread)
if (!m_audioContext.isUndefined() && !m_audioContext.isNull())
m_audioContext.call<emscripten::val>("suspend");
#endif
m_workletNode = emscripten::val::undefined();
}
QT_END_NAMESPACE