diff --git a/ChangeLog.md b/ChangeLog.md index 9ef5b78559ff0..f291cfbf93e8c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -161,6 +161,9 @@ See docs/process.md for more on how version tagging works. existence of `Buffer.from` which was added in v5.10.0. If it turns out there is still a need to support these older node versions we can add a polyfil under LEGACY_VM_SUPPORT (#14447). +- Added support for running Emscripten-compiled native code in AudioWorklets as + if they were regular pthreads. See `/tests/audioworklet/tone/` for a working + minimal example. 2.0.24 - 06/10/2021 ------------------- @@ -192,6 +195,9 @@ See docs/process.md for more on how version tagging works. - You can now explicitly request that an environment variable remain unset by setting its value in `ENV` to `undefined`. This is useful for variables, such as `LANG`, for which Emscripten normally provides a default value. +- Added support for running Emscripten-compiled native code in AudioWorklets as + if they were regular pthreads. See `/tests/audioworklet/tone/` for a working + minimal example. 2.0.23 - 05/26/2021 ------------------- diff --git a/emcc.py b/emcc.py index ae8a4cb574612..25f44cd07ed3e 100755 --- a/emcc.py +++ b/emcc.py @@ -146,7 +146,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'] COMPILE_ONLY_FLAGS = set(['--default-obj-ext']) @@ -297,6 +297,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)') @@ -1953,6 +1956,10 @@ def default_setting(name, new_default): '_pthread_testcancel', '_exit', ] + if settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET: + settings.EXPORTED_FUNCTIONS += [ + '_pthread_create', + ] settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [ '$exitOnMainThread', ] @@ -3380,7 +3387,6 @@ def modularize(): f.write(src) # Export using a UMD style export, or ES6 exports if selected - if settings.EXPORT_ES6: f.write('export default %s;' % settings.EXPORT_NAME) elif not settings.MINIMAL_RUNTIME: @@ -3393,6 +3399,12 @@ def modularize(): exports["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s; ''' % {'EXPORT_NAME': settings.EXPORT_NAME}) + # Store the export on the global scope for audio worklets + if settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET: + f.write(f'''if (typeof AudioWorkletGlobalScope === 'function') + globalThis["{settings.EXPORT_NAME}"] = {settings.EXPORT_NAME}; +''') + shared.configuration.get_temp_files().note(final_js) save_intermediate('modularized') diff --git a/src/library.js b/src/library.js index 9b655ab4f058d..9b3a6fc659127 100644 --- a/src/library.js +++ b/src/library.js @@ -3434,7 +3434,7 @@ LibraryManager.library = { $setWasmTableEntry__deps: ['$wasmTableMirror'], $setWasmTableEntry: function(idx, func) { wasmTable.set(idx, func); - wasmTableMirror[idx] = func; + wasmTableMirror[idx] = wasmTable.get(idx); }, $getWasmTableEntry__internal: true, diff --git a/src/library_pthread.js b/src/library_pthread.js index 255c60133c54f..5847b5f70534b 100644 --- a/src/library_pthread.js +++ b/src/library_pthread.js @@ -224,11 +224,45 @@ var LibraryPThread = { PThread.tlsInitFunctions[i](); } }, - // Loads the WebAssembly module into the given list of Workers. + // Builds the initial load message for the Worker + buildWorkerLoadMessage: function() { + return { + 'cmd': 'load', + // If the application main .js file was loaded from a Blob, then it is not possible + // to access the URL of the current script that could be passed to a Web Worker so that + // it could load up the same file. In that case, developer must either deliver the Blob + // object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can + // independently load up the same main application file. + 'urlOrBlob': Module['mainScriptUrlOrBlob'] +#if !EXPORT_ES6 + || _scriptDir +#endif + , +#if WASM2JS + // the polyfill WebAssembly.Memory instance has function properties, + // which will fail in postMessage, so just send a custom object with the + // property we need, the buffer + 'wasmMemory': { 'buffer': wasmMemory.buffer }, +#else // WASM2JS + 'wasmMemory': wasmMemory, +#endif // WASM2JS + 'wasmModule': wasmModule, +#if LOAD_SOURCE_MAP + 'wasmSourceMap': wasmSourceMap, +#endif +#if USE_OFFSET_CONVERTER + 'wasmOffsetConverter': wasmOffsetConverter, +#endif +#if MAIN_MODULE + 'dynamicLibraries': Module['dynamicLibraries'], +#endif + } + }, + // Sets up the message handler on the passed in worker // onFinishedLoading: A callback function that will be called once all of // the workers have been initialized and are // ready to host pthreads. - loadWasmModuleToWorker: function(worker, onFinishedLoading) { + setupWorkerMessageHandler: function(worker, onFinishedLoading) { worker.onmessage = function(e) { var d = e['data']; var cmd = d['cmd']; @@ -292,7 +326,8 @@ var LibraryPThread = { if (Module['onAbort']) { Module['onAbort'](d['arg']); } - } else { + } + else { err("worker sent an unknown command " + cmd); } PThread.currentProxiedOperationCallerThread = undefined; @@ -322,39 +357,6 @@ var LibraryPThread = { assert(wasmMemory instanceof WebAssembly.Memory, 'WebAssembly memory should have been loaded by now!'); assert(wasmModule instanceof WebAssembly.Module, 'WebAssembly Module should have been loaded by now!'); #endif - - // Ask the new worker to load up the Emscripten-compiled page. This is a heavy operation. - worker.postMessage({ - 'cmd': 'load', - // If the application main .js file was loaded from a Blob, then it is not possible - // to access the URL of the current script that could be passed to a Web Worker so that - // it could load up the same file. In that case, developer must either deliver the Blob - // object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can - // independently load up the same main application file. - 'urlOrBlob': Module['mainScriptUrlOrBlob'] -#if !EXPORT_ES6 - || _scriptDir -#endif - , -#if WASM2JS - // the polyfill WebAssembly.Memory instance has function properties, - // which will fail in postMessage, so just send a custom object with the - // property we need, the buffer - 'wasmMemory': { 'buffer': wasmMemory.buffer }, -#else // WASM2JS - 'wasmMemory': wasmMemory, -#endif // WASM2JS - 'wasmModule': wasmModule, -#if LOAD_SOURCE_MAP - 'wasmSourceMap': wasmSourceMap, -#endif -#if USE_OFFSET_CONVERTER - 'wasmOffsetConverter': wasmOffsetConverter, -#endif -#if MAIN_MODULE - 'dynamicLibraries': Module['dynamicLibraries'], -#endif - }); }, // Creates a new web Worker and places it in the unused worker pool to wait for its use. @@ -423,10 +425,115 @@ var LibraryPThread = { #endif #endif PThread.allocateUnusedWorker(); - PThread.loadWasmModuleToWorker(PThread.unusedWorkers[0]); + PThread.setupWorkerMessageHandler(PThread.unusedWorkers[0]); + + // Ask the new worker to load up the Emscripten-compiled page. This is a heavy operation. + PThread.unusedWorkers[0].postMessage(PThread.buildWorkerLoadMessage()); } return PThread.unusedWorkers.pop(); + }, + + busySpinWait: function(msecs) { + var t = performance.now() + msecs; + 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 + aw.addModule(pthreadMainJs).then(function() { + // Create a dummy worklet node that we use to establish the message channel + // This worklet is not conected anywhere so 'process' doesn't get called so it's + // not a performance overhead to have it instantiated + // + // NOTE: We pass in the 'load' message here in processorOptions because a recent + // Chrome change (v95+) broke `WebAssembly.Module` sending in postMessage below, but + // it still works if passed through `processorOptions` + var dummy = new AudioWorkletNode(audioCtx, 'pthread-dummy-processor', { + numberOfInputs: 0, + numberOfOutputs : 1, + outputChannelCount : [1], + processorOptions: PThread.buildWorkerLoadMessage() + }) + + // Push this node into the PThread internal worker pool so it + // gets picked up in _pthread_create below. + PThread.unusedWorkers.push(dummy); + + // Add postMessage directly on the object, forwarded to port.postMessage (emulates Worker) + dummy.postMessage = dummy.port.postMessage.bind(dummy.port); + + // We still call setupWorkerMessageHandler to setup the pthread environment, + // but we skip the actual script loading since it's already been done via + // addModule above. + PThread.setupWorkerMessageHandler(dummy); + + // Forward port.onMessage to onmessage on the object (emulates Worker) but + // add a few worklet-only messages + dummy.port.onmessage = function(e) { + var d = e['data']; + var cmd = d['cmd']; + if(cmd === 'addmodule') { + // This is sent from the worklet after the worker.js has processed the 'load' + // commands and needs to load the main js (which it can't do directly in + // AudioWorkletGlobalScope so we do it here from the main thread) + aw.addModule((Module['mainScriptUrlOrBlob'] || _scriptDir)).then(function() { + dummy.postMessage({'cmd': 'moduleloaded'}); + }); + } else if (cmd === 'running') { + // This is notified to let us know the pthread environment is ready and we can go ahead + // and create any pending AudioWorkletNodes + aw.pthread.resolveRunPromise(); + delete aw.pthread.resolveRunPromise; + } else { + // Pass it on to the regular worker onmessage + dummy.onmessage(e) + } + } + + // 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 = dummy; + }, 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. + createAudioWorkletNode: 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 }, $freeThreadData__noleakcheck: true, @@ -993,8 +1100,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 @@ -1031,10 +1138,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); @@ -1048,7 +1165,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 @@ -1057,12 +1174,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 @@ -1132,33 +1262,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 246fa488fca7d..6c9441dbc2565 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 265c13a474800..e501acc733e25 100644 --- a/src/settings.js +++ b/src/settings.js @@ -621,6 +621,7 @@ var LEGACY_VM_SUPPORT = 0; // '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. // This settings can be a comma-separated list of these environments, e.g., diff --git a/src/settings_internal.js b/src/settings_internal.js index 7a73d048d5694..58b2df387413c 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -141,6 +141,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. // Currently always off for MEMORY64. diff --git a/src/shell.js b/src/shell.js index 60c5382eb8be5..6d26d8e26ea15 100644 --- a/src/shell.js +++ b/src/shell.js @@ -21,6 +21,9 @@ // after the generated code, you will need to define var Module = {}; // before the code. Then that object will be used in the code, and you // can continue to use Module afterwards as well. +// If running as an audio worklet, pull the Module object from the global scope +// where it gets set up in worker.js that's loaded in the first addModule call +// and only shares the global scope with the main js #if USE_CLOSURE_COMPILER // if (!Module)` is crucial for Closure Compiler here as it will otherwise replace every `Module` occurrence with a string var /** @type {{ @@ -31,8 +34,18 @@ var /** @type {{ preloadResults: Object }} */ Module; +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET +if(typeof AudioWorkletGlobalScope === 'function') { + Module = globalThis.Module +} +#endif if (!Module) /** @suppress{checkTypes}*/Module = {"__EMSCRIPTEN_PRIVATE_MODULE_EXPORT_NAME_SUBSTITUTION__":1}; #else +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET +if(typeof AudioWorkletGlobalScope === 'function') { + var Module = globalThis.Module +} +#endif var Module = typeof {{{ EXPORT_NAME }}} !== 'undefined' ? {{{ EXPORT_NAME }}} : {}; #endif // USE_CLOSURE_COMPILER @@ -91,14 +104,16 @@ 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 // Attempt to auto-detect the environment var ENVIRONMENT_IS_WEB = typeof window === 'object'; var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; +var ENVIRONMENT_IS_AUDIOWORKLET = typeof AudioWorkletGlobalScope === 'function'; // N.b. Electron.js environment is simultaneously a NODE-environment, but // also a web environment. var ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_AUDIOWORKLET; #endif // ENVIRONMENT #if ASSERTIONS @@ -377,7 +392,13 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { } setWindowTitle = function(title) { document.title = title }; -} else +} +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET +else if (ENVIRONMENT_IS_AUDIOWORKLET) { + // Nothing for worklets! +} +#endif // ENVIRONMENT_MAY_BE_AUDIOWORKLET +else #endif // ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER { #if ASSERTIONS @@ -455,7 +476,7 @@ assert(typeof Module['TOTAL_MEMORY'] === 'undefined', 'Module.TOTAL_MEMORY has b {{{ makeRemovedRuntimeFunction('alignMemory') }}} #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 #if !ENVIRONMENT_MAY_BE_WEB diff --git a/src/shell_minimal.js b/src/shell_minimal.js index fd95c9fbd0f21..28bcedc976e17 100644 --- a/src/shell_minimal.js +++ b/src/shell_minimal.js @@ -140,7 +140,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 852af41f63b43..30378133f69f4 100644 --- a/src/worker.js +++ b/src/worker.js @@ -50,6 +50,42 @@ if (typeof process === 'object' && typeof process.versions === 'object' && typeo } #endif // ENVIRONMENT_MAY_BE_NODE +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET +if (typeof AudioWorkletGlobalScope === "function") { + 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); + + // Pass the initial message to the message handler as if it was sent via `sendMessage` + self.onmessage({ + data: arg.processorOptions + }); + } + + // 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) @@ -134,6 +170,17 @@ 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 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') }}}) { + postMessage({'cmd': 'addmodule'}); + return + } #endif #if MODULARIZE && EXPORT_ES6 @@ -174,6 +221,20 @@ self.onmessage = function(e) { #endif #endif #endif // MODULARIZE && EXPORT_ES6 +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + } else if (e.data.cmd === 'moduleloaded') { + #if MODULARIZE + #if MINIMAL_RUNTIME + {{{ EXPORT_NAME }}}(imports).then(function (instance) { + Module = instance; + }); + #else + {{{ EXPORT_NAME }}}(Module).then(function (instance) { + Module = instance; + }); + #endif + #endif +#endif // ENVIRONMENT_MAY_BE_AUDIOWORKLET } else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry // point. @@ -214,6 +275,12 @@ self.onmessage = function(e) { } #endif // EMBIND +#if ENVIRONMENT_MAY_BE_AUDIOWORKLET + if (Module['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, @@ -283,6 +350,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['__emscripten_thread_exit'](-1/*PTHREAD_CANCELED*/); diff --git a/system/lib/pthread/library_pthread.c b/system/lib/pthread/library_pthread.c index fdb5cbd31252d..1a7d9c61d56df 100644 --- a/system/lib/pthread/library_pthread.c +++ b/system/lib/pthread/library_pthread.c @@ -823,6 +823,10 @@ int _emscripten_call_on_thread(int forceAsync, pthread_t targetThread, EM_FUNC_S // 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; + void __emscripten_init_main_thread_js(void* tb); // See system/lib/README.md for static constructor ordering. diff --git a/tests/audioworklet/futex/audioworklet_futex.cpp b/tests/audioworklet/futex/audioworklet_futex.cpp new file mode 100644 index 0000000000000..c0ac17bbcc812 --- /dev/null +++ b/tests/audioworklet/futex/audioworklet_futex.cpp @@ -0,0 +1,100 @@ +// 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 + +// This test tests the futex implementation for audio worklets which is +// very similar to the main thread futex spin loop implementation since +// audio worklets don't support Atomics.Wait/Notify to keep them as +// non-blocking as possible + +pthread_t workletThreadId = 0; // This is set on the main thread when the pthread is created +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 +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. + // Blocking like 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; +} + +// Initializes the audio context and the pthread it it's AudioWorkletGlobalScope +EM_JS(uint32_t, init, (pthread_t* pthreadPtr), { + // Create the context + Module.audioCtx = new AudioContext(); + + // 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]); + }); + + // Creates an AudioWorkletNode and connects it to the output once it's created + PThread.createAudioWorkletNode( + Module.audioCtx, + 'test-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() +{ + 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') + init(&workletThreadId); + return 0; +} diff --git a/tests/audioworklet/futex/audioworklet_futex_post.js b/tests/audioworklet/futex/audioworklet_futex_post.js new file mode 100644 index 0000000000000..dcc2587b2b6b5 --- /dev/null +++ b/tests/audioworklet/futex/audioworklet_futex_post.js @@ -0,0 +1,17 @@ +// Register our audio processors if the code loads in an AudioWorkletGlobalScope +if (typeof AudioWorkletGlobalScope === "function") { + class TestProcessor extends AudioWorkletProcessor { + constructor() { + super(); + Module["_signalTestSuccess"](); + } + + // We need a dummy process method otherwise `registerProcessor` fails + process(inputs, outputs, parameters) { + return true; + } + } + + // Register the processor as per the audio worklet spec + registerProcessor('test-processor', TestProcessor); +} \ No newline at end of file diff --git a/tests/audioworklet/tone/audioworklet_tone.cpp b/tests/audioworklet/tone/audioworklet_tone.cpp new file mode 100644 index 0000000000000..0cfbd79a9c362 --- /dev/null +++ b/tests/audioworklet/tone/audioworklet_tone.cpp @@ -0,0 +1,116 @@ +// 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 + +// This is a simple example of using the emscripten audio worklet integration +// to run any native audio code. +// There are three components: +// 1) generateAudio is the native function that generates the audio samples +// 2) init is the JS-side audio worklet initialization that: +// - a) creates an AudioContext +// - b) initializes a PThread context in that AudioContext +// - c) creates an AudioWorkletNode running the 'native-passthrough-processor' that calls generateAudio from 1) +// 3) the NativePassthroughProcessor in audioworklet_tone_post.js that is the audio thread side implementation of 'native-passthrough-processor' +// +// Compile with: +// emcc -s USE_PTHREADS=1 -s ENVIRONMENT=web,worker,audioworklet --post-js=audioworklet_tone_post.js -o audioworklet_tone.html audioworklet_tone.cpp +// +// Or if using MODULARIZE=1 +// emcc -s USE_PTHREADS=1 -s MODULARIZE=1 -s EXPORT_NAME=MyModule -s ENVIRONMENT=web,worker,audioworklet --extern-post-js=audioworklet_tone_post.js --shell-file ../../shell_that_launches_modularize.html -o audioworklet_tone.html audioworklet_tone.cpp +// note the !!--extern-post-js!! in the MODULARIZE case - currently there is a Chromium implementation bug that fails to synchronize processor +// registrations to the main thread if the `registerProcessor` call doesn't execute during initial JS evaluation. So we need to make sure we +// put it outside the modularized part (--post-js will keep it inside) so it executes during initial eval and registers properly. +// See https://bugs.chromium.org/p/chromium/issues/detail?id=1218892 + +static pthread_t workletThreadId = 0; +static uint32_t sampleRate = 48000; +static float invSampleRate = 1.0f / sampleRate; + +// 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, init, (pthread_t* pthreadPtr), { + // Create the context + Module.audioCtx = new AudioContext(); + + // Initialize the pthread shared by all AudioWorkletNodes in this context + PThread.initAudioWorkletPThread(Module.audioCtx, pthreadPtr).then(function() { + out("Audio worklet PThread context initialized!") + }, function(err) { + out("Audio worklet PThread context initialization failed: " + [err, err.stack]); + }); + + // Creates an AudioWorkletNode and connects it to the output once it's created + PThread.createAudioWorkletNode( + 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 node created! Tap/click on the window if you don't hear audio!"); + workletNode.connect(Module.audioCtx.destination); + }, function(err) { + out("Audio worklet node creation failed: " + [err, err.stack]); + }); + + // 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 }; + window.addEventListener("touchstart", function() { Module.audioCtx.resume() }, opts); + window.addEventListener("mousedown", function() { Module.audioCtx.resume() }, opts); + window.addEventListener("keydown", function() { Module.audioCtx.resume() }, opts); + } + + return Module.audioCtx.sampleRate; +}); + +int main() +{ + sampleRate = init(&workletThreadId); + invSampleRate = 1.0f / sampleRate; + printf("Initialized audio context. Sample rate: %d. PThread init pending.\n", sampleRate); + return 0; +} diff --git a/tests/audioworklet/tone/audioworklet_tone_post.js b/tests/audioworklet/tone/audioworklet_tone_post.js new file mode 100644 index 0000000000000..8cbb73268dcc9 --- /dev/null +++ b/tests/audioworklet/tone/audioworklet_tone_post.js @@ -0,0 +1,38 @@ +/** + * 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. + // It calls the native function then copies the samples into the output buffer + class NativePassthroughProcessor extends AudioWorkletProcessor { + process(inputs, outputs, parameters) { + const output = outputs[0]; + 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/test_browser.py b/tests/test_browser.py index da00ae169afe5..893c85eac1dd0 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -5204,6 +5204,22 @@ 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): + for closure in ['--closure=0', '--closure=1']: + self.btest(path_from_root('tests', 'audioworklet', 'futex', 'audioworklet_futex.cpp'), + expected='1', + args=['-s', 'USE_PTHREADS=1', '-s', 'ENVIRONMENT=web,worker,audioworklet', closure, + '--post-js', path_from_root('tests', 'audioworklet', 'futex', 'audioworklet_futex_post.js')]) + self.btest(path_from_root('tests', 'audioworklet', 'futex', 'audioworklet_futex.cpp'), + expected='1', + args=['-s', 'USE_PTHREADS=1', '-s', 'MODULARIZE=1', '-s', + 'EXPORT_NAME=MyModule', '-s', 'ENVIRONMENT=web,worker,audioworklet', closure, + '--extern-post-js', path_from_root('tests', 'audioworklet', 'futex', 'audioworklet_futex_post.js'), + '--shell-file', test_file('shell_that_launches_modularize.html')]) + EMRUN = path_from_root('emrun') diff --git a/tests/third_party/posixtestsuite b/tests/third_party/posixtestsuite index 869e266b090a8..a5e7ff5f906c9 160000 --- a/tests/third_party/posixtestsuite +++ b/tests/third_party/posixtestsuite @@ -1 +1 @@ -Subproject commit 869e266b090a8d44e51bb040904c942556b1e4ab +Subproject commit a5e7ff5f906c9a8f18d70f6472c4055bc311c34d