Skip to content

Commit

Permalink
propagate processoreror if process is not defined
Browse files Browse the repository at this point in the history
  • Loading branch information
b-ma committed May 11, 2024
1 parent a34cc97 commit 540f7ff
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 54 deletions.
5 changes: 5 additions & 0 deletions js/AudioWorkletGlobalScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -104,6 +105,10 @@ globalThis.AudioWorkletProcessor = class AudioWorkletProcessor {

return this.#port;
}

[kWorkletQueueTask](cmd) {
this.#port.postMessage({ cmd });
}
}

// follow algorithm from:
Expand Down
19 changes: 19 additions & 0 deletions js/AudioWorkletNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -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() {
Expand Down
89 changes: 86 additions & 3 deletions js/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ Object.defineProperties(OfflineAudioCompletionEvent.prototype, {
configurable: true,
value: 'OfflineAudioCompletionEvent',
},

renderedBuffer: kEnumerableProperty,
});

Expand Down Expand Up @@ -78,7 +77,6 @@ Object.defineProperties(AudioProcessingEvent.prototype, {
configurable: true,
value: 'AudioProcessingEvent',
},

playbackTime: kEnumerableProperty,
inputBuffer: kEnumerableProperty,
outputBuffer: kEnumerableProperty,
Expand Down Expand Up @@ -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;
139 changes: 88 additions & 51 deletions src/audio_worklet_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<JsSymbol, JsBoolean>(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::<JsFunction>("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::<JsSymbol, JsObject>(k_worklet_inputs)?;

for (input_number, input) in inputs.iter().enumerate() {
let mut channels = js_inputs.get_element::<JsObject>(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::<JsSymbol, JsObject>(k_worklet_inputs)?;
let js_outputs = processor.get_property::<JsSymbol, JsObject>(k_worklet_outputs)?;

for (input_number, input) in inputs.iter().enumerate() {
let mut channels = js_inputs.get_element::<JsObject>(input_number as u32)?;
for (output_number, output) in outputs.iter().enumerate() {
let mut channels = js_outputs.get_element::<JsObject>(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::<JsSymbol, JsObject>(k_worklet_outputs)?;
let mut js_params = processor.get_property::<JsSymbol, JsObject>(k_worklet_params)?;
let js_params_cache =
processor.get_property::<JsSymbol, JsObject>(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::<JsObject>(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::<JsTypedArray>(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::<JsTypedArray>(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::<JsObject>(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::<JsSymbol, JsFunction>(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::<JsSymbol, JsObject>(k_worklet_params)?;
let js_params_cache = processor.get_property::<JsSymbol, JsObject>(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::<JsObject>(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::<JsTypedArray>(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::<JsTypedArray>(cache_index)?;
js_params.set_named_property(name, float32_arr).unwrap();
}

let process_method = processor.get_named_property::<JsFunction>("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(())
}

Expand Down

0 comments on commit 540f7ff

Please sign in to comment.