Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasmwebaudiosource.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <emscripten.h>
7#include <emscripten/val.h>
8#include <QDebug>
9#include <QtMath>
10#include <QIODevice>
11
12using emscripten::EM_VAL;
13
15
16constexpr unsigned int DEFAULT_BUFFER_DURATION = 250'000; // µs
17
18int QWasmAudioSource::s_nextId = 0;
19
21
23{
24 auto *src = s_registry.value(callbackId);
25 if (!src)
26 return;
27 src->m_workletReady = true;
28 src->connectMediaStreamIfReady();
29}
30
32{
33 if (auto *src = s_registry.value(callbackId))
34 src->deliverBufferedData();
35}
36
37extern "C" {
40}
41
42#if QT_CONFIG(thread)
43
44constexpr int RING_BUFFER_DURATION = 100'000; // µs
45
46// Load the AudioWorklet processor via Blob URL.
47// The processor writes PCM directly into the WASM heap (SharedArrayBuffer)
48// ring buffer.
49// Pointers are byte offsets into the WASM linear memory (which is a
50// SharedArrayBuffer in threaded builds). The write position is updated with
51// Atomics.store so the C++ acquire-load on m_writePos sees coherent data.
52
53EM_JS(void, qt_loadWorkletModule,
54 (EM_VAL ctxHandle,
55 int ringPtr, int ringSize,
56 int wposPtr, int volPtr,
57 int channels, int fmt, int bps,
58 int callbackId), {
59 if (!Module._qtWorkletParams) Module._qtWorkletParams = {};
60 Module._qtWorkletParams[callbackId] = {
61 heap: HEAP8.buffer, // SharedArrayBuffer: the WASM linear memory, shared with the C++ thread
62 ringPtr: ringPtr,
63 ringSize: ringSize,
64 wposPtr: wposPtr,
65 volPtr: volPtr,
66 channels: channels,
67 fmt: fmt,
68 bps: bps
69 };
70 var code = [
71 'class QtCapture extends AudioWorkletProcessor {',
72 ' constructor(opts) {',
73 ' super(opts);',
74 ' var options = opts.processorOptions;',
75 ' this._heap8 = new Int8Array(options.heap);', // typed views over the SharedArrayBuffer
76 ' this._heap16 = new Int16Array(options.heap);', // same buffer, different element width
77 ' this._heap32 = new Int32Array(options.heap);',
78 ' this._volumeConvInt = new Int32Array(1);',
79 ' this._volumeConvFloat = new Float32Array(this._volumeConvInt.buffer);',
80 ' this._sampleConvFloat = new Float32Array(1);',
81 ' this._sampleConvInt = new Int32Array(this._sampleConvFloat.buffer);',
82 ' this._ringBufferPtr = options.ringPtr | 0;',
83 ' this._ringBufferSize = options.ringSize | 0;',
84 ' this._writePositionIndex = (options.wposPtr >> 2) | 0;',
85 ' this._volumeIndex = (options.volPtr >> 2) | 0;',
86 ' this._numChannels = options.channels | 0;',
87 ' this._format = options.fmt | 0;',
88 ' this._bytesPerSample = options.bps | 0;',
89 ' }',
90 ' process(inputs) {',
91 ' var input = inputs[0];',
92 ' if (!input || !input.length || !input[0] || !input[0].length) return true;',
93 ' var numChannels = Math.min(input.length, this._numChannels);',
94 ' var samplesPerChannel = input[0].length;',
95 ' var bytesPerSample = this._bytesPerSample, ringSize = this._ringBufferSize, format = this._format;',
96 ' var ringPtr = this._ringBufferPtr;',
97 ' this._volumeConvInt[0] = Atomics.load(this._heap32, this._volumeIndex);',
98 ' var vol = this._volumeConvFloat[0];',
99 ' var writePos = Atomics.load(this._heap32, this._writePositionIndex);',
100 ' for (var i = 0; i < samplesPerChannel; i++) {',
101 ' for (var c = 0; c < numChannels; c++) {',
102 ' var sample = input[c][i] * vol;',
103 ' sample = sample < -1 ? -1 : sample > 1 ? 1 : sample;',
104 ' var offset = ringPtr + writePos;',
105 ' if (format === 1) { this._heap8 [offset] = ((sample + 1.0) * 127.5) | 0; }',
106 ' else if (format === 2) { this._heap16[offset>>1] = (sample * 32767) | 0; }',
107 ' else if (format === 3) { this._heap32[offset>>2] = (sample * 2147483647) | 0; }',
108 ' else { this._sampleConvFloat[0] = sample; this._heap32[offset>>2] = this._sampleConvInt[0]; }',
109 ' writePos = (writePos + bytesPerSample) % ringSize;',
110 ' }',
111 ' }',
112 ' Atomics.store(this._heap32, this._writePositionIndex, writePos);',
113 ' this.port.postMessage(null);',
114 ' return true;',
115 ' }',
116 '}',
117 'registerProcessor("qt-audio-capture", QtCapture);'
118 ].join('\n');
119 var blob = new Blob([code], {type: 'application/javascript'});
120 var url = URL.createObjectURL(blob);
121 Emval.toValue(ctxHandle).audioWorklet.addModule(url).then(function() {
122 URL.revokeObjectURL(url);
123 Module._qt_onWorkletReady(callbackId);
124 });
125});
126
127EM_JS(void, qt_mt_setupWorkletPort, (EM_VAL nodeHandle, int callbackId), {
128 Emval.toValue(nodeHandle).port.onmessage = function() {
129 Module._qt_onAudioFrameReady(callbackId);
130 };
131});
132
133
134#else // QT_CONFIG(thread)
135
136// Single-threaded: load a JS worklet that sends frames via MessagePort.
137
138 // qWarning() << "single threaded";
139
140// qWarning() << "Single-threaded";
141
142EM_JS(void, qt_st_loadWorkletModule, (EM_VAL ctxHandle, int instanceId), {
146 var code = [
147 'class QtCapture extends AudioWorkletProcessor {',
148 ' process(inputs) {',
149 ' var input = inputs[0];',
150 ' if (input && input.length && input[0] && input[0].length) {',
151 ' var numChannels = input.length, samplesPerChannel = input[0].length;',
152 ' var buffer = new Float32Array(numChannels * samplesPerChannel);',
153 ' for (var c = 0; c < numChannels; c++) buffer.set(input[c], c * samplesPerChannel);',
154 ' this.port.postMessage({ch:numChannels,spch:samplesPerChannel,buf:buffer.buffer},[buffer.buffer]);',
155 ' }',
156 ' return true;',
157 ' }',
158 '}',
159 'registerProcessor("qt-audio-capture", QtCapture);'
160 ].join('\n');
161 var blob = new Blob([code], {type: 'application/javascript'});
166 });
167});
168
169EM_JS(EM_VAL, qt_st_createWorkletNode, (EM_VAL ctxHandle, int instanceId, int channelCount), {
170 var node = new AudioWorkletNode(Emval.toValue(ctxHandle), 'qt-audio-capture', {
171 numberOfInputs: 1,
172 numberOfOutputs: 0,
173 channelCount: channelCount,
174 channelCountMode: 'explicit'
175 });
176 node.port.onmessage = function(e) {
177 Module._qtAudioData[instanceId].push(e.data);
178 Module._qt_onAudioFrameReady(instanceId);
179 };
180 return Emval.toHandle(node);
181});
182
183
184// Dequeue the oldest frame into a C heap buffer (planar Float32, ch*spch floats).
185EM_JS(int, qt_st_readFrame, (int instanceId, float *heapPtr, int *outCh, int *outSpch), {
187 if (!q || !q.length) return 0;
188 var frame = q.shift();
190 HEAPF32.set(data, heapPtr >> 2);
191 HEAP32[outCh >> 2] = frame.ch;
192 HEAP32[outSpch >> 2] = frame.spch;
193 return data.length;
194});
195
196#endif // QT_CONFIG(thread)
197
199{
200 QWasmAudioSource *m_source;
201public:
202 explicit QWasmAudioSourceDevice(QWasmAudioSource *src) : QIODevice(src), m_source(src) {}
203 bool isSequential() const override { return true; }
204protected:
205 qint64 readData(char *data, qint64 maxlen) override { return m_source->readFromBuffer(data, maxlen); }
206 qint64 writeData(const char *, qint64) override { Q_UNREACHABLE(); return 0; }
207};
208
209// Interleave planar float and convert to PCM — used by the single-threaded path.
210// multithread does this in worklet processor, so no need to share this.
211static void convertFloatToPcm(const float *planarData, int numChannels, int samplesPerChannel,
212 float volume, QAudioFormat::SampleFormat fmt, int bytesPerSample,
213 char *out)
214{
215 switch (fmt) {
216 case QAudioFormat::UInt8:
217 for (int i = 0; i < samplesPerChannel; ++i)
218 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
219 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
220 *reinterpret_cast<quint8 *>(out) = static_cast<quint8>((s + 1.0f) * 127.5f);
221 }
222 break;
223 case QAudioFormat::Int16:
224 for (int i = 0; i < samplesPerChannel; ++i)
225 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
226 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
227 *reinterpret_cast<qint16 *>(out) = static_cast<qint16>(s * 32767.0f);
228 }
229 break;
230 case QAudioFormat::Int32:
231 for (int i = 0; i < samplesPerChannel; ++i)
232 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
233 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
234 *reinterpret_cast<qint32 *>(out) = static_cast<qint32>(s * 2147483647.0f);
235 }
236 break;
237 case QAudioFormat::Float:
238 for (int i = 0; i < samplesPerChannel; ++i)
239 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
240 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
241 *reinterpret_cast<float *>(out) = s;
242 }
243 break;
244 default:
245 break;
246 }
247}
248
249// ===========================================================================
250// QWasmAudioSource implementation
251// ===========================================================================
252
253QWasmAudioSource::QWasmAudioSource(QAudioDevice device,
254 const QAudioFormat &fmt,
255 QObject *parent)
256 : QPlatformAudioSource(std::move(device), fmt, parent)
257{
258 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
259}
260
262{
263 teardownPipeline();
264}
265
266void QWasmAudioSource::start(QIODevice *device)
267{
268 m_device = device;
269 start(true);
270}
271
273{
274 auto *dev = new QWasmAudioSourceDevice(this);
275 dev->open(QIODevice::ReadOnly);
276 m_device = dev;
277 start(false);
278 return dev;
279}
280
281void QWasmAudioSource::start(bool pullMode)
282{
283 if (m_running || m_inputStream)
284 return;
285
286 if (m_format.sampleFormat() == QAudioFormat::Unknown
287 || m_format.channelCount() < 1
288 || m_format.channelCount() > 8) {
289 qWarning() << "QWasmAudioSource: unsupported format" << m_format;
290 setError(QAudio::OpenError);
291 return;
292 }
293
294 m_pullMode = pullMode;
295 m_processed = 0;
296 m_streamReady = false;
297 m_workletReady = false;
298 m_callbackId = ++s_nextId;
299 s_registry.insert(m_callbackId, this);
300
301#if QT_CONFIG(thread)
302 m_ringBuffer.resize(m_format.bytesForDuration(RING_BUFFER_DURATION));
303 m_writePos.store(0, std::memory_order_relaxed);
304 m_readPos.store(0, std::memory_order_relaxed);
305#endif
306
307 m_inputStream = new JsMediaInputStream(this);
308 m_inputStream->setUseAudio(true);
309 m_inputStream->setUseVideo(false);
310 connect(m_inputStream, &JsMediaInputStream::mediaAudioStreamReady, this, [this]() {
311 m_mediaStream = m_inputStream->getMediaStream();
312 m_streamReady = true;
313 connectMediaStreamIfReady();
314 });
315 m_inputStream->setStreamDevice(m_audioDevice.id().toStdString());
316
317#if QT_CONFIG(thread)
318 {
319 auto attrs = emscripten::val::object();
320 attrs.set("latencyHint", emscripten::val("interactive"));
321 attrs.set("sampleRate", m_format.sampleRate());
322 auto sinkId = emscripten::val::object();
323 sinkId.set("type", emscripten::val("none")); // do not send to output device
324 attrs.set("sinkId", sinkId);
325 m_audioContext = emscripten::val::global("AudioContext").new_(attrs);
326 }
327 // m_ringBuffer lives in the WASM heap (a SharedArrayBuffer in threaded builds);
328 // the worklet accesses it directly via HEAP8.buffer without any copy.
329 qt_loadWorkletModule(m_audioContext.as_handle(),
330 static_cast<int>(reinterpret_cast<intptr_t>(m_ringBuffer.data())),
331 static_cast<int>(m_ringBuffer.size()),
332 static_cast<int>(reinterpret_cast<intptr_t>(&m_writePos)),
333 static_cast<int>(reinterpret_cast<intptr_t>(&m_volumeAtomic)),
334 m_format.channelCount(),
335 static_cast<int>(m_format.sampleFormat()),
336 m_format.bytesPerSample(),
337 m_callbackId);
338#else
339 auto attrs = emscripten::val::object();
340 attrs.set("latencyHint", emscripten::val("interactive"));
341 attrs.set("sampleRate", m_format.sampleRate());
342 auto sinkId = emscripten::val::object();
343 sinkId.set("type", emscripten::val("none")); // do not send to output device
344 attrs.set("sinkId", sinkId);
345 m_audioContext = emscripten::val::global("AudioContext").new_(attrs);
346 qt_st_loadWorkletModule(m_audioContext.as_handle(), m_callbackId);
347#endif
348
349 m_elapsedTimer.start();
350}
351
353{
354 if (!m_running)
355 return;
356 if (m_pullMode)
357 deliverBufferedData();
358 teardownPipeline();
359 if (!m_pullMode)
360 m_device->deleteLater();
361 m_device = nullptr;
362}
363
365{
366 teardownPipeline();
367 m_processed = 0;
368 setError(QAudio::NoError);
369 m_device = nullptr;
370}
371
372void QWasmAudioSource::setBufferSize(qsizetype value)
373{
374 m_bufferSize = value;
375}
376
378{
379 return m_bufferSize;
380}
381
383{
384 return m_format.durationForBytes(m_processed);
385}
386
388{
389 if (!m_running) return QAudio::StoppedState;
390 if (m_suspended) return QAudio::SuspendedState;
391 return QAudio::ActiveState;
392}
393
395{
396 QPlatformAudioSource::setVolume(vol);
397#if QT_CONFIG(thread)
398 m_volumeAtomic.store(vol, std::memory_order_relaxed);
399#endif
400}
401
403{
404 if (!m_running || m_suspended)
405 return;
406 m_suspended = true;
407 m_audioContext.call<void>("suspend");
408}
409
411{
412 if (!m_running || !m_suspended)
413 return;
414 m_suspended = false;
415 m_audioContext.call<void>("resume");
416}
417
418void QWasmAudioSource::connectMediaStreamIfReady()
419{
420 if (!m_streamReady || !m_workletReady)
421 return;
422#if QT_CONFIG(thread)
423 auto nodeOpts = emscripten::val::object();
424 nodeOpts.set("numberOfInputs", 1);
425 nodeOpts.set("numberOfOutputs", 0);
426 nodeOpts.set("channelCount", m_format.channelCount());
427 nodeOpts.set("channelCountMode", emscripten::val("explicit"));
428 nodeOpts.set("processorOptions",
429 emscripten::val::module_property("_qtWorkletParams")[m_callbackId]);
430 m_workletNode = emscripten::val::global("AudioWorkletNode")
431 .new_(m_audioContext, std::string("qt-audio-capture"), nodeOpts);
432 qt_mt_setupWorkletPort(m_workletNode.as_handle(), m_callbackId);
433 m_audioContext.call<emscripten::val>("createMediaStreamSource", m_mediaStream)
434 .call<void>("connect", m_workletNode, 0, 0);
435 m_audioContext.call<void>("resume");
436 m_running.store(true, std::memory_order_release);
437#else
438 m_workletNode = emscripten::val::take_ownership(
439 qt_st_createWorkletNode(m_audioContext.as_handle(), m_callbackId, m_format.channelCount()));
440 m_audioContext.call<emscripten::val>("createMediaStreamSource", m_mediaStream)
441 .call<void>("connect", m_workletNode);
442 m_audioContext.call<void>("resume");
443 m_running = true;
444#endif
445}
446
447void QWasmAudioSource::deliverBufferedData()
448{
449 if (!m_running || !m_device || m_suspended)
450 return;
451
452#if QT_CONFIG(thread)
453 const int avail = static_cast<int>(bytesReady());
454 if (avail == 0)
455 return;
456 if (m_pullMode) {
457 const int ringSize = m_ringBuffer.size();
458 int rpos = m_readPos.load(std::memory_order_relaxed);
459 const int tail = ringSize - rpos;
460 if (avail <= tail) {
461 m_device->write(m_ringBuffer.constData() + rpos, avail);
462 } else {
463 m_device->write(m_ringBuffer.constData() + rpos, tail);
464 m_device->write(m_ringBuffer.constData(), avail - tail);
465 }
466 m_processed += avail;
467 m_readPos.store((rpos + avail) % ringSize, std::memory_order_release);
468 } else {
469 emit m_device->readyRead();
470 }
471#else
472 float frameBuf[128 * 8]; // one Web Audio quantum: 128 frames × 8 ch max
473 int numCh = 0, spch = 0;
474 const int bytesPerSample = m_format.bytesPerSample();
475 const float vol = volume();
476 m_pendingData.reserve(m_pendingData.size() + m_bufferSize);
477 while (qt_st_readFrame(m_callbackId, frameBuf, &numCh, &spch) > 0) {
478 const int prevSize = m_pendingData.size();
479 m_pendingData.resize(prevSize + spch * numCh * bytesPerSample);
480 convertFloatToPcm(frameBuf, numCh, spch, vol,
481 m_format.sampleFormat(), bytesPerSample,
482 m_pendingData.data() + prevSize);
483 }
484 if (m_pendingData.isEmpty())
485 return;
486 if (m_pullMode) {
487 m_processed += m_pendingData.size();
488 m_device->write(m_pendingData);
489 m_pendingData.clear();
490 } else {
491 emit m_device->readyRead();
492 }
493#endif
494}
495
496qint64 QWasmAudioSource::readFromBuffer(char *data, qint64 maxlen)
497{
498#if QT_CONFIG(thread)
499 const int avail = static_cast<int>(bytesReady());
500 const int chunk = static_cast<int>(qMin(maxlen, static_cast<qint64>(avail)));
501 if (chunk == 0)
502 return 0;
503 const int ringSize = m_ringBuffer.size();
504 int rpos = m_readPos.load(std::memory_order_relaxed);
505 const int tail = ringSize - rpos;
506 if (chunk <= tail) {
507 memcpy(data, m_ringBuffer.constData() + rpos, chunk);
508 } else {
509 memcpy(data, m_ringBuffer.constData() + rpos, tail);
510 memcpy(data + tail, m_ringBuffer.constData(), chunk - tail);
511 }
512 m_processed += chunk;
513 m_readPos.store((rpos + chunk) % ringSize, std::memory_order_release);
514 return chunk;
515#else
516 const qint64 chunk = qMin(maxlen, static_cast<qint64>(m_pendingData.size()));
517 if (chunk == 0)
518 return 0;
519 memcpy(data, m_pendingData.constData(), chunk);
520 m_pendingData.remove(0, chunk);
521 m_processed += chunk;
522 return chunk;
523#endif
524}
525
527{
528 if (!m_running)
529 return 0;
530#if QT_CONFIG(thread)
531 const int w = m_writePos.load(std::memory_order_acquire);
532 const int r = m_readPos.load(std::memory_order_relaxed);
533 return static_cast<qsizetype>((w - r + m_ringBuffer.size()) % m_ringBuffer.size());
534#else
535 return static_cast<qsizetype>(m_pendingData.size());
536#endif
537}
538
539void QWasmAudioSource::teardownPipeline()
540{
541 if (m_callbackId) {
542 s_registry.remove(m_callbackId);
543 auto paramsMap = emscripten::val::module_property("_qtWorkletParams");
544 if (!paramsMap.isUndefined()) paramsMap.set(m_callbackId, emscripten::val::undefined());
545 auto dataMap = emscripten::val::module_property("_qtAudioData");
546 if (!dataMap.isUndefined()) dataMap.set(m_callbackId, emscripten::val::array());
547 m_callbackId = 0;
548 }
549 m_workletNode = emscripten::val::undefined();
550 if (!m_audioContext.isUndefined()) {
551 m_audioContext.call<void>("close");
552 m_audioContext = emscripten::val::undefined();
553 }
554#if QT_CONFIG(thread)
555 m_running.store(false, std::memory_order_release);
556#else
557 m_running = false;
558 m_pendingData.clear();
559#endif
560 m_suspended = m_workletReady = m_streamReady = false;
561 delete m_inputStream;
562 m_inputStream = nullptr;
563 m_mediaStream = emscripten::val::undefined();
564 m_elapsedTimer.invalidate();
565}
566
567QT_END_NAMESPACE
void mediaAudioStreamReady()
void setUseAudio(bool useAudio)
Definition qwasmjs_p.h:97
void setUseVideo(bool useVideo)
Definition qwasmjs_p.h:98
qint64 readData(char *data, qint64 maxlen) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...
qint64 writeData(const char *, qint64) override
Writes up to maxSize bytes from data to the device.
bool isSequential() const override
Returns true if this device is sequential; otherwise returns false.
QWasmAudioSourceDevice(QWasmAudioSource *src)
static void workletReadyCallback(int callbackId)
qint64 processedUSecs() const override
QAudio::State state() const override
QIODevice * start() override
qsizetype bufferSize() const override
void setBufferSize(qsizetype value) override
void start(QIODevice *device) override
void setVolume(float volume) override
qint64 readFromBuffer(char *data, qint64 maxlen)
static void audioDataCallback(int callbackId)
qsizetype bytesReady() const override
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, outputChannelCounts:[channels], processorOptions:{ channels:channels } });node.port.onmessage=function() { Module._qt_sinkDeliverData(callbackId);};return Emval.toHandle(node);})
QT_BEGIN_NAMESPACE constexpr unsigned int DEFAULT_BUFFER_DURATION
static void convertFloatToPcm(const float *planarData, int numChannels, int samplesPerChannel, float volume, QAudioFormat::SampleFormat fmt, int bytesPerSample, char *out)
static QHash< int, QWasmAudioSource * > s_registry
EMSCRIPTEN_KEEPALIVE void qt_onWorkletReady(int id)
EM_JS(int, qt_st_readFrame,(int instanceId, float *heapPtr, int *outCh, int *outSpch), { var q=Module._qtAudioData &&Module._qtAudioData[instanceId];if(!q||!q.length) return 0;var frame=q.shift();var data=new Float32Array(frame.buf);HEAPF32.set(data, heapPtr > > 2);HEAP32[outCh > > 2]=frame.ch;HEAP32[outSpch > > 2]=frame.spch;return data.length;})
EMSCRIPTEN_KEEPALIVE void qt_onAudioFrameReady(int id)