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);
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]);',
155 'registerProcessor("qt-audio-capture", QtCapture);'
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', {
169 channelCount: channelCount,
170 channelCountMode:
'explicit'
172 node.port.onmessage = function(e) {
173 Module._qtAudioData[instanceId].push(e.data);
174 Module._qt_onAudioFrameReady(instanceId);
176 return Emval.toHandle(node);
196 float volume, QAudioFormat::SampleFormat fmt,
int bytesPerSample,
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);
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);
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);
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;
242 qint64 readData(
char *data, qint64 maxlen)
override {
return m_source->readFromBuffer(data, maxlen); }
251 const QAudioFormat &fmt,
253 : QPlatformAudioSource(std::move(device), fmt, parent)
255 m_bufferSize = m_format.bytesForDuration(DEFAULT_BUFFER_DURATION);
272 dev->open(QIODevice::ReadOnly);
274 startPipeline(
false);
280 if (m_running || m_inputStream)
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);
291 m_pullMode = pullMode;
293 m_streamReady =
false;
294 m_workletReady =
false;
295 m_callbackId = ++s_nextId;
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);
304 m_inputStream =
new JsMediaInputStream(
this);
308 m_mediaStream = m_inputStream->getMediaStream();
309 m_streamReady =
true;
310 connectMediaStreamIfReady();
312 m_inputStream->setStreamDevice(m_audioDevice.id().toStdString());
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"));
321 attrs.set(
"sinkId", sinkId);
322 m_audioContext = emscripten::val::global(
"AudioContext").new_(attrs);
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(),
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"));
341 attrs.set(
"sinkId", sinkId);
342 m_audioContext = emscripten::val::global(
"AudioContext").new_(attrs);
343 qt_st_loadWorkletModule(m_audioContext.as_handle(), m_callbackId);
346 m_elapsedTimer.start();
354 deliverBufferedData();
357 m_device->deleteLater();
365 setError(QAudio::NoError);
371 m_bufferSize = value;
381 return m_format.durationForBytes(m_processed);
386 if (!m_running)
return QAudio::StoppedState;
387 if (m_suspended)
return QAudio::SuspendedState;
388 return QAudio::ActiveState;
393 QPlatformAudioSource::setVolume(vol);
395 m_volumeAtomic.store(vol, std::memory_order_relaxed);
401 if (!m_running || m_suspended)
404 m_audioContext.call<
void>(
"suspend");
409 if (!m_running || !m_suspended)
412 m_audioContext.call<
void>(
"resume");
417 if (!m_streamReady || !m_workletReady)
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);
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");
446 if (!m_running || !m_device || m_suspended)
450 const int avail =
static_cast<
int>(bytesReady());
454 const int ringSize = m_ringBuffer.size();
455 int rpos = m_readPos.load(std::memory_order_relaxed);
456 const int tail = ringSize - rpos;
458 m_device->write(m_ringBuffer.constData() + rpos, avail);
460 m_device->write(m_ringBuffer.constData() + rpos, tail);
461 m_device->write(m_ringBuffer.constData(), avail - tail);
463 m_processed += avail;
464 m_readPos.store((rpos + avail) % ringSize, std::memory_order_release);
466 emit m_device->readyRead();
469 float frameBuf[128 * 8];
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);
481 if (m_pendingData.isEmpty())
484 m_processed += m_pendingData.size();
485 m_device->write(m_pendingData);
486 m_pendingData.clear();
488 emit m_device->readyRead();
496 const int avail =
static_cast<
int>(bytesReady());
497 const int chunk =
static_cast<
int>(qMin(maxlen,
static_cast<qint64>(avail)));
500 const int ringSize = m_ringBuffer.size();
501 int rpos = m_readPos.load(std::memory_order_relaxed);
502 const int tail = ringSize - rpos;
504 memcpy(data, m_ringBuffer.constData() + rpos, chunk);
506 memcpy(data, m_ringBuffer.constData() + rpos, tail);
507 memcpy(data + tail, m_ringBuffer.constData(), chunk - tail);
509 m_processed += chunk;
510 m_readPos.store((rpos + chunk) % ringSize, std::memory_order_release);
513 const qint64 chunk = qMin(maxlen,
static_cast<qint64>(m_pendingData.size()));
516 memcpy(data, m_pendingData.constData(), chunk);
517 m_pendingData.remove(0, chunk);
518 m_processed += chunk;
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());
532 return static_cast<qsizetype>(m_pendingData.size());
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());
546 m_workletNode = emscripten::val::undefined();
547 if (!m_audioContext.isUndefined()) {
548 m_audioContext.call<
void>(
"close");
549 m_audioContext = emscripten::val::undefined();
552 m_running.store(
false, std::memory_order_release);
555 m_pendingData.clear();
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();
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)