Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AUDIO_WORKLET] Added API for getting the buffer's quantum size #22681

Merged
merged 10 commits into from
Oct 15, 2024
6 changes: 4 additions & 2 deletions site/source/docs/api_reference/wasm_audio_worklets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ processing graph as AudioWorkletNodes.

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

This callback will be executed on a dedicated separate audio processing
thread with real-time processing priority. Each Web Audio context will
Expand Down Expand Up @@ -157,7 +159,7 @@ which resumes the audio context when the user clicks on the DOM Canvas element t
void *userData)
{
for(int i = 0; i < numOutputs; ++i)
for(int j = 0; j < 128*outputs[i].numberOfChannels; ++j)
for(int j = 0; j < outputs[i].quantumSize*outputs[i].numberOfChannels; ++j)
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

return true; // Keep the graph output going
Expand Down
44 changes: 28 additions & 16 deletions src/audio_worklet.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ function createWasmAudioWorkletProcessor(audioParams) {
let opts = args.processorOptions;
this.callbackFunction = Module['wasmTable'].get(opts['cb']);
this.userData = opts['ud'];
// Plus the number of samples to process, fixed for the lifetime of the
// context that created this processor. Note for when moving to Web Audio
// 1.1: the typed array passed to process() should be the same size as the
// the quantum size, and this exercise of passing in the value shouldn't
// be required (to be verified).
this.quantumSize = opts['qs'];
}

static get parameterDescriptors() {
Expand All @@ -45,53 +51,59 @@ function createWasmAudioWorkletProcessor(audioParams) {
let numInputs = inputList.length,
numOutputs = outputList.length,
numParams = 0, i, j, k, dataPtr,
stackMemoryNeeded = (numInputs + numOutputs) * 8,
quantumBytes = this.quantumSize * 4,
stackMemoryNeeded = (numInputs + numOutputs) * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}},
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;
for (i of inputList) stackMemoryNeeded += i.length * quantumBytes;
for (i of outputList) stackMemoryNeeded += i.length * quantumBytes;
for (i in parameters) stackMemoryNeeded += parameters[i].byteLength + {{{ C_STRUCTS.AudioParamFrame.__size__ }}}, ++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;
dataPtr = inputsPtr + numInputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}};
for (i of inputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k++] = i.length;
HEAPU32[k++] = dataPtr;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.quantumSize / 4 }}}] = this.quantumSize;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
// Marshal the input audio sample data for each audio channel of this input
for (j of i) {
HEAPF32.set(j, dataPtr>>2);
dataPtr += 512;
dataPtr += quantumBytes;
}
}

// Copy output audio descriptor structs to Wasm
outputsPtr = dataPtr;
k = outputsPtr >> 2;
outputDataPtr = (dataPtr += numOutputs * 8) >> 2;
outputDataPtr = (dataPtr += numOutputs * {{{ C_STRUCTS.AudioSampleFrame.__size__ }}}) >> 2;
for (i of outputList) {
// Write the AudioSampleFrame struct instance
HEAPU32[k++] = i.length;
HEAPU32[k++] = dataPtr;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.numberOfChannels / 4 }}}] = i.length;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.quantumSize / 4 }}}] = this.quantumSize;
HEAPU32[k + {{{ C_STRUCTS.AudioSampleFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioSampleFrame.__size__ / 4 }}};
// Reserve space for the output data
dataPtr += 512 * i.length;
dataPtr += quantumBytes * i.length;
}

