diff --git a/.scripts/wpt-harness.js b/.scripts/wpt-harness.js index 434846cc..efe0b314 100644 --- a/.scripts/wpt-harness.js +++ b/.scripts/wpt-harness.js @@ -39,6 +39,7 @@ const ignoreCases = [ 'the-audio-api/the-iirfilternode-interface/iir-filter-silent-block-crash.html', 'the-audio-api/the-mediaelementaudiosourcenode-interface/mediaElementAudioSource-mediaStreamAudioDestination-stream-crash.html', 'the-audio-api/the-mediaelementaudiosourcenode-interface/mediaElementAudioSource_closed_context-crash.html', + 'the-audio-api/the-audioworklet-interface/process-parameters.https.html', ] // ------------------------------------------------------- diff --git a/js/AudioWorkletGlobalScope.js b/js/AudioWorkletGlobalScope.js index 2b7a42b6..06ccc6f5 100644 --- a/js/AudioWorkletGlobalScope.js +++ b/js/AudioWorkletGlobalScope.js @@ -10,24 +10,18 @@ import { AudioWorkletProcessor } from './AudioWorkletProcessor.js'; import { isIterable } from './lib/is-iterable.js'; import { isConstructor } from './lib/is-constructor.js'; import { - kWorkletCallableProcess, kWorkletMarkNonCallableProcess, - kWorkletInputs, - kWorkletOutputs, - kWorkletParams, - kWorkletParamsCache, kWorkletGetBuffer, kWorkletGetBuffer1, kWorkletRecycleBuffer, kWorkletRecycleBuffer1, kWorkletMarkAsUntransferable, - kWorkletUnpackProcess, } from './lib/audio-worklet/symbols.js'; import { - pendingProcessorConstructionData, -} from './lib/audio-worklet/pending-processor-construction-data.js' + pendingProcessor, +} from './lib/audio-worklet/pending-processor.js'; import { - BufferPool + BufferPool, } from './lib/audio-worklet/BufferPool.js'; import nativeBinding from '../load-native.js'; @@ -242,35 +236,57 @@ parentPort.on('message', async event => { const ctor = nameProcessorCtorMap.get(name); // entities of interest for the AudioWorkletProcess base class - pendingProcessorConstructionData.inner = { + pendingProcessor.constructionData = { messagePort, errorPort, numberOfInputs: options.numberOfInputs, numberOfOutputs: options.numberOfOutputs, parameterDescriptors: ctor.parameterDescriptors, + errored: null, }; - let instance; let errored = false; + let isMock = false; try { - instance = new ctor(options); + pendingProcessor.instance = new ctor(options); } catch (err) { - // if the given processor constructor failed, we create a dummy processor + errored = true; + + // If the given processor constructor failed, we create a dummy processor // that we mark immediately as non-callable. This prevents situations where // the NapiAudioWorkletProcessor, which already exists at this point, hangs // forever waiting for its JS counterpart // @todo - This design could be improved in the future by flagging somehow // the Rust processor to avoid the cross thread communication - errored = true; - instance = new AudioWorkletProcessor(options); - instance[kWorkletMarkNonCallableProcess](['node-web-audio-api:worklet:ctor-error', err]); + if (!pendingProcessor.instance) { + isMock = true; + // super may have been called but instance did throw, we can reset super + pendingProcessor.super = null; + pendingProcessor.instance = new AudioWorkletProcessor(options); + pendingProcessor.instance[kWorkletMarkNonCallableProcess]('node-web-audio-api:worklet:ctor-error', err); + } } - pendingProcessorConstructionData.inner = null; - // store in global so that Rust can match the JS processor - // with its corresponding NapiAudioWorkletProcessor - processors[`${id}`] = instance; + // Check that process method exists either on instance or on prototype. + // If execution of process fail for any reason, it will be catched in + // AudioWorkletProcessor::[kWorkletUnpackProcess] + // cf. wpt/webaudio/the-audio-api/the-audioworklet-interface/process-getter.https.html + // Do not use `hasOwnProperty` because we cannot assume that we know + // the prototype chain. + if (!isMock) { + if (!('process' in pendingProcessor.instance)) { + const err = new TypeError(`Invalid AudioWorkletNode "${pendingProcessor.instance.constructor.name}": Invalid "process" method`); + pendingProcessor.instance[kWorkletMarkNonCallableProcess]('node-web-audio-api:worklet:process-invalid', err); + } + } + + // Store in global to match the JS processor with its corresponding NapiAudioWorkletProcessor + processors[`${id}`] = pendingProcessor.instance; + + pendingProcessor.constructionData = null; + pendingProcessor.instance = null; + pendingProcessor.super = null; // notify main thread that instantiation has finished somehow if (errored) { parentPort.postMessage({ cmd: 'node-web-audio-api:worklet:ctor-error', id }); diff --git a/js/AudioWorkletProcessor.js b/js/AudioWorkletProcessor.js index d3150a25..8c47dfac 100644 --- a/js/AudioWorkletProcessor.js +++ b/js/AudioWorkletProcessor.js @@ -7,14 +7,11 @@ import { kWorkletParamsCache, kWorkletGetBuffer, kWorkletGetBuffer1, - kWorkletRecycleBuffer, - kWorkletRecycleBuffer1, - kWorkletMarkAsUntransferable, kWorkletUnpackProcess, } from './lib/audio-worklet/symbols.js'; import { - pendingProcessorConstructionData, -} from './lib/audio-worklet/pending-processor-construction-data.js' + pendingProcessor, +} from './lib/audio-worklet/pending-processor.js'; export class AudioWorkletProcessor { static get parameterDescriptors() { @@ -25,17 +22,26 @@ export class AudioWorkletProcessor { #errorPort = null; constructor() { + // check that this constructor has never been called for this processor instantiation + // cf. wpt/webaudio/the-audio-api/the-audioworklet-interface/processor-construction-port.https.html + if (pendingProcessor.super !== null) { + this[kWorkletCallableProcess] = false; + throw new TypeError(`Cannot construct "${this.constructor.name}": Invalid pending construction data`); + } + const { messagePort, errorPort, numberOfInputs, numberOfOutputs, parameterDescriptors, - } = pendingProcessorConstructionData.inner; + } = pendingProcessor.constructionData; this.#messagePort = messagePort; this.#errorPort = errorPort; + pendingProcessor.super = this; + // Mark [[callable process]] as true, set to false in render quantum // either if "process" does not exists or if it throws an error this[kWorkletCallableProcess] = true; @@ -46,8 +52,8 @@ export class AudioWorkletProcessor { // not for the reason explained try { // Populate with dummy values which will be replaced in first render call - this[kWorkletInputs] = new Array(numberOfInputs).fill([]); - this[kWorkletOutputs] = new Array(numberOfOutputs).fill([]); + this[kWorkletInputs] = Object.freeze(new Array(numberOfInputs).fill(Object.freeze([]))); + this[kWorkletOutputs] = Object.freeze( new Array(numberOfOutputs).fill(Object.freeze([]))); // Object to be reused as `process` parameters argument this[kWorkletParams] = {}; // Cache of 2 Float32Array (of length 128 and 1) for each param, to be reused on @@ -61,7 +67,7 @@ export class AudioWorkletProcessor { ]; } } catch (err) { - this[kWorkletMarkNonCallableProcess](['node-web-audio-api:worklet:ctor-error', err]); + this[kWorkletMarkNonCallableProcess]('node-web-audio-api:worklet:ctor-error', err); } } @@ -76,24 +82,31 @@ export class AudioWorkletProcessor { // Wrapper around the "real" process method that allows to // - unpack arguments from napi-rs `apply` // - cast return value to boolean - // - catch and cleanly return error so that rust can properly handle it - // - // This method is called only if a "real" process method has been found + // - catch and propagate error while keeping the rust side clean + // This method is called only if a "real" process attribute has been found at construction + // However if this is the first call we don't know yet if process is callable [kWorkletUnpackProcess]([inputs, outputs, parameters]) { - // output must be filled with zero - // cf. the-audioworklet-interface/audioworkletprocessor-unconnected-outputs.https.window.html - outputs.forEach(output => { - output.forEach(channel => channel.fill(0)); - }); - try { return !!this.process(inputs, outputs, parameters); } catch (err) { - return err; + // we can just mark the process as non callable here and return false + // (no need to return the error to rust and have another rust / js roundtrip) + let error; + // make sure we propagate an error instance, i.e. support `throw "my message";` + // @note that `Error.isError` is not available in Node v22 + if (!(err instanceof Error)) { + error = new Error(err); + } else { + error = err; + } + + this[kWorkletMarkNonCallableProcess]('node-web-audio-api:worklet:process-error', error); + + return false; } } - [kWorkletMarkNonCallableProcess]([cmd, err]) { + [kWorkletMarkNonCallableProcess](cmd, err) { this[kWorkletCallableProcess] = false; this.#errorPort.postMessage({ cmd, err }); } diff --git a/js/BaseAudioContext.js b/js/BaseAudioContext.js index 00a29bfd..7e6db617 100644 --- a/js/BaseAudioContext.js +++ b/js/BaseAudioContext.js @@ -514,8 +514,8 @@ Object.defineProperties(BaseAudioContext.prototype, { currentTime: kEnumerableProperty, renderQuantumSize: kEnumerableProperty, state: kEnumerableProperty, - onstatechange: kEnumerableProperty, decodeAudioData: kEnumerableProperty, createBuffer: kEnumerableProperty, createPeriodicWave: kEnumerableProperty, + onstatechange: kEnumerableProperty, }); diff --git a/js/lib/audio-worklet/BufferPool.js b/js/lib/audio-worklet/BufferPool.js index f5ec12c2..df1fe87d 100644 --- a/js/lib/audio-worklet/BufferPool.js +++ b/js/lib/audio-worklet/BufferPool.js @@ -39,9 +39,11 @@ export class BufferPool { } recycle(buffer) { - // make sure we can't pollute the pool - if (buffer.length === this.#bufferSize) { - this.#pool.push(buffer); + // make sure we don't pollute the pool + if (buffer.length !== this.#bufferSize) { + throw new Error(`Attempt to recycle a buffer of length ${buffer.length} in a pool of buffers of length ${this.#bufferSize}`); } + + this.#pool.push(buffer); } } diff --git a/js/lib/audio-worklet/pending-processor-construction-data.js b/js/lib/audio-worklet/pending-processor-construction-data.js deleted file mode 100644 index 5ff4500b..00000000 --- a/js/lib/audio-worklet/pending-processor-construction-data.js +++ /dev/null @@ -1 +0,0 @@ -export const pendingProcessorConstructionData = { inner: null }; diff --git a/js/lib/audio-worklet/pending-processor.js b/js/lib/audio-worklet/pending-processor.js new file mode 100644 index 00000000..d21a59fd --- /dev/null +++ b/js/lib/audio-worklet/pending-processor.js @@ -0,0 +1,5 @@ +export const pendingProcessor = { + constructionData: null, + instance: null, // mark derived class constructor as been successfully called + super: null, // mark AudioWorkletProcessor constructor as been called +}; diff --git a/src/audio_worklet_node.rs b/src/audio_worklet_node.rs index 6313c75f..bb51f230 100644 --- a/src/audio_worklet_node.rs +++ b/src/audio_worklet_node.rs @@ -131,17 +131,12 @@ thread_local! { static HAS_THREAD_PRIO: Cell = const { Cell::new(false) }; } -struct WorkletAbruptCompletionResult { - cmd: String, - err: Error, -} - /// Check that given JS and Rust input / output layout are the same, /// i.e. that each input / output have the same number of channels /// /// Note that we don't check the number of inputs / outputs as they are defined /// at construction and cannot be changed -fn check_same_io_layout(js_io: &Array, rs_io: &'static [&'static [&'static [f32]]]) -> bool { +fn is_same_io_layout(js_io: &Array, rs_io: &'static [&'static [&'static [f32]]]) -> bool { for (i, rs_channels) in rs_io.iter().enumerate() { let js_channels = js_io.get::(i as u32); @@ -165,8 +160,7 @@ fn check_same_io_layout(js_io: &Array, rs_io: &'static [&'static [&'static [f32] /// Recreate the JS inputs or output data structures (input and output are handled separately). /// We must rebuild the whole structure from scratch because the resulting Arrays are frozen. -/// -/// @todo - move this logic to JS to minimize language boundary crossing (needs benchmarking) +// @note: mini benchmarks have been made w/ an alternative JS implementation, was way slower fn rebuild_io_layout<'a>( env: &'a Env, js_io: Array, @@ -315,149 +309,111 @@ fn process_audio_worklet( return Ok(()); } - // The `process` method is not executed directly because napi-rs `apply` - // implementation retrieve the arguments as an array in the first argument, Then - // we need to unpack them first (cf. `AudioWorkletProcessor[kWorkletUnpackProcess]`) - // - // Note that we use `get_named_property` rather than `has_named_property` so that - // we check that `process` is a function as well. - let process_method_result = - processor.get_named_property::>("process"); - - let abrupt_completion = match process_method_result { - Ok(_process_method) => { - let render_quantum_size = - global.get_named_property::("renderQuantumSize")? as usize; - - let k_worklet_unpack_process = - env.symbol_for("node-web-audio-api:worklet-unpack-process")?; - - // The `kWorkletUnpackProcess` wrapper function coerce value returned from `process` - // to bool, therefore if we get anything else, i.e. `Unknown`, an error occurred. - let unpack_process_function = processor - .get_property::>>( - k_worklet_unpack_process, - )?; - - let k_worklet_inputs = env.symbol_for("node-web-audio-api:worklet-inputs")?; - let mut js_inputs = processor.get_property::(k_worklet_inputs)?; - - let k_worklet_outputs = env.symbol_for("node-web-audio-api:worklet-outputs")?; - let mut js_outputs = processor.get_property::(k_worklet_outputs)?; - - // - let k_worklet_params = env.symbol_for("node-web-audio-api:worklet-params")?; - let mut js_params = processor.get_property::(k_worklet_params)?; - // - let k_worklet_params_cache = - env.symbol_for("node-web-audio-api:worklet-params-cache")?; - let js_params_cache = - processor.get_property::(k_worklet_params_cache)?; - - // Check input and output channel layout, and rebuild JS object if something changed - if !check_same_io_layout(&js_inputs, inputs) { - let new_js_inputs = rebuild_io_layout(env, js_inputs, inputs)?; - processor.set_property(k_worklet_inputs, new_js_inputs)?; - js_inputs = processor.get_property::(k_worklet_inputs)?; - } + let render_quantum_size = global.get_named_property::("renderQuantumSize")? as usize; - if !check_same_io_layout(&js_outputs, outputs) { - let new_js_outputs = rebuild_io_layout(env, js_outputs, outputs)?; - processor.set_property(k_worklet_outputs, new_js_outputs)?; - js_outputs = processor.get_property::(k_worklet_outputs)?; - } + let k_worklet_inputs = env.symbol_for("node-web-audio-api:worklet-inputs")?; + let mut js_inputs = processor.get_property::(k_worklet_inputs)?; - // Copy inputs into JS inputs buffers - for (input_number, input) in inputs.iter().enumerate() { - let js_input = js_inputs.get::(input_number as u32)?.unwrap(); + let k_worklet_outputs = env.symbol_for("node-web-audio-api:worklet-outputs")?; + let mut js_outputs = processor.get_property::(k_worklet_outputs)?; - for (channel_number, channel) in input.iter().enumerate() { - let mut js_channel = js_input - .get::(channel_number as u32)? - .unwrap(); - let js_channel: &mut [f32] = unsafe { js_channel.as_mut() }; - js_channel.copy_from_slice(channel); - } - } + // + let k_worklet_params = env.symbol_for("node-web-audio-api:worklet-params")?; + let mut js_params = processor.get_property::(k_worklet_params)?; + // + let k_worklet_params_cache = env.symbol_for("node-web-audio-api:worklet-params-cache")?; + let js_params_cache = processor.get_property::(k_worklet_params_cache)?; - // Copy params values into JS params buffers - // - // @todo(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 mut param_values = float32_arr_cache.get::(cache_index)?.unwrap(); - // copy data into underlying ArrayBuffer - let buffer: &mut [f32] = unsafe { param_values.as_mut() }; - buffer.copy_from_slice(data); - // replace current values with new Float32Array - js_params.set_named_property(name, param_values)?; - } + // Check input and output channel layout, and rebuild JS object if something changed + if !is_same_io_layout(&js_inputs, inputs) { + let new_js_inputs = rebuild_io_layout(env, js_inputs, inputs)?; + processor.set_property(k_worklet_inputs, new_js_inputs)?; + js_inputs = processor.get_property::(k_worklet_inputs)?; + } - let js_result = - unpack_process_function.apply(processor, (js_inputs, js_outputs, js_params))?; + if !is_same_io_layout(&js_outputs, outputs) { + let new_js_outputs = rebuild_io_layout(env, js_outputs, outputs)?; + processor.set_property(k_worklet_outputs, new_js_outputs)?; + js_outputs = processor.get_property::(k_worklet_outputs)?; + } + + // Copy inputs into JS inputs buffers + for (input_number, input) in inputs.iter().enumerate() { + let js_input = js_inputs.get::(input_number as u32)?.unwrap(); + + for (channel_number, channel) in input.iter().enumerate() { + let mut js_channel = js_input + .get::(channel_number as u32)? + .unwrap(); + let js_channel: &mut [f32] = unsafe { js_channel.as_mut() }; + js_channel.copy_from_slice(channel); + } + } - match js_result { - Either::A(tail_time) => { - // copy JS output buffers back into outputs - for (output_number, output) in outputs.iter().enumerate() { - let js_output = js_outputs.get_element::(output_number as u32)?; + // Clear output buffers + // cf. wpt/webaudio/the-audio-api/the-audioworklet-interface/audioworkletprocessor-process-zero-outputs.https.html + for (output_number, output) in outputs.iter().enumerate() { + let js_output = js_outputs.get::(output_number as u32)?.unwrap(); - for (channel_number, channel) in output.iter().enumerate() { - let js_channel = js_output - .get::(channel_number as u32)? - .unwrap(); + for (channel_number, _) in output.iter().enumerate() { + let mut js_channel = js_output + .get::(channel_number as u32)? + .unwrap(); + let js_channel: &mut [f32] = unsafe { js_channel.as_mut() }; + js_channel.fill(0.); + } + } - let src = js_channel.as_ptr(); - let dst = channel.as_ptr() as *mut f32; + // Copy params values into JS params buffers + // @todo(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 mut param_values = float32_arr_cache.get::(cache_index)?.unwrap(); + // copy data into underlying ArrayBuffer + let buffer: &mut [f32] = unsafe { param_values.as_mut() }; + buffer.copy_from_slice(data); + // replace current values with new Float32Array + js_params.set_named_property(name, param_values)?; + } - unsafe { - std::ptr::copy_nonoverlapping(src, dst, render_quantum_size); - } - } - } + // The `process` method is executed indirectly because napi-rs `apply` implementation + // retrieve the arguments as an array in the first argument, Then we need to unpack + // them first (cf. `AudioWorkletProcessor[kWorkletUnpackProcess]`) + let k_worklet_unpack_process = env.symbol_for("node-web-audio-api:worklet-unpack-process")?; - let _ = tail_time_sender.send(tail_time); // allowed to fail + // The `kWorkletUnpackProcess` wrapper function coerce value returned from `process` + // to bool, if any error occurred in process, it has been catched in `kWorkletUnpackProcess`` + // which marked the processor has non-callable and returned false + let unpack_process_function = processor + .get_property::>( + k_worklet_unpack_process, + )?; - None - } - // error thrown in process - Either::B(err) => Some(WorkletAbruptCompletionResult { - cmd: "node-web-audio-api:worklet:process-error".to_string(), - err: err.into(), - }), - } - } - // no process method found - Err(err) => Some(WorkletAbruptCompletionResult { - cmd: "node-web-audio-api:worklet:process-invalid".to_string(), - err, - }), - }; + let tail_time = unpack_process_function.apply(processor, (js_inputs, js_outputs, js_params))?; - // Handle errors - // @todo - propagate back to rust side to remove processor from graph - if let Some(abrupt_completion) = abrupt_completion { - let WorkletAbruptCompletionResult { cmd, err } = abrupt_completion; + // copy JS output buffers back into outputs + for (output_number, output) in outputs.iter().enumerate() { + let js_output = js_outputs.get_element::(output_number as u32)?; - // Mark as non callable and dispatch `processorerror` event on main thread - let k_worklet_mark_non_callable = - env.symbol_for("node-web-audio-api:worklet-mark-non-callable-process")?; + for (channel_number, channel) in output.iter().enumerate() { + let js_channel = js_output + .get::(channel_number as u32)? + .unwrap(); - let mark_non_callable_function = processor - .get_property::>( - k_worklet_mark_non_callable, - )?; - let js_error = env.create_error(err)?; - let _ = mark_non_callable_function.apply(processor, (cmd, js_error)); + let src = js_channel.as_ptr(); + let dst = channel.as_ptr() as *mut f32; - // Mark tail time to false in audio graph - // https://webaudio.github.io/web-audio-api/#active-source - let _ = tail_time_sender.send(false); + unsafe { + std::ptr::copy_nonoverlapping(src, dst, render_quantum_size); + } + } } + let _ = tail_time_sender.send(tail_time); // allowed to fail + Ok(()) } diff --git a/tests/AudioWorklet.spec.js b/tests/AudioWorklet.spec.js index 79e71184..328ff0c2 100644 --- a/tests/AudioWorklet.spec.js +++ b/tests/AudioWorklet.spec.js @@ -194,7 +194,7 @@ describe('AudioWorklet', () => { await audioContext.close(); }); - it(`should throw clean error if worklet is invalid`, async () => { + it(`should throw clean error if worklet is invalid (1)`, async () => { // blob worklets do not support import const blob = new Blob(['import stuff from "./abc"'], { type: 'application/javascript' }); const objectUrl = URL.createObjectURL(blob); @@ -213,7 +213,7 @@ describe('AudioWorklet', () => { assert.isTrue(errored); }); - it(`should throw clean error if worklet is invalid`, async () => { + it(`should throw clean error if worklet is invalid (2)`, async () => { const audioContext = new AudioContext(); let errored = false; @@ -264,6 +264,57 @@ describe('AudioWorkletProcessor', () => { assert.isTrue(errored); }); + it('should throw a clean error when process is not callable', async () => { + let errored = false; + + const audioContext = new AudioContext(); + await audioContext.audioWorklet.addModule('./worklets/invalid-process.worklet.js'); + + const invalid = new AudioWorkletNode(audioContext, 'invalid-process'); + invalid.addEventListener('processorerror', (e) => { + prettyPrintErr(e.error); + errored = true; + }); + + await delay(100); + await audioContext.close(); + assert.isTrue(errored); + }); + + it('should throw a clean error when process throws', async () => { + let errored = false; + + const audioContext = new AudioContext(); + await audioContext.audioWorklet.addModule('./worklets/invalid-process.worklet.js'); + + const invalid = new AudioWorkletNode(audioContext, 'process-throws'); + invalid.addEventListener('processorerror', (e) => { + prettyPrintErr(e.error); + errored = true; + }); + + await delay(100); + await audioContext.close(); + assert.isTrue(errored); + }); + + it('should throw a clean error when process throws raw message', async () => { + let errored = false; + + const audioContext = new AudioContext(); + await audioContext.audioWorklet.addModule('./worklets/invalid-process.worklet.js'); + + const invalid = new AudioWorkletNode(audioContext, 'process-throws-raw'); + invalid.addEventListener('processorerror', (e) => { + prettyPrintErr(e.error); + errored = true; + }); + + await delay(100); + await audioContext.close(); + assert.isTrue(errored); + }); + it('OfflineAudioContext.startRendering should return when processor constructor is invalid', async () => { const audioContext = new OfflineAudioContext(1, 128, 48000); await audioContext.audioWorklet.addModule('./worklets/invalid-ctor.worklet.js'); diff --git a/tests/OfflineAudioContext.spec.js b/tests/OfflineAudioContext.spec.js index 79284ab6..04aac687 100644 --- a/tests/OfflineAudioContext.spec.js +++ b/tests/OfflineAudioContext.spec.js @@ -36,7 +36,7 @@ describe('# OfflineAudioContext', () => { assert.deepEqual(aResult, bResult); }); - it(`should throw clean error when called twice`, async () => { + it(`[TO BE IMPROVED] should throw clean error when called twice`, async () => { const offline = new OfflineAudioContext(1, 48000, 48000); await offline.startRendering(); diff --git a/tests/worklets/invalid-process.worklet.js b/tests/worklets/invalid-process.worklet.js new file mode 100644 index 00000000..873e593c --- /dev/null +++ b/tests/worklets/invalid-process.worklet.js @@ -0,0 +1,25 @@ +class InvalidProcess extends AudioWorkletProcessor { + // attribute exists but is not callbable + process = null +} + +registerProcessor('invalid-process', InvalidProcess); + +class ProcessThrows extends AudioWorkletProcessor { + // attribute exists but is not callbable + process = (inputs, outputs, params) => { + outputs[3][1] = 'throws'; + } +} + +registerProcessor('process-throws', ProcessThrows); + + +class ProcessThrowsRaw extends AudioWorkletProcessor { + // attribute exists but is not callbable + process = (inputs, outputs, params) => { + throw 'row string thrown'; + } +} + +registerProcessor('process-throws-raw', ProcessThrowsRaw);