7#include <emscripten/val.h>
12using emscripten::EM_VAL;
24 auto *src = s_registry.value(callbackId);
27 src->m_workletReady =
true;
28 src->connectMediaStreamIfReady();
33 if (
auto *src = s_registry.value(callbackId))
34 src->deliverBufferedData();
44constexpr int RING_BUFFER_DURATION = 100'000;
53EM_JS(
void, qt_loadWorkletModule,
55 int ringPtr,
int ringSize,
56 int wposPtr,
int volPtr,
57 int channels,
int fmt,
int bps,
59 if (!Module._qtWorkletParams) Module._qtWorkletParams = {};
60 Module._qtWorkletParams[callbackId] = {
71 'class QtCapture extends AudioWorkletProcessor {',
72 ' constructor(opts) {',
74 ' var options = opts.processorOptions;',
75 ' this._heap8 = new Int8Array(options.heap);',
76 ' this._heap16 = new Int16Array(options.heap);',
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;',
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;',
112 ' Atomics.store(this._heap32, this._writePositionIndex, writePos);',
113 ' this.port.postMessage(null);',
117 'registerProcessor("qt-audio-capture", QtCapture);'
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);
127EM_JS(
void, qt_mt_setupWorkletPort, (EM_VAL nodeHandle,
int callbackId), {
128 Emval.toValue(nodeHandle).port.onmessage = function() {
129 Module._qt_onAudioFrameReady(callbackId);
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]);',
159 'registerProcessor("qt-audio-capture", QtCapture);'
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', {
173 channelCount: channelCount,
174 channelCountMode:
'explicit'
176 node.port.onmessage = function(e) {
177 Module._qtAudioData[instanceId].push(e.data);
178 Module._qt_onAudioFrameReady(instanceId);
180 return Emval.toHandle(node);
205 qint64 readData(
char *data, qint64 maxlen)
override {
return m_source->readFromBuffer(data, maxlen); }
212 float volume, QAudioFormat::SampleFormat fmt,
int bytesPerSample,
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);
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);
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);
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;
254 const QAudioFormat &fmt,
256 : QPlatformAudioSource(std::move(device), fmt, parent)
258 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
275 dev->open(QIODevice::ReadOnly);
283 if (m_running || m_inputStream)
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);
294 m_pullMode = pullMode;
296 m_streamReady =
false;
297 m_workletReady =
false;
298 m_callbackId = ++s_nextId;
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);
307 m_inputStream =
new JsMediaInputStream(
this);
311 m_mediaStream = m_inputStream->getMediaStream();
312 m_streamReady =
true;
313 connectMediaStreamIfReady();
315 m_inputStream->setStreamDevice(m_audioDevice.id().toStdString());
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"));
324 attrs.set(
"sinkId", sinkId);
325 m_audioContext = emscripten::val::global(
"AudioContext").new_(attrs);
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(),
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"));
344 attrs.set(
"sinkId", sinkId);
345 m_audioContext = emscripten::val::global(
"AudioContext").new_(attrs);
346 qt_st_loadWorkletModule(m_audioContext.as_handle(), m_callbackId);
349 m_elapsedTimer.start();
357 deliverBufferedData();
360 m_device->deleteLater();
368 setError(QAudio::NoError);
374 m_bufferSize = value;
384 return m_format.durationForBytes(m_processed);
389 if (!m_running)
return QAudio::StoppedState;
390 if (m_suspended)
return QAudio::SuspendedState;
391 return QAudio::ActiveState;
396 QPlatformAudioSource::setVolume(vol);
398 m_volumeAtomic.store(vol, std::memory_order_relaxed);
404 if (!m_running || m_suspended)
407 m_audioContext.call<
void>(
"suspend");
412 if (!m_running || !m_suspended)
415 m_audioContext.call<
void>(
"resume");
420 if (!m_streamReady || !m_workletReady)
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);
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");
449 if (!m_running || !m_device || m_suspended)
453 const int avail =
static_cast<
int>(bytesReady());
457 const int ringSize = m_ringBuffer.size();
458 int rpos = m_readPos.load(std::memory_order_relaxed);
459 const int tail = ringSize - rpos;
461 m_device->write(m_ringBuffer.constData() + rpos, avail);
463 m_device->write(m_ringBuffer.constData() + rpos, tail);
464 m_device->write(m_ringBuffer.constData(), avail - tail);
466 m_processed += avail;
467 m_readPos.store((rpos + avail) % ringSize, std::memory_order_release);
469 emit m_device->readyRead();
472 float frameBuf[128 * 8];
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);
484 if (m_pendingData.isEmpty())
487 m_processed += m_pendingData.size();
488 m_device->write(m_pendingData);
489 m_pendingData.clear();
491 emit m_device->readyRead();
499 const int avail =
static_cast<
int>(bytesReady());
500 const int chunk =
static_cast<
int>(qMin(maxlen,
static_cast<qint64>(avail)));
503 const int ringSize = m_ringBuffer.size();
504 int rpos = m_readPos.load(std::memory_order_relaxed);
505 const int tail = ringSize - rpos;
507 memcpy(data, m_ringBuffer.constData() + rpos, chunk);
509 memcpy(data, m_ringBuffer.constData() + rpos, tail);
510 memcpy(data + tail, m_ringBuffer.constData(), chunk - tail);
512 m_processed += chunk;
513 m_readPos.store((rpos + chunk) % ringSize, std::memory_order_release);
516 const qint64 chunk = qMin(maxlen,
static_cast<qint64>(m_pendingData.size()));
519 memcpy(data, m_pendingData.constData(), chunk);
520 m_pendingData.remove(0, chunk);
521 m_processed += chunk;
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());
535 return static_cast<qsizetype>(m_pendingData.size());
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());
549 m_workletNode = emscripten::val::undefined();
550 if (!m_audioContext.isUndefined()) {
551 m_audioContext.call<
void>(
"close");
552 m_audioContext = emscripten::val::undefined();
555 m_running.store(
false, std::memory_order_release);
558 m_pendingData.clear();
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();
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
~QWasmAudioSource() 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)