diff --git a/js/AudioWorkletGlobalScope.js b/js/AudioWorkletGlobalScope.js index e07d2ac..d16df4c 100644 --- a/js/AudioWorkletGlobalScope.js +++ b/js/AudioWorkletGlobalScope.js @@ -15,6 +15,7 @@ const { sampleRate, } = workerData; +const kWorkletQueueTask = Symbol.for('node-web-audio-api:worklet-queue-task'); const kWorkletCallableProcess = Symbol.for('node-web-audio-api:worklet-callable-process'); const kWorkletInputs = Symbol.for('node-web-audio-api:worklet-inputs'); const kWorkletOutputs = Symbol.for('node-web-audio-api:worklet-outputs'); @@ -104,6 +105,10 @@ globalThis.AudioWorkletProcessor = class AudioWorkletProcessor { return this.#port; } + + [kWorkletQueueTask](cmd) { + this.#port.postMessage({ cmd }); + } } // follow algorithm from: diff --git a/js/AudioWorkletNode.js b/js/AudioWorkletNode.js index fda761b..b906290 100644 --- a/js/AudioWorkletNode.js +++ b/js/AudioWorkletNode.js @@ -16,6 +16,12 @@ const { const { kEnumerableProperty, } = require('./lib/utils.js'); +const { + propagateEvent, +} = require('./lib/events.js'); +const { + ErrorEvent, +} = require('./Events.js'); /* eslint-enable no-unused-vars */ @@ -212,6 +218,19 @@ module.exports = (jsExport, nativeBinding) => { parsedOptions, napiObj.id, ); + + this.#port.on('message', event => { + // ErrorEvent named processorerror + switch (event.cmd) { + case 'node-web-audio-api:worklet:invalid-process': { + const err = new ErrorEvent('processorerror', { + message: `Failed to execute 'process' on 'AudioWorkletNode': Invalid 'process' method` + }); + propagateEvent(this, err); + break; + } + } + }); } get parameters() { diff --git a/js/Events.js b/js/Events.js index 24bc554..f420507 100644 --- a/js/Events.js +++ b/js/Events.js @@ -30,7 +30,6 @@ Object.defineProperties(OfflineAudioCompletionEvent.prototype, { configurable: true, value: 'OfflineAudioCompletionEvent', }, - renderedBuffer: kEnumerableProperty, }); @@ -78,7 +77,6 @@ Object.defineProperties(AudioProcessingEvent.prototype, { configurable: true, value: 'AudioProcessingEvent', }, - playbackTime: kEnumerableProperty, inputBuffer: kEnumerableProperty, outputBuffer: kEnumerableProperty, @@ -135,13 +133,98 @@ Object.defineProperties(AudioRenderCapacityEvent.prototype, { configurable: true, value: 'AudioRenderCapacityEvent', }, - timestamp: kEnumerableProperty, averageLoad: kEnumerableProperty, peakLoad: kEnumerableProperty, underrunRatio: kEnumerableProperty, }); +// https://html.spec.whatwg.org/multipage/webappapis.html#errorevent +// interface ErrorEvent : Event { +// constructor(DOMString type, optional ErrorEventInit eventInitDict = {}); + +// readonly attribute DOMString message; +// readonly attribute USVString filename; +// readonly attribute unsigned long lineno; +// readonly attribute unsigned long colno; +// readonly attribute any error; +// }; + +// dictionary ErrorEventInit : EventInit { +// DOMString message = ""; +// USVString filename = ""; +// unsigned long lineno = 0; +// unsigned long colno = 0; +// any error; +// }; +class ErrorEvent extends Event { + #message = ''; + #filename = ''; + #lineno = 0; + #colno = 0; + #error = undefined; + + constructor(type, eventInitDict = {}) { + super(type); + + if (eventInitDict && typeof eventInitDict.message === 'string') { + this.#message = eventInitDict.message; + } + + if (eventInitDict && typeof eventInitDict.filename === 'string') { + this.#filename = eventInitDict.filename; + } + + if (eventInitDict && Number.isFinite(eventInitDict.lineno)) { + this.#lineno = eventInitDict.lineno; + } + + if (eventInitDict && Number.isFinite(eventInitDict.colno)) { + this.#colno = eventInitDict.colno; + } + + if (eventInitDict && eventInitDict.error instanceof Error) { + this.#error = eventInitDict.error; + } + } + + get message() { + return this.#message; + } + + get filename() { + return this.#filename; + } + + get lineno() { + return this.#lineno; + } + + get colno() { + return this.#colno; + } + + get error() { + return this.#error; + } +} + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + __proto__: null, + writable: false, + enumerable: false, + configurable: true, + value: 'ErrorEvent', + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty, +}); + module.exports.OfflineAudioCompletionEvent = OfflineAudioCompletionEvent; module.exports.AudioProcessingEvent = AudioProcessingEvent; module.exports.AudioRenderCapacityEvent = AudioRenderCapacityEvent; +module.exports.ErrorEvent = ErrorEvent; diff --git a/src/audio_worklet_node.rs b/src/audio_worklet_node.rs index 18b0e99..cca96b8 100644 --- a/src/audio_worklet_node.rs +++ b/src/audio_worklet_node.rs @@ -156,69 +156,106 @@ fn process_audio_worklet(env: &Env, args: ProcessorArguments) -> Result<()> { global.set_named_property("currentTime", current_time)?; global.set_named_property("currentFrame", current_frame)?; - let processor = processor.coerce_to_object()?; + let mut processor = processor.coerce_to_object()?; + + let k_worklet_callable_process = + get_symbol_for(env, "node-web-audio-api:worklet-callable-process"); + // return early if worklet has been tagged as not callable, + // @note - maybe this could be guaranteed on rust side + let callable_process = processor + .get_property::(k_worklet_callable_process)? + .get_value()?; + + if !callable_process { + let _ = tail_time_sender.send(false); + return Ok(()); + } - let k_worklet_inputs = get_symbol_for(env, "node-web-audio-api:worklet-inputs"); - let k_worklet_outputs = get_symbol_for(env, "node-web-audio-api:worklet-outputs"); - let k_worklet_params = get_symbol_for(env, "node-web-audio-api:worklet-params"); - let k_worklet_params_cache = get_symbol_for(env, "node-web-audio-api:worklet-params-cache"); + match processor.get_named_property::("process") { + Ok(process_method) => { + let k_worklet_inputs = get_symbol_for(env, "node-web-audio-api:worklet-inputs"); + let k_worklet_outputs = get_symbol_for(env, "node-web-audio-api:worklet-outputs"); + let k_worklet_params = get_symbol_for(env, "node-web-audio-api:worklet-params"); + let k_worklet_params_cache = + get_symbol_for(env, "node-web-audio-api:worklet-params-cache"); + + let js_inputs = processor.get_property::(k_worklet_inputs)?; + + for (input_number, input) in inputs.iter().enumerate() { + let mut channels = js_inputs.get_element::(input_number as u32)?; + + for (channel_number, channel) in input.iter().enumerate() { + let samples = + float_buffer_to_js(env, channel.as_ptr() as *mut _, channel.len()); + channels.set_element(channel_number as u32, samples)?; + } + + // delete remaining channels, if any + for i in input.len() as u32..channels.get_array_length().unwrap() { + channels.delete_element(i)?; + } + } - let js_inputs = processor.get_property::(k_worklet_inputs)?; + let js_outputs = processor.get_property::(k_worklet_outputs)?; - for (input_number, input) in inputs.iter().enumerate() { - let mut channels = js_inputs.get_element::(input_number as u32)?; + for (output_number, output) in outputs.iter().enumerate() { + let mut channels = js_outputs.get_element::(output_number as u32)?; - for (channel_number, channel) in input.iter().enumerate() { - let samples = float_buffer_to_js(env, channel.as_ptr() as *mut _, channel.len()); - channels.set_element(channel_number as u32, samples)?; - } + for (channel_number, channel) in output.iter().enumerate() { + let samples = + float_buffer_to_js(env, channel.as_ptr() as *mut _, channel.len()); + channels.set_element(channel_number as u32, samples)?; + } - // delete remaining channels, if any - for i in input.len() as u32..channels.get_array_length().unwrap() { - channels.delete_element(i)?; - } - } + // delete remaining channels, if any + for i in output.len() as u32..channels.get_array_length().unwrap() { + channels.delete_element(i)?; + } + } - let js_outputs = processor.get_property::(k_worklet_outputs)?; + let mut js_params = processor.get_property::(k_worklet_params)?; + let js_params_cache = + processor.get_property::(k_worklet_params_cache)?; + + // @perf - We could rely on the fact that ParameterDescriptors + // are ordered maps to avoid sending param names in `param_values` + for (name, data) in param_values.iter() { + let float32_arr_cache = js_params_cache.get_named_property::(name)?; + // retrieve right Float32Array according to actual param size, i.e. 128 or 1 + let cache_index = if data.len() == 1 { 1 } else { 0 }; + let float32_arr = float32_arr_cache.get_element::(cache_index)?; + // copy data into undeerlying ArrayBuffer + let mut array_buffer_value = float32_arr.into_value()?.arraybuffer.into_value()?; + let u8_slice = to_byte_slice(data); + array_buffer_value.copy_from_slice(u8_slice); + // get new owned value, as `float32_arr` as been consumed by `into_value` call + let float32_arr = float32_arr_cache.get_element::(cache_index)?; + js_params.set_named_property(name, float32_arr)?; + } - for (output_number, output) in outputs.iter().enumerate() { - let mut channels = js_outputs.get_element::(output_number as u32)?; + let js_ret: JsUnknown = + process_method.apply3(processor, js_inputs, js_outputs, js_params)?; + let ret = js_ret.coerce_to_bool()?.get_value()?; - for (channel_number, channel) in output.iter().enumerate() { - let samples = float_buffer_to_js(env, channel.as_ptr() as *mut _, channel.len()); - channels.set_element(channel_number as u32, samples)?; + let _ = tail_time_sender.send(ret); // allowed to fail } - - // delete remaining channels, if any - for i in output.len() as u32..channels.get_array_length().unwrap() { - channels.delete_element(i)?; + Err(_) => { + let k_worklet_queue_task = get_symbol_for(env, "node-web-audio-api:worklet-queue-task"); + // would be more usefull on rust side + let value = env.get_boolean(false)?; + processor.set_property(k_worklet_callable_process, value)?; + // @todo - set active source flag to false + // https://webaudio.github.io/web-audio-api/#active-source + let _ = tail_time_sender.send(false); + // [spec] Queue a task to the control thread fire an ErrorEvent + // named processorerror at the associated AudioWorkletNode. + let queue_task = + processor.get_property::(k_worklet_queue_task)?; + let cmd = env.create_string("node-web-audio-api:worklet:invalid-process")?; + queue_task.apply1(processor, cmd)?; } } - let mut js_params = processor.get_property::(k_worklet_params)?; - let js_params_cache = processor.get_property::(k_worklet_params_cache)?; - - // @perf - We could rely on the fact that ParameterDescriptors - // are ordered maps to avoid sending param names in `param_values` - for (name, data) in param_values.iter() { - let float32_arr_cache = js_params_cache.get_named_property::(name)?; - // retrieve right Float32Array according to actual param size, i.e. 128 or 1 - let cache_index = if data.len() == 1 { 1 } else { 0 }; - let float32_arr = float32_arr_cache.get_element::(cache_index)?; - // copy data into undeerlying ArrayBuffer - let mut array_buffer_value = float32_arr.into_value()?.arraybuffer.into_value()?; - let u8_slice = to_byte_slice(data); - array_buffer_value.copy_from_slice(u8_slice); - // get new owned value, as `float32_arr` as been consumed by `into_value` call - let float32_arr = float32_arr_cache.get_element::(cache_index)?; - js_params.set_named_property(name, float32_arr).unwrap(); - } - - let process_method = processor.get_named_property::("process")?; - let js_ret: JsUnknown = process_method.apply3(processor, js_inputs, js_outputs, js_params)?; - let ret = js_ret.coerce_to_bool()?.get_value()?; - let _ = tail_time_sender.send(ret); // allowed to fail - Ok(()) }