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
qwasmwebaudiosink.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
9#include <QDebug>
10#include <QIODevice>
11#include <QtMath>
12
13using emscripten::EM_VAL;
14
16
17constexpr unsigned int DEFAULT_BUFFER_DURATION = 250'000; // µs
18constexpr int RING_BUFFER_DURATION = 250'000; // µs
19
20int QWasmAudioSink::s_nextId = 0;
21
23
25{
26 if (auto *sink = s_sinkRegistry.value(callbackId))
27 sink->connectWorklet();
28}
29
31{
32 if (auto *sink = s_sinkRegistry.value(callbackId))
33 sink->deliverData();
34}
35
36extern "C" {
39}
40
41// ---------------------------------------------------------------------------
42// Shared EM_JS helpers (both paths)
43// ---------------------------------------------------------------------------
44
45EM_JS(void, qt_sink_clearWorklet, (int callbackId), {
47});
48
49// ---------------------------------------------------------------------------
50// Threaded-path EM_JS helpers (SharedArrayBuffer/Atomics ring buffer)
51// ---------------------------------------------------------------------------
52
53#if QT_CONFIG(thread)
54
55// Load the AudioWorklet processor via Blob URL.
56// The worklet reads PCM from the WASM heap ring buffer (SharedArrayBuffer),
57// converts sample-by-sample to planar Float32, and writes to outputs[0].
58// rposPtr is updated by the worklet with Atomics.store after each render quantum;
59// the C++ side reads it with m_readPos.load(std::memory_order_acquire).
60
61EM_JS(void, qt_sink_loadWorkletModule,
62 (EM_VAL ctxHandle,
63 int ringBufferPtr, int ringBufferSize,
64 int readPositionPtr, int writePositionPtr, int volumePtr,
65 int activeIdPtr,
66 int channels, int sampleFormat, int bytesPerSample,
67 int callbackId), {
68 if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
69 Module._qtSinkWorkletParams[callbackId] = {
70 heap: HEAP8.buffer,
71 ringPtr: ringBufferPtr,
72 ringSize: ringBufferSize,
73 rposPtr: readPositionPtr,
74 wposPtr: writePositionPtr,
75 volPtr: volumePtr,
76 activeIdPtr: activeIdPtr,
77 callbackId: callbackId,
78 channels: channels,
79 fmt: sampleFormat,
80 bps: bytesPerSample
81 };
82 var code = [
83 'class QtSink extends AudioWorkletProcessor {',
84 ' constructor(opts) {',
85 ' super(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);',
104 ' }',
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;',
132 ' }',
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;',
136 ' }',
137 ' }',
138 ' }',
139 ' Atomics.store(this._heap32, this._readPositionIdx, currentReadPosition);',
140 ' this.port.postMessage(null);',
141 ' return true;',
142 ' }',
143 '}',
144 'registerProcessor("qt-audio-sink", QtSink);'
145 ].join('\n');
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);
151 });
152});
153
154EM_JS(void, qt_mt_sink_setupWorkletPort, (EM_VAL nodeHandle, int callbackId), {
155 Emval.toValue(nodeHandle).port.onmessage = function() {
156 Module._qt_sinkDeliverData(callbackId);
157 };
158});
159
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', {
165 numberOfInputs: 0,
166 numberOfOutputs: 1,
167 outputChannelCounts: [channels],
168 processorOptions: params
169 });
170 return Emval.toHandle(node);
171});
172
173// Store updated ring buffer params and signal ready without reloading the module.
174// Used on subsequent start() calls when the processor is already registered.
175
176EM_JS(void, qt_sink_storeWorkletParams,
177 (int ringBufferPtr, int ringBufferSize,
178 int readPositionPtr, int writePositionPtr, int volumePtr,
179 int activeIdPtr,
180 int channels, int sampleFormat, int bytesPerSample,
181 int callbackId), {
182 if (!Module._qtSinkWorkletParams) Module._qtSinkWorkletParams = {};
183 Module._qtSinkWorkletParams[callbackId] = {
184 heap: HEAP8.buffer,
185 ringPtr: ringBufferPtr,
186 ringSize: ringBufferSize,
187 rposPtr: readPositionPtr,
188 wposPtr: writePositionPtr,
189 volPtr: volumePtr,
190 activeIdPtr: activeIdPtr,
191 callbackId: callbackId,
192 channels: channels,
193 fmt: sampleFormat,
194 bps: bytesPerSample
195 };
196});
197
198#else // QT_CONFIG(thread)
199
200// Single-threaded path: load a JS worklet that dequeues planar Float32 frames
201// posted from the main thread via MessagePort.
202
204 (EM_VAL ctxHandle, int callbackId, int channels), {
206 var code = [
207 'class QtSink extends AudioWorkletProcessor {',
208 ' constructor(opts) {',
209 ' super(opts);',
210 ' this._numChannels = opts.processorOptions.channels | 0;',
211 ' this._queue = [];',
212 ' this._pos = 0;',
213 ' this.port.onmessage = (e) => { this._queue.push(e.data); };',
214 ' }',
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();',
222 ' this._pos = 0;',
223 ' }',
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];',
228 ' this._pos++;',
229 ' }',
230 ' this.port.postMessage(null);',
231 ' return true;',
232 ' }',
233 '}',
234 'registerProcessor("qt-audio-sink", QtSink);'
235 ].join('\n');
236 var blob = new Blob([code], { type: 'application/javascript' });
241 });
242});
243
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', {
247 numberOfInputs: 0,
248 numberOfOutputs: 1,
249 outputChannelCounts: [channels],
250 processorOptions: { channels: channels }
251 });
252 node.port.onmessage = function() { Module._qt_sinkDeliverData(callbackId); };
253 return Emval.toHandle(node);
254});
255
256// Post one render quantum (planar Float32, ch × spch floats) to the worklet.
257// WASM heap is not a SharedArrayBuffer in single-threaded builds, so we copy
258// the data before transferring ownership of the buffer to the worklet thread.
259
261 (EM_VAL nodeHandle, int channels, int samplesPerChannel, const float *dataPtr), {
265});
266
267#endif // QT_CONFIG(thread)
268
269// ---------------------------------------------------------------------------
270// QWasmAudioSinkDevice — returned to caller in push mode
271// ---------------------------------------------------------------------------
272
274{
275 QWasmAudioSink *m_sink;
276public:
277 explicit QWasmAudioSinkDevice(QWasmAudioSink *sink) : QIODevice(sink), m_sink(sink) {}
278 bool isSequential() const override { return true; }
279protected:
280 qint64 readData(char *, qint64) override { return 0; }
281 qint64 writeData(const char *data, qint64 maxLength) override
282 {
283 const int freeBytes = static_cast<int>(m_sink->bytesFree());
284 const int toWrite = static_cast<int>(qMin(maxLength, static_cast<qint64>(freeBytes)));
285 if (toWrite == 0)
286 return 0;
287 m_sink->writeToRingBuffer(data, toWrite);
288 return toWrite;
289 }
290};
291
292// ---------------------------------------------------------------------------
293// QWasmAudioSink
294// ---------------------------------------------------------------------------
295
296QWasmAudioSink::QWasmAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
297 : QPlatformAudioSink(std::move(device), format, parent)
298{
299 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
300 m_deviceId = m_audioDevice.id().toStdString();
301 // "System audiooutput/audioinput" is a Qt-internal fallback ID used when the
302 // browser has not granted device permissions. It is not a valid browser sinkId,
303 // so map it to "" to request the default audio output device.
304 if (m_deviceId.compare(0, 7, "System ") == 0)
305 m_deviceId.clear();
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);
313}
314
316{
317 teardownPipeline();
318 if (!m_audioContext.isUndefined() && !m_audioContext.isNull()
319 && m_audioContext["state"].as<std::string>() != "closed") {
320 m_audioContext.call<emscripten::val>("close");
321 }
322}
323
324void QWasmAudioSink::start(QIODevice *device)
325{
326 Q_ASSERT(device);
327 Q_ASSERT(device->openMode().testFlag(QIODevice::ReadOnly));
328 m_device = device;
329 start(true); // pullMode
330}
331
333{
334 auto *sinkDevice = new QWasmAudioSinkDevice(this);
335 sinkDevice->open(QIODevice::WriteOnly);
336 m_device = sinkDevice;
337 start(false /*pullMode*/);
338 return sinkDevice;
339}
340
341void QWasmAudioSink::start(AudioCallback &&callback)
342{
343 m_audioCallback = std::move(callback);
344 start(false); // pushMode
345}
346
347void QWasmAudioSink::start(bool pullMode)
348{
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);
354 return;
355 }
356
357 m_pullMode = pullMode;
358 m_callbackId = ++s_nextId;
359 s_sinkRegistry.insert(m_callbackId, this);
360
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);
364
365 m_processed.store(0, std::memory_order_relaxed);
366#if QT_CONFIG(thread)
367 m_activeCallbackId.store(m_callbackId, std::memory_order_relaxed);
368 // m_audioContext is created once in the constructor and reused across mode changes.
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(),
381 m_callbackId);
382 } else {
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(),
393 m_callbackId);
394 connectWorklet();
395 }
396#else
397 if (!m_workletModuleLoaded) {
398 qt_st_sink_loadWorkletModule(m_audioContext.as_handle(), m_callbackId,
399 m_format.channelCount());
400 } else {
401 connectWorklet();
402 }
403#endif
404
405 m_audioContext.call<emscripten::val>("resume");
406}
407
408// ---------------------------------------------------------------------------
409// Connect worklet once the module has loaded
410// ---------------------------------------------------------------------------
411
412void QWasmAudioSink::connectWorklet()
413{
414 m_workletModuleLoaded = true;
415 m_running = true;
416#if QT_CONFIG(thread)
417 deliverData();
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"]);
423#else
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"]);
428 deliverData();
429#endif
430 emit stateChanged(QAudio::ActiveState);
431}
432
433// ---------------------------------------------------------------------------
434// Fill ring buffer from device/callback and deliver frames to worklet (ST only)
435// ---------------------------------------------------------------------------
436
437void QWasmAudioSink::deliverData()
438{
439 if (!m_running || m_suspended)
440 return;
441
442 if (m_audioContext["state"] == emscripten::val("suspended"))
443 m_audioContext.call<emscripten::val>("resume");
444
445 if (m_pullMode && m_device) {
446 const int freeBytes = static_cast<int>(bytesFree());
447 if (freeBytes > 0) {
448 QByteArray pullBuffer(freeBytes, Qt::Uninitialized);
449 const qint64 bytesRead = m_device->read(pullBuffer.data(), freeBytes);
450 if (bytesRead > 0)
451 writeToRingBuffer(pullBuffer.constData(), static_cast<int>(bytesRead));
452 }
453 } else if (m_audioCallback) {
454 int freeBytes = static_cast<int>(bytesFree());
455 freeBytes -= freeBytes % m_format.bytesPerFrame(); // keep m_writePos frame-aligned
456 if (freeBytes > 0) {
457 QByteArray callbackBuffer(freeBytes, Qt::Uninitialized);
458 // Pass volume=1.0f here: volume is applied by the worklet (threaded)
459 // or by deliverToWorklet() (single-threaded), not pre-applied here.
460 QtMultimediaPrivate::runAudioCallback(
461 *m_audioCallback,
462 QSpan<std::byte>(reinterpret_cast<std::byte *>(callbackBuffer.data()), freeBytes),
463 m_format,
464 1.0f);
465 writeToRingBuffer(callbackBuffer.constData(), freeBytes);
466 }
467 }
468
469#if !QT_CONFIG(thread)
470 deliverToWorklet();
471#endif
472}
473
474// ---------------------------------------------------------------------------
475// Single-threaded: drain ring buffer → convert PCM → post quanta to worklet
476// ---------------------------------------------------------------------------
477
478#if !QT_CONFIG(thread)
479void QWasmAudioSink::deliverToWorklet()
480{
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();
488
489 while (true) {
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)
494 break;
495
496 // Copy one quantum from the ring buffer into a local stack buffer.
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);
501 } else {
502 memcpy(quantumBuffer, m_ringBuffer.constData() + readPosition, tailBytes);
503 memcpy(quantumBuffer + tailBytes, m_ringBuffer.constData(), bytesPerFrame - tailBytes);
504 }
505 m_readPos.store((readPosition + bytesPerFrame) % ringBufferSize, std::memory_order_release);
506 m_processed += bytesPerFrame;
507
508 // Convert interleaved PCM to planar Float32 with volume applied.
509 float planar[128 * 8];
510 for (int i = 0; i < renderBlockFrames; ++i) {
511 for (int channel = 0; channel < numChannels; ++channel) {
512 float sample = 0.0f;
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;
517 break;
518 case QAudioFormat::Int16:
519 sample = reinterpret_cast<const qint16 *>(quantumBuffer)[sampleIndex] / 32767.0f;
520 break;
521 case QAudioFormat::Int32:
522 sample = reinterpret_cast<const qint32 *>(quantumBuffer)[sampleIndex] / 2147483647.0f;
523 break;
524 case QAudioFormat::Float:
525 sample = reinterpret_cast<const float *>(quantumBuffer)[sampleIndex];
526 break;
527 default:
528 break;
529 }
530 planar[channel * renderBlockFrames + i] = qBound(-1.0f, sample * volume, 1.0f);
531 }
532 }
533 qt_st_sink_postFrame(m_workletNode.as_handle(), numChannels, renderBlockFrames, planar);
534 }
535}
536#endif
537
538// ---------------------------------------------------------------------------
539// Ring buffer write helper (shared by push device, pull mode, callback mode)
540// ---------------------------------------------------------------------------
541
542void QWasmAudioSink::writeToRingBuffer(const char *data, int bytes)
543{
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);
549 } else {
550 memcpy(m_ringBuffer.data() + writePosition, data, tailBytes);
551 memcpy(m_ringBuffer.data(), data + tailBytes, bytes - tailBytes);
552 }
553 m_writePos.store((writePosition + bytes) % ringBufferSize, std::memory_order_release);
554#if QT_CONFIG(thread)
555 // Threaded: track bytes sent to ring buffer as a proxy for bytes played.
556 // The error is bounded by the ring buffer occupancy (~100ms).
557 m_processed.fetch_add(static_cast<quint64>(bytes), std::memory_order_relaxed);
558#endif
559}
560
561// ---------------------------------------------------------------------------
562// Control
563// ---------------------------------------------------------------------------
564
566{
567 if (!m_running)
568 return;
569 teardownPipeline();
570 if (!m_pullMode && m_device)
571 delete m_device;
572 m_device = nullptr;
573 emit stateChanged(QAudio::StoppedState);
574}
575
577{
578 teardownPipeline();
579 m_processed.store(0, std::memory_order_relaxed);
580 setError(QAudio::NoError);
581 emit stateChanged(QAudio::StoppedState);
582}
583
585{
586 if (!m_running || m_suspended)
587 return;
588 m_suspended = true;
589 m_audioContext.call<emscripten::val>("suspend");
590 emit stateChanged(QAudio::SuspendedState);
591}
592
594{
595 if (!m_running || !m_suspended)
596 return;
597 m_suspended = false;
598 m_audioContext.call<emscripten::val>("resume");
599 emit stateChanged(QAudio::ActiveState);
600}
601
603{
604 const int ringBufferSize = m_ringBuffer.size();
605 if (ringBufferSize == 0)
606 return 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); // -1 distinguishes full from empty
611}
612
613void QWasmAudioSink::setBufferSize(qsizetype value)
614{
615 if (!m_running)
616 m_bufferSize = value;
617}
618
620{
621 return m_bufferSize;
622}
623
625{
626 return m_format.durationForBytes(
627 static_cast<qint64>(m_processed.load(std::memory_order_relaxed)));
628}
629
631{
632 if (!m_running)
633 return QAudio::StoppedState;
634 if (m_suspended)
635 return QAudio::SuspendedState;
636 return QAudio::ActiveState;
637}
638
639void QWasmAudioSink::setVolume(float volume)
640{
641 QPlatformAudioEndpointBase::setVolume(volume);
642 m_volumeAtomic.store(volume, std::memory_order_relaxed);
643}
644
645void QWasmAudioSink::setError(QAudio::Error error)
646{
647 QPlatformAudioEndpointBase::setError(error);
648}
649
650// ---------------------------------------------------------------------------
651// Teardown
652// ---------------------------------------------------------------------------
653
654void QWasmAudioSink::teardownPipeline()
655{
656 if (m_callbackId) {
657 s_sinkRegistry.remove(m_callbackId);
658 qt_sink_clearWorklet(m_callbackId);
659 m_callbackId = 0;
660 }
661
662 m_running = false;
663 m_suspended = false;
664 m_audioCallback.reset();
665
666#if QT_CONFIG(thread)
667 // Invalidate the active ID so the current worklet sees a generation mismatch
668 // on its next process() call and returns false, deactivating itself.
669 m_activeCallbackId.store(0, std::memory_order_release);
670#endif
671
672 // Disconnect the node so audio stops immediately.
673 // The AudioContext stays alive and is reused on the next start().
674 // Do not suspend the threaded AudioContext here — the worklet deactivates
675 // itself via the activeCallbackId mismatch, and suspending causes an audible
676 // gap when teardown is immediately followed by a restart (e.g. channel change).
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");
682#endif
683 m_workletNode = emscripten::val::undefined();
684}
685
686QT_END_NAMESPACE
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 resume() override
void stop() override
void start(QIODevice *device) override
void suspend() 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)
void reset() 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