Skip to content

Commit ac381b8

Browse files
authored
[AUDIO_WORKLET] Added API for getting the buffer's quantum size (#22681)
The Web Audio API defines the processed sample size as always being `128`, which is hardcoded in both the test code and docs. The upcoming Web Audio API has the option to set this to a user defined setting or request the machine's preference, so in preparation the Audio Worklet is extended with a function to query the context's _quantum_ at creation time (and before the worklet is created), and also the processing callback contains a field with the same value. For the simplest uses transitioning to the processing callback's field value will mean future changes will simply work. Once the 1.1 version of the Web Audio API is supported, the context creation can be amended to accept a quantum hint, and any code written again these PR's changes will still work.
1 parent c6dc647 commit ac381b8

10 files changed

+119
-28
lines changed

site/source/docs/api_reference/wasm_audio_worklets.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ processing graph as AudioWorkletNodes.
4545

4646
Once a class type is instantiated on the Web Audio graph and the graph is
4747
running, a C/C++ function pointer callback will be invoked for each 128
48-
samples of the processed audio stream that flows through the node.
48+
samples of the processed audio stream that flows through the node. Newer Web
49+
Audio API specs allow this to be changed, so for future compatibility use the
50+
``AudioSampleFrame``'s ``quantumSize`` to get the value.
4951

5052
This callback will be executed on a dedicated separate audio processing
5153
thread with real-time processing priority. Each Web Audio context will
@@ -157,7 +159,7 @@ which resumes the audio context when the user clicks on the DOM Canvas element t
157159
void *userData)
158160
{
159161
for(int i = 0; i < numOutputs; ++i)
160-
for(int j = 0; j < 128*outputs[i].numberOfChannels; ++j)
162+
for(int j = 0; j < outputs[i].quantumSize*outputs[i].numberOfChannels; ++j)
161163
outputs[i].data[j] = emscripten_random() * 0.2 - 0.1; // Warning: scale down audio volume by factor of 0.2, raw noise can be really loud otherwise
162164
163165
return true; // Keep the graph output going

src/audio_worklet.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ function createWasmAudioWorkletProcessor(audioParams) {
3131
let opts = args.processorOptions;
3232
this.callbackFunction = Module['wasmTable'].get(opts['cb']);
3333
this.userData = opts['ud'];
34+
// Plus the number of samples to process, fixed for the lifetime of the
35+
// context that created this processor. Note for when moving to Web Audio
36+
// 1.1: the typed array passed to process() should be the same size as the
37+
// the quantum size, and this exercise of passing in the value shouldn't
38+
// be required (to be verified).
39+
this.quantumSize = opts['qs'];
3440
}
3541

3642
static get parameterDescriptors() {
@@ -45,53 +51,59 @@ function createWasmAudioWorkletProcessor(audioParams) {
4551
let numInputs = inputList.length,
4652
numOutputs = outputList.length,
4753
numParams = 0, i, j, k, dataPtr,
48-
stackMemoryNeeded = (numInputs + numOutputs) * 8,
54+
quantumBytes = this.quantumSize * 4,
55+
stackMemoryNeeded = (numInputs + numOutputs) * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}},
4956
oldStackPtr = stackSave(),
5057
inputsPtr, outputsPtr, outputDataPtr, paramsPtr,
5158
didProduceAudio, paramArray;
5259

5360
// Calculate how much stack space is needed.
54-
for (i of inputList) stackMemoryNeeded += i.length * 512;
55-
for (i of outputList) stackMemoryNeeded += i.length * 512;
56-
for (i in parameters) stackMemoryNeeded += parameters[i].byteLength + 8, ++numParams;
61+
for (i of inputList) stackMemoryNeeded += i.length * quantumBytes;
62+
for (i of outputList) stackMemoryNeeded += i.length * quantumBytes;
63+
for (i in parameters) stackMemoryNeeded += parameters[i].byteLength + {{{ C_STRUCTS.AudioParamFrame.__size__ }}}, ++numParams;
5764

5865
// Allocate the necessary stack space.
5966
inputsPtr = stackAlloc(stackMemoryNeeded);
6067

6168
// Copy input audio descriptor structs and data to Wasm
6269
k = inputsPtr >> 2;
63-
dataPtr = inputsPtr + numInputs * 8;
70+
dataPtr = inputsPtr + numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
6471
for (i of inputList) {
6572
// Write the AudioSampleFrame struct instance
66-
HEAPU32[k++] = i.length;
67-
HEAPU32[k++] = dataPtr;
73+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
74+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.quantumSize / 4 }}}] = this.quantumSize;
75+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
76+
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
6877
// Marshal the input audio sample data for each audio channel of this input
6978
for (j of i) {
7079
HEAPF32.set(j, dataPtr>>2);
71-
dataPtr += 512;
80+
dataPtr += quantumBytes;
7281
}
7382
}
7483