// Copy parameters descriptor structs and data to Wasm
paramsPtr = dataPtr;
k = paramsPtr >> 2;
dataPtr += numParams * 8;
dataPtr += numParams * {{{ C_STRUCTS.AudioParamFrame.__size__ }}};
for (i = 0; paramArray = parameters[i++];) {
// Write the AudioParamFrame struct instance
HEAPU32[k++] = paramArray.length;
HEAPU32[k++] = dataPtr;
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.length / 4 }}}] = paramArray.length;
HEAPU32[k + {{{ C_STRUCTS.AudioParamFrame.data / 4 }}}] = dataPtr;
k += {{{ C_STRUCTS.AudioParamFrame.__size__ / 4 }}};
// Marshal the audio parameters array
HEAPF32.set(paramArray, dataPtr>>2);
dataPtr += paramArray.length*4;
Expand All @@ -105,7 +117,7 @@ function createWasmAudioWorkletProcessor(audioParams) {
// not have one, so manually copy all bytes in)
for (i of outputList) {
for (j of i) {
for (k = 0; k < 128; ++k) {
for (k = 0; k < this.quantumSize; ++k) {
j[k] = HEAPF32[outputDataPtr++];
}
}
Expand Down
1 change: 1 addition & 0 deletions src/library_sigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ sigs = {
emscripten_atomic_cancel_wait_async__sig: 'ii',
emscripten_atomic_wait_async__sig: 'ipippd',
emscripten_atomics_is_lock_free__sig: 'ii',
emscripten_audio_context_quantum_size__sig: 'ii',
emscripten_audio_context_state__sig: 'ii',
emscripten_audio_node_connect__sig: 'viiii',
emscripten_audio_worklet_post_function_sig__sig: 'vippp',
Expand Down
30 changes: 27 additions & 3 deletions src/library_webaudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,21 @@ let LibraryWebAudio = {
// Wasm handle ID.
$emscriptenGetAudioObject: (objectHandle) => EmAudio[objectHandle],

// emscripten_create_audio_context() does not itself use
// Performs the work of getting the AudioContext's quantum size.
$emscriptenGetContextQuantumSize: (contextHandle) => {
// TODO: in a future release this will be something like:
// return EmAudio[contextHandle].renderQuantumSize || 128;
// It comes two caveats: it needs the hint when generating the context adding to
// emscripten_create_audio_context(), and altering the quantum requires a secure
// context and fallback implementing. Until then we simply use the 1.0 API value:
return 128;
Copy link
Collaborator

@juj juj Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this code already today read

return EmAudio[contextHandle]['renderQuantumSize'] || 128;

That way the code would also be verifying that contextHandle is a valid context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but I was worried about its [SecureContext] marking in the spec and what that entails, and also since we can't yet set it then it doesn't make a difference.

But I'll happily change it and look at it again if/when it breaks, and when the API allows for the setting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, works either way.

[SecureContext] just means that the parameter won't be populated if the page is served via an insecure http:// handler. The value won't then be present, and will return undefined.

(http://localhost URLs get a special case and is always considered secure).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern was it'd throw an exception, not just silently return undefined.

(For localhost it still needs Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers for audio demos to run, BTW, since SharedArrayBuffer isn't available and throws a reference error)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern was it'd throw an exception, not just silently return undefined.

Ah, right. An exception won't happen, that is guaranteed by the IDL spec of [SecureContext]: https://www.w3.org/TR/secure-contexts/#integration-idl which states multiple times

image

But let's leave this later for when an implementation comes along.

},

// emscripten_create_audio_context() does not itself use the
// emscriptenGetAudioObject() function, but mark it as a dependency, because
// the user will not be able to utilize the node unless they call
// emscriptenGetAudioObject() on it on JS side to connect it to the graph, so
// this avoids the user needing to manually do it on the command line.
// this avoids the user needing to manually add the dependency on the command line.
emscripten_create_audio_context__deps: ['$emscriptenRegisterAudioObject', '$emscriptenGetAudioObject'],
emscripten_create_audio_context: (options) => {
let ctx = window.AudioContext || window.webkitAudioContext;
Expand Down Expand Up @@ -264,6 +274,7 @@ let LibraryWebAudio = {
});
},

emscripten_create_wasm_audio_worklet_node__deps: ['$emscriptenGetContextQuantumSize'],
emscripten_create_wasm_audio_worklet_node: (contextHandle, name, options, callback, userData) => {
#if ASSERTIONS
assert(contextHandle, `Called emscripten_create_wasm_audio_worklet_node() with a null Web Audio Context handle!`);
Expand All @@ -282,7 +293,11 @@ let LibraryWebAudio = {
numberOfInputs: HEAP32[options],
numberOfOutputs: HEAP32[options+1],
outputChannelCount: HEAPU32[options+2] ? readChannelCountArray(HEAPU32[options+2]>>2, HEAP32[options+1]) : void 0,
processorOptions: { 'cb': callback, 'ud': userData }
processorOptions: {
'cb': callback,
'ud': userData,
'qs': emscriptenGetContextQuantumSize(contextHandle)
}
} : void 0;

#if WEBAUDIO_DEBUG
Expand All @@ -293,6 +308,15 @@ let LibraryWebAudio = {
},
#endif // ~AUDIO_WORKLET

emscripten_audio_context_quantum_size__deps: ['$emscriptenGetContextQuantumSize'],
emscripten_audio_context_quantum_size: (contextHandle) => {
#if ASSERTIONS
assert(EmAudio[contextHandle], `Called emscripten_audio_context_quantum_size() with an invalid Web Audio Context handle ${contextHandle}`);
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]}`);
#endif
return emscriptenGetContextQuantumSize(contextHandle);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would delete the second function $emscriptenGetContextQuantumSize and just have one function emscripten_audio_context_quantum_size() and use that - it would be simpler that way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One was to expose it to JS for use in the worklet or in other code, the other native. Originally I didn't pass the value to the worklet, instead reading it from this JS function, but you'd said to demonstrate the value being passed.

I can re-look at this.

Copy link
Contributor Author

@cwoffenden cwoffenden Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$emscriptenGetContextQuantumSize() gets called in two places from JS (one to pass to now pass to the worklet, the other to expose to native). I guess I could call emscripten_audio_context_quantum_size() from emscripten_create_wasm_audio_worklet_node() since both are JS in the end.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both versions of the function receive the handle ID? So in that case, I think it would be fine to call _emscripten_audio_context_quantum_size() from JS code as well. (need to add the underscore at the call site though, since the function won't start with a $ prefix)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both versions of the function receive the handle ID?

I can tidy this in another PR.


emscripten_audio_node_connect: (source, destination, outputIndex, inputIndex) => {
var srcNode = EmAudio[source];
var dstNode = EmAudio[destination];
Expand Down
14 changes: 14 additions & 0 deletions src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,20 @@
]
}
},
{
"file": "emscripten/webaudio.h",
"structs": {
"AudioSampleFrame": [
"numberOfChannels",
"quantumSize",
"data"
],
"AudioParamFrame": [
"length",
"data"
]
}
},
{
"file": "AL/al.h",
"defines": [
Expand Down
11 changes: 11 additions & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,17 @@
"__WASI_RIGHTS_SOCK_SHUTDOWN": 268435456
},
"structs": {
"AudioParamFrame": {
"__size__": 8,
"data": 4,
"length": 0
},
"AudioSampleFrame": {
"__size__": 12,
"data": 8,
"numberOfChannels": 0,
"quantumSize": 4
},
"EmscriptenBatteryEvent": {
"__size__": 32,
"charging": 24,
Expand Down
11 changes: 11 additions & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,17 @@
"__WASI_RIGHTS_SOCK_SHUTDOWN": 268435456
},
"structs": {
"AudioParamFrame": {
"__size__": 16,
"data": 8,
"length": 0
},
"AudioSampleFrame": {
"__size__": 16,
"data": 8,
"numberOfChannels": 0,
"quantumSize": 4
},
"EmscriptenBatteryEvent": {
"__size__": 32,
"charging": 24,
Expand Down
12 changes: 10 additions & 2 deletions system/include/emscripten/webaudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,27 @@ typedef void (*EmscriptenWorkletProcessorCreatedCallback)(EMSCRIPTEN_WEBAUDIO_T
// 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.
void emscripten_create_wasm_audio_worklet_processor_async(EMSCRIPTEN_WEBAUDIO_T audioContext, const WebAudioWorkletProcessorCreateOptions *options, EmscriptenWorkletProcessorCreatedCallback callback, void *userData3);

// 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.
// 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.
int emscripten_audio_context_quantum_size(EMSCRIPTEN_WEBAUDIO_T audioContext);
cwoffenden marked this conversation as resolved.
Show resolved Hide resolved

typedef int EMSCRIPTEN_AUDIO_WORKLET_NODE_T;

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

typedef struct AudioParamFrame
{
// Specifies the length of the input array data (in float elements). This will be guaranteed to either have
// a value of 1 or 128, depending on whether the audio parameter changed during this frame.
// 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.
int length;
// An array of length specified in 'length'.
float *data;
Expand Down
14 changes: 11 additions & 3 deletions test/webaudio/audio_worklet_tone_generator.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <emscripten/webaudio.h>
#include <emscripten/em_math.h>

#include <stdio.h>

// This program tests that sharing the WebAssembly Memory works between the
// audio generator thread and the main browser UI thread. Two sliders,
// frequency and volume, can be adjusted on the HTML page, and the audio thread
Expand All @@ -25,7 +27,7 @@ float currentVolume = 0.3; // [local variable to the audio thread]
volatile int audioProcessedCount = 0;
#endif

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

// Produce a sine wave tone of desired frequency to all output channels.
for(int o = 0; o < numOutputs; ++o)
for(int i = 0; i < 128; ++i)
for(int i = 0; i < outputs[o].quantumSize; ++i)
{
float s = emscripten_math_sin(phase);
phase += phaseIncrement;
for(int ch = 0; ch < outputs[o].numberOfChannels; ++ch)
outputs[o].data[ch*128 + i] = s * currentVolume;
outputs[o].data[ch*outputs[o].quantumSize + i] = s * currentVolume;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the quantum size is a constant, I wonder if it would be better to have all these examples use that global quantumSize integer, e.g.

int quantumSize;

bool ProcessAudio(int numInputs, ...) {
  for(int o = 0; o < numOutputs; ++o)
    for(int i = 0; i < quantumSize; ++i)
      ...
}

int main() {
  quantumSize = emscripten_audio_context_quantum_size(context);
}

This would communicate the invariant that the quantumSize cannot change between inputs and outputs in a single call to ProcessAudio(). That might help someone learning about AudioWorklets.

Alternatively, I wonder if the field AudioSampleFrame::quantumSize might be simpler to rename to AudioSampleFrame::length to signal that it just means the length of the data array. Then users could have

for(int i = 0; i < outputs[o].length; ++i) {
  outputs[o].data[i] = ...;

with very idiomatic reading C code. The "quantumSize" name in this context reads fancier than it necessarily needs to be (and doesn't match the Web Audio spec which calls it "renderQuantumSize").

Works either way, leaving it up to you to decide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like setting it as a global, and I agree, I also don't like the name quantumSize either (or renderQuantumSize or the use of quantum in general for this, or the way audio frame is used, etc.).

If I'm going to spend more time with this I'd rather that be spent marking the existing API as deprecated and creating a new callback signature.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, that makes sense. 👍

Copy link
Contributor Author

@cwoffenden cwoffenden Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made #22741 since it was easy to do (for me) now this has landed, that calls it samplesPerChannel and bytesPerChannel, which I think is better wording. I only left in emscripten_audio_context_quantum_size() because that's what the API (almost) calls it which affects only advanced API usage.

}

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

EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(&attrs);

// Get the context's quantum size. Once the audio API allows this to be user
// defined or exposes the hardware's own value, this will be needed to
// determine the worklet stack size.
int quantumSize = emscripten_audio_context_quantum_size(context);
printf("Context quantum size: %d\n", quantumSize);

// and kick off Audio Worklet scope initialization, which shares the Wasm
// Module and Memory to the AudioWorklet scope and initializes its stack.
emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0);
Expand Down
4 changes: 2 additions & 2 deletions test/webaudio/audioworklet.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ _Thread_local int testTlsVariable = 1;
int lastTlsVariableValueInAudioThread = 1;
#endif

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

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

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