Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .scripts/wpt-harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]

// -------------------------------------------------------
Expand Down
56 changes: 36 additions & 20 deletions js/AudioWorkletGlobalScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
Expand Down
53 changes: 33 additions & 20 deletions js/AudioWorkletProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
}
}

Expand All @@ -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 });
}
Expand Down
2 changes: 1 addition & 1 deletion js/BaseAudioContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,8 @@ Object.defineProperties(BaseAudioContext.prototype, {
currentTime: kEnumerableProperty,
renderQuantumSize: kEnumerableProperty,
state: kEnumerableProperty,
onstatechange: kEnumerableProperty,
decodeAudioData: kEnumerableProperty,
createBuffer: kEnumerableProperty,
createPeriodicWave: kEnumerableProperty,
onstatechange: kEnumerableProperty,
});
8 changes: 5 additions & 3 deletions js/lib/audio-worklet/BufferPool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

This file was deleted.

5 changes: 5 additions & 0 deletions js/lib/audio-worklet/pending-processor.js
Original file line number Diff line number Diff line change
@@ -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
};
Loading