7584
// Copy output audio descriptor structs to Wasm
7685
outputsPtr = dataPtr;
7786
k = outputsPtr >> 2;
78-
outputDataPtr = (dataPtr += numOutputs * 8) >> 2;
87+
outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}) >> 2;
7988
for (i of outputList) {
8089
// Write the AudioSampleFrame struct instance
81-
HEAPU32[k++] = i.length;
82-
HEAPU32[k++] = dataPtr;
90+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
91+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.quantumSize / 4 }}}] = this.quantumSize;
92+
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
93+
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
8394
// Reserve space for the output data
84-
dataPtr += 512 * i.length;
95+
dataPtr += quantumBytes * i.length;
8596
}
8697

8798
// Copy parameters descriptor structs and data to Wasm
8899
paramsPtr = dataPtr;
89100
k = paramsPtr >> 2;
90-
dataPtr += numParams * 8;
101+
dataPtr += numParams * {{{ C_STRUCTS.AudioParamFrame.__size__ }}};
91102
for (i = 0; paramArray = parameters[i++];) {
92103
// Write the AudioParamFrame struct instance
93-
HEAPU32[k++] = paramArray.length;
94-
HEAPU32[k++] = dataPtr;
104+
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.length / 4 }}}] = paramArray.length;
105+
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.data / 4 }}}] = dataPtr;
106+
k += {{{ C_STRUCTS.AudioParamFrame.__size__ / 4 }}};
95107
// Marshal the audio parameters array
96108
HEAPF32.set(paramArray, dataPtr>>2);
97109
dataPtr += paramArray.length*4;
@@ -105,7 +117,7 @@ function createWasmAudioWorkletProcessor(audioParams) {
105117
// not have one, so manually copy all bytes in)
106118
for (i of outputList) {
107119
for (j of i) {
108-
for (k = 0; k < 128; ++k) {
120+
for (k = 0; k < this.quantumSize; ++k) {
109121
j[k] = HEAPF32[outputDataPtr++];
110122
}
111123
}

src/library_sigs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ sigs = {
586586
emscripten_atomic_cancel_wait_async__sig: 'ii',
587587
emscripten_atomic_wait_async__sig: 'ipippd',
588588
emscripten_atomics_is_lock_free__sig: 'ii',
589+
emscripten_audio_context_quantum_size__sig: 'ii',
589590
emscripten_audio_context_state__sig: 'ii',
590591
emscripten_audio_node_connect__sig: 'viiii',
591592
emscripten_audio_worklet_post_function_sig__sig: 'vippp',

src/library_webaudio.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,21 @@ let LibraryWebAudio = {
3737
// Wasm handle ID.
3838
$emscriptenGetAudioObject: (objectHandle) => EmAudio[objectHandle],
3939

40-
// emscripten_create_audio_context() does not itself use
40+
// Performs the work of getting the AudioContext's quantum size.
41+
$emscriptenGetContextQuantumSize: (contextHandle) => {
42+
// TODO: in a future release this will be something like:
43+
// return EmAudio[contextHandle].renderQuantumSize || 128;
44+
// It comes two caveats: it needs the hint when generating the context adding to
45+
// emscripten_create_audio_context(), and altering the quantum requires a secure
46+
// context and fallback implementing. Until then we simply use the 1.0 API value:
47+
return 128;
48+
},
49+
50+
// emscripten_create_audio_context() does not itself use the
4151
// emscriptenGetAudioObject() function, but mark it as a dependency, because
4252
// the user will not be able to utilize the node unless they call
4353
// emscriptenGetAudioObject() on it on JS side to connect it to the graph, so
44-
// this avoids the user needing to manually do it on the command line.
54+
// this avoids the user needing to manually add the dependency on the command line.
4555
emscripten_create_audio_context__deps: ['$emscriptenRegisterAudioObject', '$emscriptenGetAudioObject'],
4656
emscripten_create_audio_context: (options) => {
4757
let ctx = window.AudioContext || window.webkitAudioContext;
@@ -264,6 +274,7 @@ let LibraryWebAudio = {
264274
});
265275
},
266276

277+
emscripten_create_wasm_audio_worklet_node__deps: ['$emscriptenGetContextQuantumSize'],
267278
emscripten_create_wasm_audio_worklet_node: (contextHandle, name, options, callback, userData) => {
268279
#if ASSERTIONS
269280
assert(contextHandle, `Called emscripten_create_wasm_audio_worklet_node() with a null Web Audio Context handle!`);
@@ -282,7 +293,11 @@ let LibraryWebAudio = {
282293
numberOfInputs: HEAP32[options],
283294
numberOfOutputs: HEAP32[options+1],
284295
outputChannelCount: HEAPU32[options+2] ? readChannelCountArray(HEAPU32[options+2]>>2, HEAP32[options+1]) : void 0,
285-
processorOptions: { 'cb': callback, 'ud': userData }
296+
processorOptions: {
297+
'cb': callback,
298+
'ud': userData,
299+
'qs': emscriptenGetContextQuantumSize(contextHandle)
300+
}
286301
} : void 0;
287302

288303
#if WEBAUDIO_DEBUG
@@ -293,6 +308,15 @@ let LibraryWebAudio = {
293308
},
294309
#endif // ~AUDIO_WORKLET
295310

311+
emscripten_audio_context_quantum_size__deps: ['$emscriptenGetContextQuantumSize'],
312+
emscripten_audio_context_quantum_size: (contextHandle) => {
313+
#if ASSERTIONS
314+
assert(EmAudio[contextHandle], `Called emscripten_audio_context_quantum_size() with an invalid Web Audio Context handle ${contextHandle}`);
315+
assert(EmAudio[contextHandle] instanceof (window.AudioContext || window.webkitAudioContext), `Called emscripten_audio_context_quantum_size() on handle ${contextHandle} that is not an AudioContext, but of type ${EmAudio[contextHandle]}`);
316+
#endif
317+
return emscriptenGetContextQuantumSize(contextHandle);
318+
},
319+
296320
emscripten_audio_node_connect: (source, destination, outputIndex, inputIndex) => {
297321
var srcNode = EmAudio[source];
298322
var dstNode = EmAudio[destination];

src/struct_info.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,20 @@
11981198
]
11991199
}
12001200
},
1201+
{
1202+
"file": "emscripten/webaudio.h",
1203+
"structs": {
1204+
"AudioSampleFrame": [
1205+
"numberOfChannels",
1206+
"quantumSize",
1207+
"data"
1208+
],
1209+
"AudioParamFrame": [
1210+
"length",
1211+
"data"
1212+
]
1213+
}
1214+
},
12011215
{
12021216
"file": "AL/al.h",
12031217
"defines": [

src/struct_info_generated.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@
470470
"__WASI_RIGHTS_SOCK_SHUTDOWN": 268435456
471471
},
472472
"structs": {
473+
"AudioParamFrame": {
474+
"__size__": 8,
475+
"data": 4,
476+
"length": 0
477+
},
478+
"AudioSampleFrame": {
479+
"__size__": 12,
480+
"data": 8,
481+
"numberOfChannels": 0,
482+
"quantumSize": 4
483+
},
473484
"EmscriptenBatteryEvent": {
474485
"__size__": 32,
475486
"charging": 24,

src/struct_info_generated_wasm64.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@
470470
"__WASI_RIGHTS_SOCK_SHUTDOWN": 268435456
471471
},
472472
"structs": {
473+
"AudioParamFrame": {
474+
"__size__": 16,
475+
"data": 8,
476+
"length": 0
477+
},
478+
"AudioSampleFrame": {
479+
"__size__": 16,
480+
"data": 8,
481+
"numberOfChannels": 0,
482+
"quantumSize": 4
483+
},
473484
"EmscriptenBatteryEvent": {
474485
"__size__": 32,
475486
"charging": 24,

system/include/emscripten/webaudio.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,27 @@ typedef void (*EmscriptenWorkletProcessorCreatedCallback)(EMSCRIPTEN_WEBAUDIO_T
9595
// userData3: A custom userdata pointer to pass to the callback function. This value will be passed on to the call to the given EmscriptenWorkletProcessorCreatedCallback callback function.
9696
void emscripten_create_wasm_audio_worklet_processor_async(EMSCRIPTEN_WEBAUDIO_T audioContext, const WebAudioWorkletProcessorCreateOptions *options, EmscriptenWorkletProcessorCreatedCallback callback, void *userData3);
9797

98+
// Returns the number of samples processed per channel in an AudioSampleFrame, fixed at 128 in the Web Audio API 1.0 specification, and valid for the lifetime of the audio context.
99+
// For this to change from the default 128, the context would need creating with a yet unexposed WebAudioWorkletProcessorCreateOptions renderSizeHint, part of the 1.1 Web Audio API.
100+
int emscripten_audio_context_quantum_size(EMSCRIPTEN_WEBAUDIO_T audioContext);
101+
98102
typedef int EMSCRIPTEN_AUDIO_WORKLET_NODE_T;
99103

100104
typedef struct AudioSampleFrame
101105
{
106+
// Number of audio channels to process (multiplied by quantumSize gives the elements in data)
102107
const int numberOfChannels;
103-
// An array of length numberOfChannels*128 elements, where data[channelIndex*128+i] locates the data of the i'th sample of channel channelIndex.
108+
// Number of samples per channel in data
109+
const int quantumSize;
110+
// An array of length numberOfChannels*quantumSize elements. Samples are always arranged in a planar fashion,
111+
// where data[channelIndex*quantumSize+i] locates the data of the i'th sample of channel channelIndex.
104112
float *data;
105113
} AudioSampleFrame;
106114

107115
typedef struct AudioParamFrame
108116
{
109117
// Specifies the length of the input array data (in float elements). This will be guaranteed to either have
110-
// a value of 1 or 128, depending on whether the audio parameter changed during this frame.
118+
// a value of 1, for a parameter valid for the entire frame, or emscripten_audio_context_quantum_size() for a parameter that changes during the frame.
111119
int length;
112120
// An array of length specified in 'length'.
113121
float *data;

test/webaudio/audio_worklet_tone_generator.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include <emscripten/webaudio.h>
22
#include <emscripten/em_math.h>
33

4+
#include <stdio.h>
5+
46
// This program tests that sharing the WebAssembly Memory works between the
57
// audio generator thread and the main browser UI thread. Two sliders,
68
// frequency and volume, can be adjusted on the HTML page, and the audio thread
@@ -25,7 +27,7 @@ float currentVolume = 0.3; // [local variable to the audio thread]
2527
volatile int audioProcessedCount = 0;
2628
#endif
2729

28-
// This function will be called for every fixed 128 samples of audio to be processed.
30+
// This function will be called for every fixed-size buffer of audio samples to be processed.
2931
bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) {
3032
#ifdef REPORT_RESULT
3133
++audioProcessedCount;
@@ -38,12 +40,12 @@ bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs,
3840

3941
// Produce a sine wave tone of desired frequency to all output channels.
4042
for(int o = 0; o < numOutputs; ++o)
41-
for(int i = 0; i < 128; ++i)
43+
for(int i = 0; i < outputs[o].quantumSize; ++i)
4244
{
4345
float s = emscripten_math_sin(phase);
4446
phase += phaseIncrement;
4547
for(int ch = 0; ch < outputs[o].numberOfChannels; ++ch)
46-
outputs[o].data[ch*128 + i] = s * currentVolume;
48+
outputs[o].data[ch*outputs[o].quantumSize + i] = s * currentVolume;
4749
}
4850

4951
// Range reduce to keep precision around zero.
@@ -148,6 +150,12 @@ int main() {
148150

149151
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(&attrs);
150152

153+
// Get the context's quantum size. Once the audio API allows this to be user
154+
// defined or exposes the hardware's own value, this will be needed to
155+
// determine the worklet stack size.
156+
int quantumSize = emscripten_audio_context_quantum_size(context);
157+
printf("Context quantum size: %d\n", quantumSize);
158+
151159
// and kick off Audio Worklet scope initialization, which shares the Wasm
152160
// Module and Memory to the AudioWorklet scope and initializes its stack.
153161
emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0);

test/webaudio/audioworklet.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ _Thread_local int testTlsVariable = 1;
2929
int lastTlsVariableValueInAudioThread = 1;
3030
#endif
3131

32-
// This function will be called for every fixed 128 samples of audio to be processed.
32+
// This function will be called for every fixed-size buffer of audio samples to be processed.
3333
bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) {
3434
#ifdef REPORT_RESULT
3535
assert(testTlsVariable == lastTlsVariableValueInAudioThread);
@@ -40,7 +40,7 @@ bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs,
4040

4141
// Produce noise in all output channels.
4242
for(int i = 0; i < numOutputs; ++i)
43-
for(int j = 0; j < 128*outputs[i].numberOfChannels; ++j)
43+
for(int j = 0; j < outputs[i].quantumSize*outputs[i].numberOfChannels; ++j)
4444
outputs[i].data[j] = (rand() / (float)RAND_MAX * 2.0f - 1.0f) * 0.3f;
4545

4646
// We generated audio and want to keep this processor going. Return false here to shut down.

0 commit comments

Comments
 (0)