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
138EM_JS(void, qt_st_loadWorkletModule, (EM_VAL ctxHandle, int instanceId), {
142 var code = [
143 'class QtCapture extends AudioWorkletProcessor {',
144 ' process(inputs) {',
145 ' var input = inputs[0];',
146 ' if (input && input.length && input[0] && input[0].length) {',
147 ' var numChannels = input.length, samplesPerChannel = input[0].length;',
148 ' var buffer = new Float32Array(numChannels * samplesPerChannel);',
149 ' for (var c = 0; c < numChannels; c++) buffer.set(input[c], c * samplesPerChannel);',
150 ' this.port.postMessage({ch:numChannels,spch:samplesPerChannel,buf:buffer.buffer},[buffer.buffer]);',
151 ' }',
152 ' return true;',
153 ' }',
154 '}',
155 'registerProcessor("qt-audio-capture", QtCapture);'
156 ].join('\n');
157 var blob = new Blob([code], {type: 'application/javascript'});
162 });
163});
164
165EM_JS(EM_VAL, qt_st_createWorkletNode, (EM_VAL ctxHandle, int instanceId, int channelCount), {
166 var node = new AudioWorkletNode(Emval.toValue(ctxHandle), 'qt-audio-capture', {
167 numberOfInputs: 1,
168 numberOfOutputs: 0,
169 channelCount: channelCount,
170 channelCountMode: 'explicit'
171 });
172 node.port.onmessage = function(e) {
173 Module._qtAudioData[instanceId].push(e.data);
174 Module._qt_onAudioFrameReady(instanceId);
175 };
176 return Emval.toHandle(node);
177});
178
179
180// Dequeue the oldest frame into a C heap buffer (planar Float32, ch*spch floats).
181EM_JS(int, qt_st_readFrame, (int instanceId, float *heapPtr, int *outCh, int *outSpch), {
183 if (!q || !q.length) return 0;
184 var frame = q.shift();
186 HEAPF32.set(data, heapPtr >> 2);
187 HEAP32[outCh >> 2] = frame.ch;
188 HEAP32[outSpch >> 2] = frame.spch;
189 return data.length;
190});
191
192
193// Interleave planar float and convert to PCM — used by the single-threaded path.
194// multithread does this in worklet processor, so no need to share this.
195static void convertFloatToPcm(const float *planarData, int numChannels, int samplesPerChannel,
196 float volume, QAudioFormat::SampleFormat fmt, int bytesPerSample,
197 char *out)
198{
199 switch (fmt) {
200 case QAudioFormat::UInt8:
201 for (int i = 0; i < samplesPerChannel; ++i)
202 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
203 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
204 *reinterpret_cast<quint8 *>(out) = static_cast<quint8>((s + 1.0f) * 127.5f);
205 }
206 break;
207 case QAudioFormat::Int16:
208 for (int i = 0; i < samplesPerChannel; ++i)
209 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
210 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
211 *reinterpret_cast<qint16 *>(out) = static_cast<qint16>(s * 32767.0f);
212 }
213 break;
214 case QAudioFormat::Int32:
215 for (int i = 0; i < samplesPerChannel; ++i)
216 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
217 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
218 *reinterpret_cast<qint32 *>(out) = static_cast<qint32>(s * 2147483647.0f);
219 }
220 break;
221 case QAudioFormat::Float:
222 for (int i = 0; i < samplesPerChannel; ++i)
223 for (int ch = 0; ch < numChannels; ++ch, out += bytesPerSample) {
224 const float s = qBound(-1.0f, planarData[ch * samplesPerChannel + i] * volume, 1.0f);
225 *reinterpret_cast<float *>(out) = s;
226 }
227 break;
228 default:
229 break;
230 }
231}
232
233#endif // QT_CONFIG(thread)
234
236{
237 QWasmAudioSource *m_source;
238public:
239 explicit QWasmAudioSourceDevice(QWasmAudioSource *src) : QIODevice(src), m_source(src) {}
240 bool isSequential() const override { return true; }
241protected:
242 qint64 readData(char *data, qint64 maxlen) override { return m_source->readFromBuffer(data, maxlen); }
243 qint64 writeData(const char *, qint64) override { Q_UNREACHABLE(); return 0; }
244};
245
246// ===========================================================================
247// QWasmAudioSource implementation
248// ===========================================================================
249
250QWasmAudioSource::QWasmAudioSource(QAudioDevice device,
251 const QAudioFormat &fmt,
252 QObject *parent)
253 : QPlatformAudioSource(std::move(device), fmt, parent)
254{
255 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
256}
257
259{
260 teardownPipeline();
261}
262
263void QWasmAudioSource::start(QIODevice *device)
264{
265 m_device = device;
266 startPipeline(true);
267}
268
270{
271 auto *dev = new QWasmAudioSourceDevice(this);
272 dev->open(QIODevice::ReadOnly);
273 m_device = dev;
274 startPipeline(false);
275 return dev;
276}
277
278void QWasmAudioSource::startPipeline(bool pullMode)
279{
280 if (m_running || m_inputStream)
281 return;
282
283 if (m_format.sampleFormat() == QAudioFormat::Unknown
284 || m_format.channelCount() < 1
285 || m_format.channelCount() > 8) {
286 qWarning() << "QWasmAudioSource: unsupported format" << m_format;
287 setError(QAudio::OpenError);
288 return;
289 }
290
291 m_pullMode = pullMode;
292 m_processed = 0;
293 m_streamReady = false;
294 m_workletReady = false;
295 m_callbackId = ++s_nextId;
296 s_registry.insert(m_callbackId, this);
297
298#if QT_CONFIG(thread)
299 m_ringBuffer.resize(m_format.bytesForDuration(RING_BUFFER_DURATION));
300 m_writePos.store(0, std::memory_order_relaxed);
301 m_readPos.store(0, std::memory_order_relaxed);
302#endif
303
304 m_inputStream = new JsMediaInputStream(this);
305 m_inputStream->setUseAudio(true);
306 m_inputStream->setUseVideo(false);
307 connect(m_inputStream, &JsMediaInputStream::mediaAudioStreamReady, this, [this]() {
308 m_mediaStream = m_inputStream->getMediaStream();
309 m_streamReady = true;
310 connectMediaStreamIfReady();
311 });
312 m_inputStream->setStreamDevice(m_audioDevice.id().toStdString());
313
314#if QT_CONFIG(thread)
315 {
316 auto attrs = emscripten::val::object();
317 attrs.set("latencyHint", emscripten::val("interactive"));
318 attrs.set("sampleRate", m_format.sampleRate());
319 auto sinkId = emscripten::val::object();
320 sinkId.set("type", emscripten::val("none")); // do not send to output device
321 attrs.set("sinkId", sinkId);
322 m_audioContext = emscripten::val::global("AudioContext").new_(attrs);
323 }
324 // m_ringBuffer lives in the WASM heap (a SharedArrayBuffer in threaded builds);
325 // the worklet accesses it directly via HEAP8.buffer without any copy.
326 qt_loadWorkletModule(m_audioContext.as_handle(),
327 static_cast<int>(reinterpret_cast<intptr_t>(m_ringBuffer.data())),
328 static_cast<int>(m_ringBuffer.size()),
329 static_cast<int>(reinterpret_cast<intptr_t>(&m_writePos)),
330 static_cast<int>(reinterpret_cast<intptr_t>(&m_volumeAtomic)),
331 m_format.channelCount(),
332 static_cast<int>(m_format.sampleFormat()),
333 m_format.bytesPerSample(),
334 m_callbackId);
335#else
336 auto attrs = emscripten::val::object();
337 attrs.set("latencyHint", emscripten::val("interactive"));
338 attrs.set("sampleRate", m_format.sampleRate());
339 auto sinkId = emscripten::val::object();
340 sinkId.set("type", emscripten::val("none")); // do not send to output device
341 attrs.set("sinkId", sinkId);
342 m_audioContext = emscripten::val::global("AudioContext").new_(attrs);
343 qt_st_loadWorkletModule(m_audioContext.as_handle(), m_callbackId);
344#endif
345
346 m_elapsedTimer.start();
347}
348
350{
351 if (!m_running)
352 return;
353 if (m_pullMode)
354 deliverBufferedData();
355 teardownPipeline();
356 if (!m_pullMode)
357 m_device->deleteLater();
358 m_device = nullptr;
359}
360
362{
363 teardownPipeline();
364 m_processed = 0;
365 setError(QAudio::NoError);
366 m_device = nullptr;
367}
368
369void QWasmAudioSource::setBufferSize(qsizetype value)
370{
371 m_bufferSize = value;
372}
373
375{
376 return m_bufferSize;
377}
378
380{
381 return m_format.durationForBytes(m_processed);
382}
383
385{
386 if (!m_running) return QAudio::StoppedState;
387 if (m_suspended) return QAudio::SuspendedState;
388 return QAudio::ActiveState;
389}
390
392{
393 QPlatformAudioSource::setVolume(vol);
394#if QT_CONFIG(thread)
395 m_volumeAtomic.store(vol, std::memory_order_relaxed);
396#endif
397}
398
400{
401 if (!m_running || m_suspended)
402 return;
403 m_suspended = true;
404 m_audioContext.call<void>("suspend");
405}
406
408{
409 if (!m_running || !m_suspended)
410 return;
411 m_suspended = false;
412 m_audioContext.call<void>("resume");
413}
414
415void QWasmAudioSource::connectMediaStreamIfReady()
416{
417 if (!m_streamReady || !m_workletReady)
418 return;
419#if QT_CONFIG(thread)
420 auto nodeOpts = emscripten::val::object();
421 nodeOpts.set("numberOfInputs", 1);
422 nodeOpts.set("numberOfOutputs", 0);
423 nodeOpts.set("channelCount", m_format.channelCount());
424 nodeOpts.set("channelCountMode", emscripten::val("explicit"));
425 nodeOpts.set("processorOptions",
426 emscripten::val::module_property("_qtWorkletParams")[m_callbackId]);
427 m_workletNode = emscripten::val::global("AudioWorkletNode")
428 .new_(m_audioContext, std::string("qt-audio-capture"), nodeOpts);
429 qt_mt_setupWorkletPort(m_workletNode.as_handle(), m_callbackId);
430 m_audioContext.call<emscripten::val>("createMediaStreamSource", m_mediaStream)
431 .call<void>("connect", m_workletNode, 0, 0);
432 m_audioContext.call<void>("resume");
433 m_running.store(true, std::memory_order_release);
434#else
435 m_workletNode = emscripten::val::take_ownership(
436 qt_st_createWorkletNode(m_audioContext.as_handle(), m_callbackId, m_format.channelCount()));
437 m_audioContext.call<emscripten::val>("createMediaStreamSource", m_mediaStream)
438 .call<void>("connect", m_workletNode);
439 m_audioContext.call<void>("resume");
440 m_running = true;
441#endif
442}
443
444void QWasmAudioSource::deliverBufferedData()
445{
446 if (!m_running || !m_device || m_suspended)
447 return;
448
449#if QT_CONFIG(thread)
450 const int avail = static_cast<int>(bytesReady());
451 if (avail == 0)
452 return;
453 if (m_pullMode) {
454 const int ringSize = m_ringBuffer.size();
455 int rpos = m_readPos.load(std::memory_order_relaxed);
456 const int tail = ringSize - rpos;
457 if (avail <= tail) {
458 m_device->write(m_ringBuffer.constData() + rpos, avail);
459 } else {
460 m_device->write(m_ringBuffer.constData() + rpos, tail);
461 m_device->write(m_ringBuffer.constData(), avail - tail);
462 }
463 m_processed += avail;
464 m_readPos.store((rpos + avail) % ringSize, std::memory_order_release);
465 } else {
466 emit m_device->readyRead();
467 }
468#else
469 float frameBuf[128 * 8]; // one Web Audio quantum: 128 frames × 8 ch max
470 int numCh = 0, spch = 0;
471 const int bytesPerSample = m_format.bytesPerSample();
472 const float vol = volume();
473 m_pendingData.reserve(m_pendingData.size() + m_bufferSize);
474 while (qt_st_readFrame(m_callbackId, frameBuf, &numCh, &spch) > 0) {
475 const int prevSize = m_pendingData.size();
476 m_pendingData.resize(prevSize + spch * numCh * bytesPerSample);
477 convertFloatToPcm(frameBuf, numCh, spch, vol,
478 m_format.sampleFormat(), bytesPerSample,
479 m_pendingData.data() + prevSize);
480 }
481 if (m_pendingData.isEmpty())
482 return;
483 if (m_pullMode) {
484 m_processed += m_pendingData.size();
485 m_device->write(m_pendingData);
486 m_pendingData.clear();
487 } else {
488 emit m_device->readyRead();
489 }
490#endif
491}
492
493qint64 QWasmAudioSource::readFromBuffer(char *data, qint64 maxlen)
494{
495#if QT_CONFIG(thread)
496 const int avail = static_cast<int>(bytesReady());
497 const int chunk = static_cast<int>(qMin(maxlen, static_cast<qint64>(avail)));
498 if (chunk == 0)
499 return 0;
500 const int ringSize = m_ringBuffer.size();
501 int rpos = m_readPos.load(std::memory_order_relaxed);
502 const int tail = ringSize - rpos;
503 if (chunk <= tail) {
504 memcpy(data, m_ringBuffer.constData() + rpos, chunk);
505 } else {
506 memcpy(data, m_ringBuffer.constData() + rpos, tail);
507 memcpy(data + tail, m_ringBuffer.constData(), chunk - tail);
508 }
509 m_processed += chunk;
510 m_readPos.store((rpos + chunk) % ringSize, std::memory_order_release);
511 return chunk;
512#else
513 const qint64 chunk = qMin(maxlen, static_cast<qint64>(m_pendingData.size()));
514 if (chunk == 0)
515 return 0;
516 memcpy(data, m_pendingData.constData(), chunk);
517 m_pendingData.remove(0, chunk);
518 m_processed += chunk;
519 return chunk;
520#endif
521}
522
524{
525 if (!m_running)
526 return 0;
527#if QT_CONFIG(thread)
528 const int w = m_writePos.load(std::memory_order_acquire);
529 const int r = m_readPos.load(std::memory_order_relaxed);
530 return static_cast<qsizetype>((w - r + m_ringBuffer.size()) % m_ringBuffer.size());
531#else
532 return static_cast<qsizetype>(m_pendingData.size());
533#endif
534}
535
536void QWasmAudioSource::teardownPipeline()
537{
538 if (m_callbackId) {
539 s_registry.remove(m_callbackId);
540 auto paramsMap = emscripten::val::module_property("_qtWorkletParams");
541 if (!paramsMap.isUndefined()) paramsMap.set(m_callbackId, emscripten::val::undefined());
542 auto dataMap = emscripten::val::module_property("_qtAudioData");
543 if (!dataMap.isUndefined()) dataMap.set(m_callbackId, emscripten::val::array());
544 m_callbackId = 0;
545 }
546 m_workletNode = emscripten::val::undefined();
547 if (!m_audioContext.isUndefined()) {
548 m_audioContext.call<void>("close");
549 m_audioContext = emscripten::val::undefined();
550 }
551#if QT_CONFIG(thread)
552 m_running.store(false, std::memory_order_release);
553#else
554 m_running = false;
555 m_pendingData.clear();
556#endif
557 m_suspended = m_workletReady = m_streamReady = false;
558 delete m_inputStream;
559 m_inputStream = nullptr;
560 m_mediaStream = emscripten::val::undefined();
561 m_elapsedTimer.invalidate();
562}
563
564QT_END_NAMESPACE
void mediaAudioStreamReady()
void setUseAudio(bool useAudio)
Definition qwasmjs_p.h:100
void setUseVideo(bool useVideo)
Definition qwasmjs_p.h:101
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)