7#include <emscripten/val.h>
13using emscripten::EM_VAL;
26 if (
auto *sink = s_sinkRegistry.value(callbackId))
27 sink->connectWorklet();
32 if (
auto *sink = s_sinkRegistry.value(callbackId))
61EM_JS(
void, qt_sink_loadWorkletModule,
63 int ringBufferPtr,
int ringBufferSize,
64 int readPositionPtr,
int writePositionPtr,
int volumePtr,
66 int channels,
int sampleFormat,
int bytesPerSample,
68 if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
69 Module._qtSinkWorkletParams[callbackId] = {
71 ringPtr: ringBufferPtr,
72 ringSize: ringBufferSize,
73 rposPtr: readPositionPtr,
74 wposPtr: writePositionPtr,
76 activeIdPtr: activeIdPtr,
77 callbackId: callbackId,
83 'class QtSink extends AudioWorkletProcessor {',
84 ' constructor(opts) {',
86 ' var processorOptions = opts.processorOptions;',
87 ' this._heap8 = new Int8Array(processorOptions.heap);',
88 ' this._heap16 = new Int16Array(processorOptions.heap);',
89 ' this._heap32 = new Int32Array(processorOptions.heap);',
90 ' this._readPositionIdx = (processorOptions.rposPtr >> 2) | 0;',
91 ' this._writePositionIdx = (processorOptions.wposPtr >> 2) | 0;',
92 ' this._volumeIdx = (processorOptions.volPtr >> 2) | 0;',
93 ' this._activeIdIdx = (processorOptions.activeIdPtr >> 2) | 0;',
94 ' this._myId = processorOptions.callbackId | 0;',
95 ' this._ringBufferPtr = processorOptions.ringPtr | 0;',
96 ' this._ringBufferSize = processorOptions.ringSize | 0;',
97 ' this._numChannels = processorOptions.channels | 0;',
98 ' this._sampleFormat = processorOptions.fmt | 0;',
99 ' this._bytesPerSample = processorOptions.bps | 0;',
100 ' this._volConvI = new Int32Array(1);',
101 ' this._volConvF = new Float32Array(this._volConvI.buffer);',
102 ' this._fltConvI = new Int32Array(1);',
103 ' this._fltConvF = new Float32Array(this._fltConvI.buffer);',
105 ' process(inputs, outputs) {',
106 ' if (Atomics.load(this._heap32, this._activeIdIdx) !== this._myId) return false;',
107 ' var out = outputs[0];',
108 ' if (!out || !out.length) return true;',
109 ' var numChannels = out.length;',
110 ' var samplesPerChannel = out[0].length;',
111 ' this._volConvI[0] = Atomics.load(this._heap32, this._volumeIdx);',
112 ' var volume = this._volConvF[0];',
113 ' var writePosition = Atomics.load(this._heap32, this._writePositionIdx);',
114 ' var readPosition = Atomics.load(this._heap32, this._readPositionIdx);',
115 ' var ringBufferSize = this._ringBufferSize;',
116 ' var availableBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;',
117 ' var needed = samplesPerChannel * this._numChannels * this._bytesPerSample;',
118 ' var canRead = availableBytes < needed ? availableBytes : needed;',
119 ' var currentReadPosition = readPosition;',
120 ' var ringBufferPtr = this._ringBufferPtr, sampleFormat = this._sampleFormat, bytesPerSample = this._bytesPerSample;',
121 ' for (var i = 0; i < samplesPerChannel; i++) {',
122 ' for (var channel = 0; channel < this._numChannels; channel++) {',
123 ' var sample = 0.0;',
124 ' if (canRead > 0) {',
125 ' var offset = ringBufferPtr + currentReadPosition;',
126 ' if (sampleFormat === 1) { sample = (this._heap8[offset] / 127.5) - 1.0; }',
127 ' else if (sampleFormat === 2) { sample = this._heap16[offset>>1] / 32767.0; }',
128 ' else if (sampleFormat === 3) { sample = this._heap32[offset>>2] / 2147483647.0; }',
129 ' else { this._fltConvI[0] = this._heap32[offset>>2]; sample = this._fltConvF[0]; }',
130 ' currentReadPosition = (currentReadPosition + bytesPerSample) % ringBufferSize;',
131 ' canRead -= bytesPerSample;',
133 ' if (channel < numChannels) {',
134 ' sample = sample < -1.0 ? -1.0 : sample > 1.0 ? 1.0 : sample;',
135 ' out[channel][i] = volume < 1.0 ? sample * volume : sample;',
139 ' Atomics.store(this._heap32, this._readPositionIdx, currentReadPosition);',
140 ' this.port.postMessage(null);',
144 'registerProcessor("qt-audio-sink", QtSink);'
146 var blob =
new Blob([code], { type:
'application/javascript' });
147 var url = URL.createObjectURL(blob);
148 Emval.toValue(ctxHandle).audioWorklet.addModule(url).then(function() {
149 URL.revokeObjectURL(url);
150 Module._qt_sinkWorkletReady(callbackId);
154EM_JS(
void, qt_mt_sink_setupWorkletPort, (EM_VAL nodeHandle,
int callbackId), {
155 Emval.toValue(nodeHandle).port.onmessage = function() {
156 Module._qt_sinkDeliverData(callbackId);
160EM_JS(EM_VAL, qt_sink_createWorkletNode,
161 (EM_VAL ctxHandle,
int callbackId,
int channels), {
162 var ctx = Emval.toValue(ctxHandle);
163 var params = Module._qtSinkWorkletParams[callbackId];
164 var node =
new AudioWorkletNode(ctx,
'qt-audio-sink', {
167 outputChannelCounts: [channels],
168 processorOptions: params
170 return Emval.toHandle(node);
176EM_JS(
void, qt_sink_storeWorkletParams,
177 (
int ringBufferPtr,
int ringBufferSize,
178 int readPositionPtr,
int writePositionPtr,
int volumePtr,
180 int channels,
int sampleFormat,
int bytesPerSample,
182 if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
183 Module._qtSinkWorkletParams[callbackId] = {
185 ringPtr: ringBufferPtr,
186 ringSize: ringBufferSize,
187 rposPtr: readPositionPtr,
188 wposPtr: writePositionPtr,
190 activeIdPtr: activeIdPtr,
191 callbackId: callbackId,
204 (EM_VAL ctxHandle,
int callbackId,
int channels), {
207 'class QtSink extends AudioWorkletProcessor {',
208 ' constructor(opts) {',
210 ' this._numChannels = opts.processorOptions.channels | 0;',
211 ' this._queue = [];',
213 ' this.port.onmessage = (e) => { this._queue.push(e.data); };',
215 ' process(inputs, outputs) {',
216 ' var out = outputs[0];',
217 ' if (!out || !out.length) return true;',
218 ' var samplesPerChannel = out[0].length;',
219 ' for (var i = 0; i < samplesPerChannel; i++) {',
220 ' while (this._queue.length > 0 && this._pos >= this._queue[0].samplesPerChannel) {',
221 ' this._queue.shift();',
224 ' if (this._queue.length === 0) break;',
225 ' var frame = this._queue[0];',
226 ' for (var channel = 0; channel < out.length && channel < frame.numChannels; channel++)',
227 ' out[channel][i] = frame.data[channel * frame.samplesPerChannel + this._pos];',
230 ' this.port.postMessage(null);',
234 'registerProcessor("qt-audio-sink", QtSink);'
244EM_JS(EM_VAL, qt_st_sink_createWorkletNode,
245 (EM_VAL ctxHandle,
int callbackId,
int channels), {
246 var node =
new AudioWorkletNode(Emval.toValue(ctxHandle),
'qt-audio-sink', {
249 outputChannelCounts: [channels],
250 processorOptions: { channels: channels }
252 node.port.onmessage = function() { Module._qt_sinkDeliverData(callbackId); };
253 return Emval.toHandle(node);
261 (EM_VAL nodeHandle,
int channels,
int samplesPerChannel,
const float *dataPtr), {
283 const int freeBytes =
static_cast<
int>(m_sink->bytesFree());
284 const int toWrite =
static_cast<
int>(qMin(maxLength,
static_cast<qint64>(freeBytes)));
287 m_sink->writeToRingBuffer(data, toWrite);
296QWasmAudioSink::QWasmAudioSink(QAudioDevice device,
const QAudioFormat &format, QObject *parent)
297 : QPlatformAudioSink(std::move(device), format, parent)
299 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
300 m_deviceId = m_audioDevice.id().toStdString();
304 if (m_deviceId.compare(0, 7,
"System ") == 0)
306 qWarning() <<
"QWasmAudioSink: sinkId =" << (m_deviceId.empty() ?
"(none/default)" : m_deviceId.c_str());
307 emscripten::val options = emscripten::val::object();
308 options.set(
"latencyHint", emscripten::val(
"interactive"));
309 options.set(
"sampleRate", m_format.sampleRate());
310 if (!m_deviceId.empty())
311 options.set(
"sinkId", emscripten::val(m_deviceId));
312 m_audioContext = emscripten::val::global(
"AudioContext").new_(options);
318 if (!m_audioContext.isUndefined() && !m_audioContext.isNull()
319 && m_audioContext[
"state"].as<std::string>() !=
"closed") {
320 m_audioContext.call<emscripten::val>(
"close");
327 Q_ASSERT(device->openMode().testFlag(QIODevice::ReadOnly));
335 sinkDevice->open(QIODevice::WriteOnly);
336 m_device = sinkDevice;
343 m_audioCallback = std::move(callback);
349 if (m_format.sampleFormat() == QAudioFormat::Unknown
350 || m_format.channelCount() < 1
351 || m_format.channelCount() > 8) {
352 qWarning() <<
"QWasmAudioSink: unsupported format" << m_format;
353 setError(QAudio::OpenError);
357 m_pullMode = pullMode;
358 m_callbackId = ++s_nextId;
361 m_ringBuffer.resize(m_format.bytesForDuration(RING_BUFFER_DURATION));
362 m_writePos.store(0, std::memory_order_relaxed);
363 m_readPos.store(0, std::memory_order_relaxed);
365 m_processed.store(0, std::memory_order_relaxed);
367 m_activeCallbackId.store(m_callbackId, std::memory_order_relaxed);
369 if (!m_workletModuleLoaded) {
370 qt_sink_loadWorkletModule(
371 m_audioContext.as_handle(),
372 static_cast<
int>(
reinterpret_cast<intptr_t>(m_ringBuffer.data())),
373 static_cast<
int>(m_ringBuffer.size()),
374 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_readPos)),
375 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_writePos)),
376 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_volumeAtomic)),
377 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_activeCallbackId)),
378 m_format.channelCount(),
379 static_cast<
int>(m_format.sampleFormat()),
380 m_format.bytesPerSample(),
383 qt_sink_storeWorkletParams(
384 static_cast<
int>(
reinterpret_cast<intptr_t>(m_ringBuffer.data())),
385 static_cast<
int>(m_ringBuffer.size()),
386 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_readPos)),
387 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_writePos)),
388 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_volumeAtomic)),
389 static_cast<
int>(
reinterpret_cast<intptr_t>(&m_activeCallbackId)),
390 m_format.channelCount(),
391 static_cast<
int>(m_format.sampleFormat()),
392 m_format.bytesPerSample(),
397 if (!m_workletModuleLoaded) {
398 qt_st_sink_loadWorkletModule(m_audioContext.as_handle(), m_callbackId,
399 m_format.channelCount());
405 m_audioContext.call<emscripten::val>(
"resume");
414 m_workletModuleLoaded =
true;
418 m_workletNode = emscripten::val::take_ownership(
419 qt_sink_createWorkletNode(m_audioContext.as_handle(), m_callbackId,
420 m_format.channelCount()));
421 qt_mt_sink_setupWorkletPort(m_workletNode.as_handle(), m_callbackId);
422 m_workletNode.call<
void>(
"connect", m_audioContext[
"destination"]);
424 m_workletNode = emscripten::val::take_ownership(
425 qt_st_sink_createWorkletNode(m_audioContext.as_handle(), m_callbackId,
426 m_format.channelCount()));
427 m_workletNode.call<
void>(
"connect", m_audioContext[
"destination"]);
430 emit stateChanged(QAudio::ActiveState);
439 if (!m_running || m_suspended)
442 if (m_audioContext[
"state"] == emscripten::val(
"suspended"))
443 m_audioContext.call<emscripten::val>(
"resume");
445 if (m_pullMode && m_device) {
446 const int freeBytes =
static_cast<
int>(bytesFree());
448 QByteArray pullBuffer(freeBytes, Qt::Uninitialized);
449 const qint64 bytesRead = m_device->read(pullBuffer.data(), freeBytes);
451 writeToRingBuffer(pullBuffer.constData(),
static_cast<
int>(bytesRead));
453 }
else if (m_audioCallback) {
454 int freeBytes =
static_cast<
int>(bytesFree());
455 freeBytes -= freeBytes % m_format.bytesPerFrame();
457 QByteArray callbackBuffer(freeBytes, Qt::Uninitialized);
460 QtMultimediaPrivate::runAudioCallback(
462 QSpan<std::byte>(
reinterpret_cast<std::byte *>(callbackBuffer.data()), freeBytes),
465 writeToRingBuffer(callbackBuffer.constData(), freeBytes);
469#if !QT_CONFIG(thread)
478#if !QT_CONFIG(thread)
479void QWasmAudioSink::deliverToWorklet()
481 const int numChannels = m_format.channelCount();
482 const int bytesPerSample = m_format.bytesPerSample();
483 const int renderBlockFrames = 128;
484 const int bytesPerFrame = renderBlockFrames * numChannels * bytesPerSample;
485 const int ringBufferSize = m_ringBuffer.size();
486 const float volume = m_volumeAtomic.load(std::memory_order_relaxed);
487 const QAudioFormat::SampleFormat sampleFormat = m_format.sampleFormat();
490 const int writePosition = m_writePos.load(std::memory_order_acquire);
491 const int readPosition = m_readPos.load(std::memory_order_relaxed);
492 const int availableBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;
493 if (availableBytes < bytesPerFrame)
497 char quantumBuffer[128 * 8 *
sizeof(
float)];
498 const int tailBytes = ringBufferSize - readPosition;
499 if (bytesPerFrame <= tailBytes) {
500 memcpy(quantumBuffer, m_ringBuffer.constData() + readPosition, bytesPerFrame);
502 memcpy(quantumBuffer, m_ringBuffer.constData() + readPosition, tailBytes);
503 memcpy(quantumBuffer + tailBytes, m_ringBuffer.constData(), bytesPerFrame - tailBytes);
505 m_readPos.store((readPosition + bytesPerFrame) % ringBufferSize, std::memory_order_release);
506 m_processed += bytesPerFrame;
509 float planar[128 * 8];
510 for (
int i = 0; i < renderBlockFrames; ++i) {
511 for (
int channel = 0; channel < numChannels; ++channel) {
513 const int sampleIndex = i * numChannels + channel;
514 switch (sampleFormat) {
515 case QAudioFormat::UInt8:
516 sample = (
reinterpret_cast<
const quint8 *>(quantumBuffer)[sampleIndex] / 127.5f) - 1.0f;
518 case QAudioFormat::Int16:
519 sample =
reinterpret_cast<
const qint16 *>(quantumBuffer)[sampleIndex] / 32767.0f;
521 case QAudioFormat::Int32:
522 sample =
reinterpret_cast<
const qint32 *>(quantumBuffer)[sampleIndex] / 2147483647.0f;
524 case QAudioFormat::Float:
525 sample =
reinterpret_cast<
const float *>(quantumBuffer)[sampleIndex];
530 planar[channel * renderBlockFrames + i] = qBound(-1.0f, sample * volume, 1.0f);
533 qt_st_sink_postFrame(m_workletNode.as_handle(), numChannels, renderBlockFrames, planar);
542void QWasmAudioSink::writeToRingBuffer(
const char *data,
int bytes)
544 const int ringBufferSize = m_ringBuffer.size();
545 const int writePosition = m_writePos.load(std::memory_order_relaxed);
546 const int tailBytes = ringBufferSize - writePosition;
547 if (bytes <= tailBytes) {
548 memcpy(m_ringBuffer.data() + writePosition, data, bytes);
550 memcpy(m_ringBuffer.data() + writePosition, data, tailBytes);
551 memcpy(m_ringBuffer.data(), data + tailBytes, bytes - tailBytes);
553 m_writePos.store((writePosition + bytes) % ringBufferSize, std::memory_order_release);
557 m_processed.fetch_add(
static_cast<quint64>(bytes), std::memory_order_relaxed);
570 if (!m_pullMode && m_device)
573 emit stateChanged(QAudio::StoppedState);
579 m_processed.store(0, std::memory_order_relaxed);
580 setError(QAudio::NoError);
581 emit stateChanged(QAudio::StoppedState);
586 if (!m_running || m_suspended)
589 m_audioContext.call<emscripten::val>(
"suspend");
590 emit stateChanged(QAudio::SuspendedState);
595 if (!m_running || !m_suspended)
598 m_audioContext.call<emscripten::val>(
"resume");
599 emit stateChanged(QAudio::ActiveState);
604 const int ringBufferSize = m_ringBuffer.size();
605 if (ringBufferSize == 0)
607 const int writePosition = m_writePos.load(std::memory_order_relaxed);
608 const int readPosition = m_readPos.load(std::memory_order_acquire);
609 const int usedBytes = (writePosition - readPosition + ringBufferSize) % ringBufferSize;
610 return static_cast<qsizetype>(ringBufferSize - usedBytes - 1);
616 m_bufferSize = value;
626 return m_format.durationForBytes(
627 static_cast<qint64>(m_processed.load(std::memory_order_relaxed)));
633 return QAudio::StoppedState;
635 return QAudio::SuspendedState;
636 return QAudio::ActiveState;
641 QPlatformAudioEndpointBase::setVolume(volume);
642 m_volumeAtomic.store(volume, std::memory_order_relaxed);
647 QPlatformAudioEndpointBase::setError(error);
658 qt_sink_clearWorklet(m_callbackId);
664 m_audioCallback.reset();
669 m_activeCallbackId.store(0, std::memory_order_release);
677 if (!m_workletNode.isUndefined() && !m_workletNode.isNull())
678 m_workletNode.call<
void>(
"disconnect");
679#if !QT_CONFIG(thread)
680 if (!m_audioContext.isUndefined() && !m_audioContext.isNull())
681 m_audioContext.call<emscripten::val>(
"suspend");
683 m_workletNode = emscripten::val::undefined();
qint64 readData(char *, qint64) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...
QWasmAudioSinkDevice(QWasmAudioSink *sink)
bool isSequential() const override
Returns true if this device is sequential; otherwise returns false.
qint64 writeData(const char *data, qint64 maxLength) override
Writes up to maxSize bytes from data to the device.
void start(QIODevice *device) override
QIODevice * start() override
void start(AudioCallback &&callback) override
QAudio::State state() const override
qsizetype bufferSize() const override
void setBufferSize(qsizetype value) override
qsizetype bytesFree() const override
void setVolume(float volume) override
void setError(QAudio::Error) override
qint64 processedUSecs() const override
static void deliverDataCallback(int callbackId)
~QWasmAudioSink() override
static void workletReadyCallback(int callbackId)
QT_BEGIN_NAMESPACE constexpr unsigned int DEFAULT_BUFFER_DURATION
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(void, qt_sink_clearWorklet,(int callbackId), { if(Module._qtSinkWorkletParams) Module._qtSinkWorkletParams[callbackId]=undefined;})
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, outputChannelCounts:[channels], processorOptions:{ channels:channels } });node.port.onmessage=function() { Module._qt_sinkDeliverData(callbackId);};return Emval.toHandle(node);})
EMSCRIPTEN_KEEPALIVE void qt_sinkWorkletReady(int id)
static QHash< int, QWasmAudioSink * > s_sinkRegistry
EMSCRIPTEN_KEEPALIVE void qt_sinkDeliverData(int id)
constexpr int RING_BUFFER_DURATION