diff --git a/emcc.py b/emcc.py index 24ed2ff0f74c8..0f337cc77c310 100755 --- a/emcc.py +++ b/emcc.py @@ -143,7 +143,7 @@ } -VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell') +VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell', 'audioworklet') SIMD_INTEL_FEATURE_TOWER = ['-msse', '-msse2', '-msse3', '-mssse3', '-msse4.1', '-msse4.2', '-mavx'] SIMD_NEON_FLAGS = ['-mfpu=neon'] @@ -286,6 +286,9 @@ def setup_environment_settings(): 'worker' in environments or \ (settings.ENVIRONMENT_MAY_BE_NODE and settings.USE_PTHREADS) + # Worklet environment must be enabled explicitly for now + settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET = 'audioworklet' in environments + if not settings.ENVIRONMENT_MAY_BE_WORKER and settings.PROXY_TO_WORKER: exit_with_error('If you specify --proxy-to-worker and specify a "-s ENVIRONMENT=" directive, it must include "worker" as a target! (Try e.g. -s ENVIRONMENT=web,worker)') @@ -3248,7 +3251,17 @@ def modularize(): with open(final_js, 'w') as f: f.write(src) - # Export using a UMD style export, or ES6 exports if selected + # Add ; if src doesn't contain it so we generate valid JS (otherwise acorn fails) + if src.rstrip()[-1] != ';': + f.write(';') + + # Store the export on the global scope for audio worklets + if settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET: + f.write('''if (typeof AudioWorkletGlobalScope === 'function') + globalThis["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s; +''' % { + 'EXPORT_NAME': settings.EXPORT_NAME + }) if settings.EXPORT_ES6: f.write('export default %s;' % settings.EXPORT_NAME) diff --git a/src/library_pthread.js b/src/library_pthread.js index 995aee7dbb1b8..b112dbe7f5753 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -403,14 +403,23 @@ var LibraryPThread = { PThread.receiveObjectTransfer(e.data); } else if (e.data.target === 'setimmediate') { worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation. - } else { + } +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + else if (cmd === 'running') { + // This is only notified for audio worklet contexts to let us know the pthread environment + // is ready and we can go ahead and create any pending AudioWorkletNodes + worker.context.audioWorklet.pthread.resolveRunPromise(); + delete worker.context.audioWorklet.pthread.resolveRunPromise; + } +#endif + else { err("worker sent an unknown command " + cmd); } PThread.currentProxiedOperationCallerThread = undefined; }; worker.onerror = function(e) { - err('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message); + err('pthread sent an error! ' + e.stack ? e.stack : (e.filename + ':' + e.lineno + ': ' + e.message)); }; #if ENVIRONMENT_MAY_BE_NODE @@ -523,7 +532,78 @@ var LibraryPThread = { while (performance.now() < t) { ; } + }, + +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + // Initializes a pthread in the AudioWorkletGlobalScope for this audio context + initAudioWorkletPThread: function(audioCtx, pthreadPtr) { + var aw = audioCtx.audioWorklet; + assert(!aw.pthread, "Can't call initAudioWorkletPThread twice on the same audio context"); + aw.pthread = {}; + + // First, load the code into the worklet context. The .worker.js first, then the main script. +#if MINIMAL_RUNTIME + var pthreadMainJs = Module['worker']; +#else + var pthreadMainJs = locateFile('{{{ PTHREAD_WORKER_FILE }}}'); +#endif + Promise.all([ + aw.addModule(pthreadMainJs), + aw.addModule(Module['mainScriptUrlOrBlob'] || _scriptDir) + ]).then(function() { + // Create a dummy worklet node that we use to establish the message channel + var dummyWorklet = new AudioWorkletNode(audioCtx, 'pthread-dummy-processor', { + numberOfInputs: 0, + numberOfOutputs : 1, + outputChannelCount : [1] + }) + + // Push this dummyWorklet into the PThread internal worker pool so it + // gets picked up in _pthread_create below. + PThread.unusedWorkers.push(dummyWorklet); + + // Add postMessage directly on the object, forwarded to port.postMessage (emulates Worker) + dummyWorklet.postMessage = dummyWorklet.port.postMessage.bind(dummyWorklet.port); + + // We still call loadWasmModuleToWorker to setup the pthread environment, + // but we skip the actual WASM loading since it's already been done via + // addModule above. + PThread.loadWasmModuleToWorker(dummyWorklet); + + // Forward port.onMessage to onmessage on the object (emulates Worker) + dummyWorklet.port.onmessage = dummyWorklet.onmessage; + + // Call pthread_create to have Emscripten init our worklet pthread globals as if + // it was a regular worker + _pthread_create(pthreadPtr, 0, 0, 0); + + aw.pthread.dummyWorklet = dummyWorklet; + }, function(err) { + aw.pthread.rejectRunPromise(err); + delete aw.pthread; + }); + + aw.pthread.runPromise = new Promise(function(resolve, reject) { + aw.pthread.resolveRunPromise = resolve; + aw.pthread.rejectRunPromise = reject; + }); + + return aw.pthread.runPromise; + }, + + // Creates an AudioWorkletNode on the specified audio context. + // initAudioWorkletPThread must've been called on the audio context before. + createAudioWorklet: function(audioCtx, processorName, processorOpts) { + assert(audioCtx.audioWorklet.pthread.runPromise, "Call initAudioWorkletPThread once before calling createAudioWorklet"); + return new Promise(function(resolve, reject) { + audioCtx.audioWorklet.pthread.runPromise.then(function() { + resolve(new AudioWorkletNode(audioCtx, processorName, processorOpts)); + }, function(err) { + reject(err); + }) + }); } +#endif }, $killThread: function(pthread_ptr) { @@ -1077,8 +1157,8 @@ var LibraryPThread = { emscripten_futex_wait__deps: ['emscripten_main_thread_process_queued_calls'], emscripten_futex_wait: function(addr, val, timeout) { if (addr <= 0 || addr > HEAP8.length || addr&3 != 0) return -{{{ cDefine('EINVAL') }}}; - // We can do a normal blocking wait anywhere but on the main browser thread. - if (!ENVIRONMENT_IS_WEB) { + // We can do a normal blocking wait anywhere but on the main browser thread or in an audio worklet. + if (!ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_AUDIOWORKLET) { #if PTHREADS_PROFILING PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}}); #endif @@ -1115,10 +1195,20 @@ var LibraryPThread = { // ourselves before calling the potentially-recursive call. See below for // how we handle the case of our futex being notified during the time in // between when we are not set as the value of __emscripten_main_thread_futex. + // + // For audio worklets we use the same global address since they all run on + // the audio thread. It's all very similar to the main thread case, except we + // don't have to do any nested call special casing. + var theFutex = __emscripten_main_thread_futex; +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + if (ENVIRONMENT_IS_AUDIOWORKLET) { + theFutex = __emscripten_audio_worklet_futex; + } +#endif #if ASSERTIONS - assert(__emscripten_main_thread_futex > 0); + assert(theFutex > 0); #endif - var lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, addr); + var lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, addr); #if ASSERTIONS // We must not have already been waiting. assert(lastAddr == 0); @@ -1132,7 +1222,7 @@ var LibraryPThread = { PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}}); #endif // We timed out, so stop marking ourselves as waiting. - lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, 0); + lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, 0); #if ASSERTIONS // The current value must have been our address which we set, or // in a race it was set to 0 which means another thread just allowed @@ -1141,12 +1231,25 @@ var LibraryPThread = { #endif return -{{{ cDefine('ETIMEDOUT') }}}; } + +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + if (ENVIRONMENT_IS_AUDIOWORKLET) { + // Audio worklet version without any special casing like for the main thread below + lastAddr = Atomics.load(HEAP32, theFutex >> 2); + if (lastAddr != addr) { + // We were told to stop waiting, so stop. + break; + } + continue; + } +#endif + // We are performing a blocking loop here, so we must handle proxied // events from pthreads, to avoid deadlocks. // Note that we have to do so carefully, as we may take a lock while // doing so, which can recurse into this function; stop marking // ourselves as waiting while we do so. - lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, 0); + lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, 0); #if ASSERTIONS assert(lastAddr == addr || lastAddr == 0); #endif @@ -1216,33 +1319,46 @@ var LibraryPThread = { // For Atomics.notify() API Infinity is to be passed in that case. if (count >= {{{ cDefine('INT_MAX') }}}) count = Infinity; - // See if main thread is waiting on this address? If so, wake it up by resetting its wake location to zero. - // Note that this is not a fair procedure, since we always wake main thread first before any workers, so + // See if any spinning thread is waiting on this address? If so, wake it up by resetting its wake location to zero. + // Note that this is not a fair procedure, since we always wake these threads first before any workers, so // this scheme does not adhere to real queue-based waiting. + // Spin-wait futexes are used on the main thread and in worklets due to the lack of Atomic.wait(). + var spinFutexesWoken = 0; + +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + var spinFutexes = [__emscripten_main_thread_futex, __emscripten_audio_worklet_futex]; + for (var i = 0; i < spinFutexes.length; i++) { + var theFutex = spinFutexes[i]; +#else + var theFutex = __emscripten_main_thread_futex; +#endif #if ASSERTIONS - assert(__emscripten_main_thread_futex > 0); + assert(theFutex > 0); #endif - var mainThreadWaitAddress = Atomics.load(HEAP32, __emscripten_main_thread_futex >> 2); - var mainThreadWoken = 0; - if (mainThreadWaitAddress == addr) { + var waitAddress = Atomics.load(HEAP32, theFutex >> 2); + + if (waitAddress == addr) { #if ASSERTIONS // We only use __emscripten_main_thread_futex on the main browser thread, where we // cannot block while we wait. Therefore we should only see it set from // other threads, and not on the main thread itself. In other words, the // main thread must never try to wake itself up! - assert(!ENVIRONMENT_IS_WEB); + assert(theFutex != __emscripten_main_thread_futex || !ENVIRONMENT_IS_WEB); #endif - var loadedAddr = Atomics.compareExchange(HEAP32, __emscripten_main_thread_futex >> 2, mainThreadWaitAddress, 0); - if (loadedAddr == mainThreadWaitAddress) { + var loadedAddr = Atomics.compareExchange(HEAP32, theFutex >> 2, waitAddress, 0); + if (loadedAddr == waitAddress) { --count; - mainThreadWoken = 1; + spinFutexesWoken += 1; if (count <= 0) return 1; } } +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + } // close out the spinFutexes loop +#endif // Wake any workers waiting on this address. var ret = Atomics.notify(HEAP32, addr >> 2, count); - if (ret >= 0) return ret + mainThreadWoken; + if (ret >= 0) return ret + spinFutexesWoken; throw 'Atomics.notify returned an unexpected value ' + ret; }, diff --git a/src/minimal_runtime_worker_externs.js b/src/minimal_runtime_worker_externs.js index d3807bb5f1f15..8481bdf6ba3a5 100644 --- a/src/minimal_runtime_worker_externs.js +++ b/src/minimal_runtime_worker_externs.js @@ -8,5 +8,6 @@ // This file should go away in the future when worker.js is refactored to live inside the JS module. var ENVIRONMENT_IS_PTHREAD; +var ENVIRONMENT_IS_AUDIOWORKLET; /** @suppress {duplicate} */ var wasmMemory; diff --git a/src/settings.js b/src/settings.js index ee1754b3dcbe0..70befada79f63 100644 --- a/src/settings.js +++ b/src/settings.js @@ -606,12 +606,13 @@ var LEGACY_VM_SUPPORT = 0; // in node.js, or in a JS shell like d8, js, or jsc. You can set this option to // specify that the output should only run in one particular environment, which // must be one of -// 'web' - the normal web environment. -// 'webview' - just like web, but in a webview like Cordova; -// considered to be same as "web" in almost every place -// 'worker' - a web worker environment. -// 'node' - Node.js. -// 'shell' - a JS shell like d8, js, or jsc. +// 'web' - the normal web environment. +// 'webview' - just like web, but in a webview like Cordova; +// considered to be same as "web" in almost every place +// 'worker' - a web worker environment. +// 'audioworklet' - an audio worklet environment. +// 'node' - Node.js. +// 'shell' - a JS shell like d8, js, or jsc. // Or it can be a comma-separated list of them, e.g., "web,worker". If this is // the empty string, then all runtime environments are supported. // diff --git a/src/settings_internal.js b/src/settings_internal.js index ac2ad10b624df..b3fc8da5e838d 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -139,6 +139,7 @@ var ENVIRONMENT_MAY_BE_WORKER = 1; var ENVIRONMENT_MAY_BE_NODE = 1; var ENVIRONMENT_MAY_BE_SHELL = 1; var ENVIRONMENT_MAY_BE_WEBVIEW = 1; +var ENVIRONMENT_MAY_BE_AUDIOWORKLET = 1; // Whether to minify import and export names in the minify_wasm_js stage. var MINIFY_WASM_IMPORTS_AND_EXPORTS = 0; diff --git a/src/shell.js b/src/shell.js index 872ff733588a7..27966828ddc93 100644 --- a/src/shell.js +++ b/src/shell.js @@ -91,17 +91,20 @@ var ENVIRONMENT_IS_WORKER = {{{ ENVIRONMENT === 'worker' }}}; #endif var ENVIRONMENT_IS_NODE = {{{ ENVIRONMENT === 'node' }}}; var ENVIRONMENT_IS_SHELL = {{{ ENVIRONMENT === 'shell' }}}; +var ENVIRONMENT_IS_AUDIOWORKLET = {{{ ENVIRONMENT === 'worklet' }}}; #else // ENVIRONMENT var ENVIRONMENT_IS_WEB = false; var ENVIRONMENT_IS_WORKER = false; var ENVIRONMENT_IS_NODE = false; var ENVIRONMENT_IS_SHELL = false; +var ENVIRONMENT_IS_AUDIOWORKLET = false; ENVIRONMENT_IS_WEB = typeof window === 'object'; ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; +ENVIRONMENT_IS_AUDIOWORKLET = typeof AudioWorkletGlobalScope === 'function'; // N.b. Electron.js environment is simultaneously a NODE-environment, but // also a web environment. ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'; -ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; +ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_AUDIOWORKLET; #endif // ENVIRONMENT #if ASSERTIONS @@ -343,6 +346,8 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { } setWindowTitle = function(title) { document.title = title }; +} else if (ENVIRONMENT_IS_AUDIOWORKLET) { + // Nothing for worklets! } else #endif // ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER { @@ -406,7 +411,7 @@ assert(typeof Module['TOTAL_MEMORY'] === 'undefined', 'Module.TOTAL_MEMORY has b {{{ makeRemovedFSAssert('NODEFS') }}} #if USE_PTHREADS -assert(ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)'); +assert(ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE || ENVIRONMENT_IS_AUDIOWORKLET, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)'); #endif // USE_PTHREADS #endif // ASSERTIONS diff --git a/src/shell_minimal.js b/src/shell_minimal.js index 93564e0503fb6..d9e5f7583abd7 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -138,7 +138,9 @@ var _scriptDir = (typeof document !== 'undefined' && document.currentScript) ? d // MINIMAL_RUNTIME does not support --proxy-to-worker option, so Worker and Pthread environments // coincide. -var ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_PTHREAD = typeof importScripts === 'function'; +var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; +var ENVIRONMENT_IS_AUDIOWORKLET = typeof AudioWorkletGlobalScope === 'function'; +var ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_AUDIOWORKLET; var currentScriptUrl = typeof _scriptDir !== 'undefined' ? _scriptDir : ((typeof document !== 'undefined' && document.currentScript) ? document.currentScript.src : undefined); #endif // USE_PTHREADS diff --git a/src/worker.js b/src/worker.js index 83e78e9826c4e..08d29104a6d07 100644 --- a/src/worker.js +++ b/src/worker.js @@ -50,6 +50,44 @@ if (typeof process === 'object' && typeof process.versions === 'object' && typeo } #endif // ENVIRONMENT_MAY_BE_NODE +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET +if (typeof AudioWorkletGlobalScope === "function") { + var oldRegisterProcessor = registerProcessor; + registerProcessor = function(name, cls) { + console.log(`register processor ${name}`) + globalThis[name] = cls; + oldRegisterProcessor(name, cls); + } + + Object.assign(globalThis, { + Module: Module, + self: globalThis, // Unlike DedicatedWorkerGlobalScope, AudioWorkletGlobalScope doesn't have 'self' + performance: { // Polyfill performance.now() since it's missing in worklets, falling back to Date.now() + start: Date.now(), + now: function() { + return Date.now() - performance.start; + } + } + }); + + // Create a dummy processor which we use to bootstrap PThreads in the worklet context + class PThreadDummyProcessor extends AudioWorkletProcessor { + constructor(arg) { + super(); + // Make message passing work directly on the global scope to simulate what regular + // Workers do which makes all the code above work the same. + this.port.onmessage = self.onmessage; + self.postMessage = this.port.postMessage.bind(this.port); + } + + // Needs a dummy process method too otherwise 'registerProcessor' fails below + process(inputs, outputs, parameters) {} + } + + registerProcessor('pthread-dummy-processor', PThreadDummyProcessor); +} +#endif + // Thread-local: #if EMBIND var initializedJS = false; // Guard variable for one-time init of the JS state (currently only embind types registration) @@ -165,8 +203,13 @@ self.onmessage = function(e) { #if !MINIMAL_RUNTIME || MODULARIZE {{{ makeAsmImportsAccessInPthread('ENVIRONMENT_IS_PTHREAD') }}} = true; +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + {{{ makeAsmImportsAccessInPthread('ENVIRONMENT_IS_AUDIOWORKLET') }}} = typeof AudioWorkletGlobalScope === "function"; +#endif #endif + + #if MODULARIZE && EXPORT_ES6 (e.data.urlOrBlob ? import(e.data.urlOrBlob) : import('./{{{ TARGET_JS_NAME }}}')).then(function(exports) { return exports.default(Module); @@ -175,6 +218,10 @@ self.onmessage = function(e) { moduleLoaded(); }); #else +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + // When running as an AudioWorklet all the scripts are imported from the main thread (via .addModule) + if(!{{{ makeAsmImportsAccessInPthread('ENVIRONMENT_IS_AUDIOWORKLET') }}}) { +#endif if (typeof e.data.urlOrBlob === 'string') { importScripts(e.data.urlOrBlob); } else { @@ -182,6 +229,9 @@ self.onmessage = function(e) { importScripts(objectUrl); URL.revokeObjectURL(objectUrl); } +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + } +#endif #if MODULARIZE #if MINIMAL_RUNTIME {{{ EXPORT_NAME }}}(imports).then(function (instance) { @@ -246,6 +296,12 @@ self.onmessage = function(e) { } #endif // EMBIND +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + if ({{{ makeAsmImportsAccessInPthread('ENVIRONMENT_IS_AUDIOWORKLET') }}}) { + // Audio worklets don't run any entrypoint since their entry points are the 'process' function invocations + postMessage({'cmd': 'running'}); + } else { +#endif try { // pthread entry points are always of signature 'void *ThreadMain(void *arg)' // Native codebases sometimes spawn threads with other thread entry point signatures, @@ -315,6 +371,9 @@ self.onmessage = function(e) { #endif } } +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + } +#endif } else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread. if (Module['_pthread_self']()) { Module['PThread'].threadCancel(); diff --git a/system/lib/pthread/library_pthread.c b/system/lib/pthread/library_pthread.c index bad5af3a7fa69..0e00d7a5271b2 100644 --- a/system/lib/pthread/library_pthread.c +++ b/system/lib/pthread/library_pthread.c @@ -916,6 +916,10 @@ int _emscripten_call_on_thread( // the main thread is waiting, we wake it up before waking up any workers. EMSCRIPTEN_KEEPALIVE void* _emscripten_main_thread_futex; +// Stores the memory address that audio worklets are waiting on, if any. If +// a worklet is waiting, we wake it up before waking up any workers. +EMSCRIPTEN_KEEPALIVE void* _emscripten_audio_worklet_futex; + static int _main_argc; static char** _main_argv; diff --git a/tests/audioworklet/audioworklet.cpp b/tests/audioworklet/audioworklet.cpp new file mode 100644 index 0000000000000..211e2070ce21c --- /dev/null +++ b/tests/audioworklet/audioworklet.cpp @@ -0,0 +1,190 @@ +// Copyright 2015 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +static pthread_t workletThreadId = 0; // This is set on the main thread when the pthread is created +static uint32_t sampleRate = 48000; +static float invSampleRate = 1.0f / sampleRate; + +/////////////////////////////////////////////////////////////////////////////// +// Section 1 - Test-only code +// +// This is just the testing code used to verify the pthread setup in the audio +// worklet. This can be ignored if you're just wanting to see how to implement +// native code audio worklets with Emscripten. +/////////////////////////////////////////////////////////////////////////////// + +std::atomic pthreadIdFromWorklet(0); // This is set from the audio thread when the AudioWorkletNode is constructed +std::mutex testMutex; // A test mutex to test the worklet futex implementation + +// Sends the current pthread id from the audio worklet node, signalling test success +// This happens immediately in the worklet node constructor since 'process' is never +// invoked without user interaction +EMSCRIPTEN_KEEPALIVE extern "C" void signalTestSuccess() { + pthread_t threadId = pthread_self(); + pthreadIdFromWorklet.store(threadId); + + printf("Audio thread time: %f\n", emscripten_get_now()); + + // Put the worklet to sleep until the main thread sees the above store. + // NOTE: This is *really bad* and should never be done in an audio worklet, but + // is here purely to test the worklet futex implementation which is sometimes needed. + testMutex.lock(); + testMutex.unlock(); +} + +// The main loop keeps going until 'pthreadIdFromWorklet' is sent from the AudioWorkletNode +EM_BOOL mainLoop(double time, void* userdata) { + pthread_t threadId = pthreadIdFromWorklet.load(); + if(threadId != 0) { + printf("Main thread time: %f\n", emscripten_get_now()); + + if(threadId == workletThreadId) { + printf("Sucesss! Got pthread id: %lu, expected %lu\n", threadId, workletThreadId); + #ifdef REPORT_RESULT + REPORT_RESULT(1); + #endif + } else { + printf("Failed! Got wrong pthread id: %lu, expected %lu\n", threadId, workletThreadId); + #ifdef REPORT_RESULT + REPORT_RESULT(-100); + #endif + } + + testMutex.unlock(); + return EM_FALSE; + } + + return EM_TRUE; +} + +EMSCRIPTEN_KEEPALIVE extern "C" void beforeUnload() { + // Always release the mutex on unload, otherwise the worklet can get stuck if we navigate away + testMutex.unlock(); +} + +EM_JS(void, setupUnloadHandler, (), { + window.addEventListener("beforeunload", function() { + _beforeUnload(); + }); +}); + +/////////////////////////////////////////////////////////////////////////////// +// Section 2 - Audio worklet example +// +// This is the relevant example code showing how to use native code to render +// audio in an audio worklet almost the same way as if it was a regular pthread +/////////////////////////////////////////////////////////////////////////////// + +// This is the native code audio generator - it outputs an interleaved stereo buffer +// containing a simple, continuous sine wave. +EMSCRIPTEN_KEEPALIVE extern "C" float* generateAudio(unsigned int numSamples) { + assert(numSamples == 128); // Audio worklet quantum size is always 128 + static float outputBuffer[128*2]; // This is where we generate our data into + static float wavePos = 0; // This is the generator wave position [0, 2*PI) + const float PI2 = 3.14159f * 2.0f; // Very approximate :) + const float FREQ = 440.0f; // Sine frequency + const float MAXAMP = 0.2f; // 20% so it's not too loud + + float* out = outputBuffer; + while(numSamples > 0) { + // Amplitude at current position + float a = sinf(wavePos) * MAXAMP; + + // Advance position, keep it in [0, 2*PI) range to avoid running out of float precision + wavePos += invSampleRate * FREQ * PI2; + if(wavePos > PI2) { + wavePos -= PI2; + } + + // Set both left and right samples to the same value + out[0] = a; + out[1] = a; + out += 2; + + numSamples -= 1; + } + + return outputBuffer; +} + +// Initializes the audio context and the pthread it it's AudioWorkletGlobalScope +EM_JS(uint32_t, initAudioContext, (pthread_t* pthreadPtr), { + // Create the context + Module.audioCtx = new AudioContext(); + + // To make this example usable we setup a resume on user interaction as browsers + // all require the user to interact with the page before letting audio play + if (window && window.addEventListener) { + var opts = { capture: true, passive : true }; + + var resume = function () { + Module.audioCtx.resume(); + window.removeEventListener("touchstart", resume, opts); + window.removeEventListener("mousedown", resume, opts); + window.removeEventListener("keydown", resume, opts); + }; + + window.addEventListener("touchstart", resume, opts); + window.addEventListener("mousedown", resume, opts); + window.addEventListener("keydown", resume, opts); + } + + // Initialize the pthread shared by all AudioWorkletNodes in this context + PThread.initAudioWorkletPThread(Module.audioCtx, pthreadPtr).then(function() { + out("PThread context initialized!") + }, function(err) { + out("PThread context initialization failed: " + [err, err.stack]); + }); + + return Module.audioCtx.sampleRate; +}); + +// Creates an AudioWorkletNode and connects it to the output once it's created +EM_JS(void, createAudioWorklet, (), { + PThread.createAudioWorklet( + Module.audioCtx, + 'native-passthrough-processor', + { + numberOfInputs: 0, + numberOfOutputs : 1, + outputChannelCount : [2] + } + ).then(function(workletNode) { + // Connect the worklet to the audio context output + out("Audio worklet created!"); + workletNode.connect(Module.audioCtx.destination); + }, function(err) { + out("Audio worklet creation failed: " + [err, err.stack]); + }); +}); + + +int main() +{ + // Section 1 - test-only code, ignore + setupUnloadHandler(); + emscripten_request_animation_frame_loop(mainLoop, nullptr); + testMutex.lock(); // Lock the mutex so that the worklet thread can wait on it (see comment in 'signalTestSuccess') + + // Section 2 - audio worklet example + sampleRate = initAudioContext(&workletThreadId); + invSampleRate = 1.0f / sampleRate; + printf("Initialized audio context. Sample rate: %d. PThread init pending.\n", sampleRate); + createAudioWorklet(); + printf("Creating audio worklet.\n"); +} diff --git a/tests/audioworklet/audioworklet_post.js b/tests/audioworklet/audioworklet_post.js new file mode 100644 index 0000000000000..5e4dc7e7ca815 --- /dev/null +++ b/tests/audioworklet/audioworklet_post.js @@ -0,0 +1,47 @@ +/** + * This is the JS side of the AudioWorklet processing that creates our + * AudioWorkletProcessor that fetches the audio data from native code and + * copies it into the output buffers. + * + * This is intentionally not made part of Emscripten AudioWorklet integration + * because apps will usually want to a lot of control here (formats, channels, + * additional processors etc.) + */ + +// Register our audio processors if the code loads in an AudioWorkletGlobalScope +if (typeof AudioWorkletGlobalScope === "function") { + // This processor node is a simple proxy to the audio generator in native code. + class NativePassthroughProcessor extends AudioWorkletProcessor { + constructor() { + super(); + + // Test-only code + // Signal test completion if we made it to here + Module["_signalTestSuccess"](); + } + + process(inputs, outputs, parameters) { + const output = outputs[0]; + + const numChannels = output.length; + const numSamples = output[0].length; + + // Run the native audio generator function + const mem = Module["_generateAudio"](numSamples); + + // Copy the results into the output buffer, float-by-float deinterleaving the data + let curSrc = mem/4; + const chL = output[0]; + const chR = output[1]; + for (let s = 0; s < numSamples; ++s) { + chL[s] = Module.HEAPF32[curSrc++]; + chR[s] = Module.HEAPF32[curSrc++]; + } + + return true; + } + } + + // Register the processor as per the audio worklet spec + registerProcessor('native-passthrough-processor', NativePassthroughProcessor); +} \ No newline at end of file diff --git a/tests/audioworklet/shell.html b/tests/audioworklet/shell.html new file mode 100644 index 0000000000000..3738f34e860a0 --- /dev/null +++ b/tests/audioworklet/shell.html @@ -0,0 +1,1305 @@ + + + + + + Emscripten-Generated Code + + + + + image/svg+xml + + +
+
Downloading...
+ + + Resize canvas + Lock/hide mouse pointer     + + + + +
+ +
+ + +
+ +
+ + + + {{{ SCRIPT }}} + + + + + diff --git a/tests/test_browser.py b/tests/test_browser.py index 999553a79c2c5..660ea6754c2ef 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -5103,6 +5103,17 @@ def test_assert_failure(self): def test_full_js_library_strict(self): self.btest_exit(test_file('hello_world.c'), args=['-sINCLUDE_FULL_LIBRARY', '-sSTRICT_JS']) + # Tests audio worklets + @requires_threads + @requires_sound_hardware + def test_audio_worklet(self): + self.btest(path_from_root('tests', 'audioworklet', 'audioworklet.cpp'), + expected='1', + args=['-s', 'USE_PTHREADS=1', '-s', 'MODULARIZE=1', '-s', + 'EXPORT_NAME=AudioWorkletSample', '-s', 'ENVIRONMENT=web,worker,audioworklet', + '--extern-post-js', path_from_root('tests', 'audioworklet', 'audioworklet_post.js'), + '--shell-file', path_from_root('tests', 'audioworklet', 'shell.html')]) + EMRUN = path_from_root('emrun')