-
Notifications
You must be signed in to change notification settings - Fork 3.3k
/
Copy pathaudio_worklet.js
179 lines (160 loc) · 8.15 KB
/
audio_worklet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// This file is the main bootstrap script for Wasm Audio Worklets loaded in an Emscripten application.
// Build with -sAUDIO_WORKLET=1 linker flag to enable targeting Audio Worklets.
// AudioWorkletGlobalScope does not have a onmessage/postMessage() functionality at the global scope, which
// means that after creating an AudioWorkletGlobalScope and loading this script into it, we cannot
// postMessage() information into it like one would do with Web Workers.
// Instead, we must create an AudioWorkletProcessor class, then instantiate a Web Audio graph node from it
// on the main thread. Using its message port and the node constructor's
// "processorOptions" field, we can share the necessary bootstrap information from the main thread to
// the AudioWorkletGlobalScope.
function createWasmAudioWorkletProcessor(audioParams) {
class WasmAudioWorkletProcessor extends AudioWorkletProcessor {
constructor(args) {
super();
// Copy needed stack allocation functions from the Module object
// to global scope, these will be accessed in hot paths, so maybe
// they'll be a bit faster to access directly, rather than referencing
// them as properties of the Module object.
globalThis.stackAlloc = Module['stackAlloc'];
globalThis.stackSave = Module['stackSave'];
globalThis.stackRestore = Module['stackRestore'];
globalThis.HEAPU32 = Module['HEAPU32'];
globalThis.HEAPF32 = Module['HEAPF32'];
// Capture the Wasm function callback to invoke.
let opts = args.processorOptions;
this.callbackFunction = Module['wasmTable'].get(opts['cb']);
this.userData = opts['ud'];
}
static get parameterDescriptors() {
return audioParams;
}
process(inputList, outputList, parameters) {
// Marshal all inputs and parameters to the Wasm memory on the thread stack,
// then perform the wasm audio worklet call,
// and finally marshal audio output data back.
let numInputs = inputList.length,
numOutputs = outputList.length,
numParams = 0, i, j, k, dataPtr,
stackMemoryNeeded = (numInputs + numOutputs) * 8,
oldStackPtr = stackSave(),
inputsPtr, outputsPtr, outputDataPtr, paramsPtr,
didProduceAudio, paramArray;
// Calculate how much stack space is needed.
for (i of inputList) stackMemoryNeeded += i.length * 512;
for (i of outputList) stackMemoryNeeded += i.length * 512;
for (i in parameters) stackMemoryNeeded += parameters[i].byteLength + 8, ++numParams;
// Allocate the necessary stack space.
inputsPtr = stackAlloc(stackMemoryNeeded);
// Copy input audio descriptor structs and data to Wasm
k = inputsPtr >> 2;
dataPtr = inputsPtr + numInputs * 8;
for (i of inputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k++] = i.length;
HEAPU32[k++] = dataPtr;
// Marshal the input audio sample data for each audio channel of this input
for (j of i) {
HEAPF32.set(j, dataPtr>>2);
dataPtr += 512;
}
}
// Copy output audio descriptor structs to Wasm
outputsPtr = dataPtr;
k = outputsPtr >> 2;
outputDataPtr = (dataPtr += numOutputs * 8) >> 2;
for (i of outputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k++] = i.length;
HEAPU32[k++] = dataPtr;
// Reserve space for the output data
dataPtr += 512 * i.length;
}
// Copy parameters descriptor structs and data to Wasm
paramsPtr = dataPtr;
k = paramsPtr >> 2;
dataPtr += numParams * 8;
for (i = 0; paramArray = parameters[i++];) {
// Write the AudioParamFrame struct instance
HEAPU32[k++] = paramArray.length;
HEAPU32[k++] = dataPtr;
// Marshal the audio parameters array
HEAPF32.set(paramArray, dataPtr>>2);
dataPtr += paramArray.length*4;
}
// Call out to Wasm callback to perform audio processing
if (didProduceAudio = this.callbackFunction(numInputs, inputsPtr, numOutputs, outputsPtr, numParams, paramsPtr, this.userData)) {
// Read back the produced audio data to all outputs and their channels.
// (A garbage-free function TypedArray.copy(dstTypedArray, dstOffset, srcTypedArray, srcOffset, count) would sure be handy..
// but web does not have one, so manually copy all bytes in)
for (i of outputList) {
for (j of i) {
for (k = 0; k < 128; ++k) {
j[k] = HEAPF32[outputDataPtr++];
}
}
}
}
stackRestore(oldStackPtr);
// Return 'true' to tell the browser to continue running this processor. (Returning 1 or any other truthy value won't work in Chrome)
return !!didProduceAudio;
}
}
return WasmAudioWorkletProcessor;
}
// Specify a worklet processor that will be used to receive messages to this AudioWorkletGlobalScope.
// We never connect this initial AudioWorkletProcessor to the audio graph to do any audio processing.
class BootstrapMessages extends AudioWorkletProcessor {
constructor(arg) {
super();
// Initialize the global Emscripten Module object that contains e.g. the Wasm Module and Memory objects.
// After this we are ready to load in the main application JS script, which the main thread will addModule()
// to this scope.
globalThis.Module = arg['processorOptions'];
#if !MINIMAL_RUNTIME
// Default runtime relies on an injected instantiateWasm() function to initialize the Wasm Module.
globalThis.Module['instantiateWasm'] = (info, receiveInstance) => {
var instance = new WebAssembly.Instance(Module['wasm'], info);
receiveInstance(instance, Module['wasm']);
return instance.exports;
};
#endif
#if WEBAUDIO_DEBUG
console.log('AudioWorklet global scope looks like this:');
console.dir(globalThis);
#endif
// Listen to messages from the main thread. These messages will ask this scope to create the real
// AudioWorkletProcessors that call out to Wasm to do audio processing.
let p = globalThis['messagePort'] = this.port;
p.onmessage = (msg) => {
let d = msg.data;
if (d['_wpn']) { // '_wpn' is short for 'Worklet Processor Node', using an identifier that will never conflict with user messages
#if MODULARIZE
// Instantiate the MODULARIZEd Module function, which is stored for us under the special global
// name AudioWorkletModule in MODULARIZE+AUDIO_WORKLET builds.
if (globalThis.AudioWorkletModule) {
AudioWorkletModule(Module); // This populates the Module object with all the Wasm properties
delete globalThis.AudioWorkletModule; // We have now instantiated the Module function, can discard it from global scope
}
#endif
// Register a real AudioWorkletProcessor that will actually do audio processing.
registerProcessor(d['_wpn'], createWasmAudioWorkletProcessor(d['audioParams']));
#if WEBAUDIO_DEBUG
console.log(`Registered a new WasmAudioWorkletProcessor "${d['_wpn']}" with AudioParams: ${d['audioParams']}`);
#endif
// Post a Wasm Call message back telling that we have now registered the AudioWorkletProcessor class,
// and should trigger the user onSuccess callback of the emscripten_create_wasm_audio_worklet_processor_async() call.
p.postMessage({'_wsc': d['callback'], 'x': [d['contextHandle'], 1/*EM_TRUE*/, d['userData']] }); // "WaSm Call"
} else if (d['_wsc']) { // '_wsc' is short for 'wasm call', using an identifier that will never conflict with user messages
Module['wasmTable'].get(d['_wsc'])(...d['x']);
};
}
}
// No-op, not doing audio processing in this processor. It is just for receiving bootstrap messages.
// However browsers require it to still be present. It should never be called because we never add a
// node to the graph with this processor, although it does look like Chrome does still call this function.
process() {
// keep this function a no-op. Chrome redundantly wants to call this even though this processor is never added to the graph.
}
};
// Register the dummy processor that will just receive messages.
registerProcessor("message", BootstrapMessages);