Skip to content

Commit

Permalink
[AUDIO_WORKLET] Add multichannel audio tests. NFC (emscripten-core#23394
Browse files Browse the repository at this point in the history
)

These are the audio worklet tests from emscripten-core#22753 extracted to a standalone
PR. The tests are:

- Multiple stereo inputs mixing in the processor to a single stereo
output
- Multiple stereo inputs copying in the processor to multiple stereo
outputs
- Multiple mono inputs mixing in the processor to a single mono output
- Multiple mono inputs copying in the processor to L+R stereo outputs

The tests use different stack sizes (from 2kB to 6kB depending on the
requirement).

The audio tracks were composed by Tim Wright especially for Emscripten
and released under a CC0 license.
  • Loading branch information
cwoffenden authored Jan 15, 2025
1 parent 6bed395 commit bcad96d
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 0 deletions.
12 changes: 12 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5454,6 +5454,18 @@ def test_audio_worklet_post_function(self, args):
def test_audio_worklet_modularize(self, args):
self.btest_exit('webaudio/audioworklet.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-sMODULARIZE=1', '-sEXPORT_NAME=MyModule', '--shell-file', test_file('shell_that_launches_modularize.html')] + args)

# Tests multiple inputs, forcing a larger stack (note: passing BROWSER_TEST is
# specific to this test to allow it to exit rather than play forever).
@parameterized({
'': ([],),
'minimal_with_closure': (['-sMINIMAL_RUNTIME', '--closure=1', '-Oz'],),
})
def test_audio_worklet_stereo_io(self, args):
os.mkdir('audio_files')
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-DBROWSER_TEST'] + args)

def test_error_reporting(self):
# Test catching/reporting Error objects
create_file('post.js', 'throw new Error("oops");')
Expand Down
28 changes: 28 additions & 0 deletions test/test_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,34 @@ def test_audio_worklet_tone_generator(self):
def test_audio_worklet_modularize(self):
self.btest('webaudio/audioworklet.c', expected='0', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-sMINIMAL_RUNTIME', '-sMODULARIZE'])

# Tests an AudioWorklet with multiple stereo inputs mixing in the processor to a single stereo output (4kB stack)
def test_audio_worklet_stereo_io(self):
os.mkdir('audio_files')
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])

# Tests an AudioWorklet with multiple stereo inputs copying in the processor to multiple stereo outputs (6kB stack)
def test_audio_worklet_2x_stereo_io(self):
os.mkdir('audio_files')
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_2x_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])

# Tests an AudioWorklet with multiple mono inputs mixing in the processor to a single mono output (2kB stack)
def test_audio_worklet_mono_io(self):
os.mkdir('audio_files')
shutil.copy(test_file('webaudio/audio_files/emscripten-beat-mono.mp3'), 'audio_files/')
shutil.copy(test_file('webaudio/audio_files/emscripten-bass-mono.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_in_out_mono.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])

# Tests an AudioWorklet with multiple mono inputs copying in the processor to L+R stereo outputs (3kB stack)
def test_audio_worklet_2x_hard_pan_io(self):
os.mkdir('audio_files')
shutil.copy(test_file('webaudio/audio_files/emscripten-beat-mono.mp3'), 'audio_files/')
shutil.copy(test_file('webaudio/audio_files/emscripten-bass-mono.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_2x_in_hard_pan.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])


class interactive64(interactive):
def setUp(self):
Expand Down
5 changes: 5 additions & 0 deletions test/webaudio/audio_files/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Emscripten Beat and Emscripten Bass by [CoLD SToRAGE](https://www.coldstorage.org.uk) (Tim Wright).

Released under the [Creative Commons Zero (CC0)](https://creativecommons.org/publicdomain/zero/1.0/) Public Domain Dedication.

To the extent possible under law, OGP Phonogramatica has waived all copyright and related or neighbouring rights to these works.
Binary file not shown.
Binary file added test/webaudio/audio_files/emscripten-bass.mp3
Binary file not shown.
Binary file not shown.
Binary file added test/webaudio/audio_files/emscripten-beat.mp3
Binary file not shown.
70 changes: 70 additions & 0 deletions test/webaudio/audioworklet_2x_in_hard_pan.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>

#include <emscripten/em_js.h>
#include <emscripten/webaudio.h>

// Tests two mono audio inputs being copied to the left and right channels of a
// single stereo output (with a hard pan).

// This needs to be big enough for the stereo output, 2x mono inputs and the worker stack
#define AUDIO_STACK_SIZE 3072

// Shared file playback and bootstrap
#include "audioworklet_test_shared.inc"

// Callback to process and copy the audio tracks
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
audioProcessedCount++;

// Twin mono in, single stereo out
assert(numInputs == 2 && numOutputs == 1);
assert(inputs[0].numberOfChannels == 1 && inputs[1].numberOfChannels == 1);
assert(outputs[0].numberOfChannels == 2);
// All with the same number of samples
assert(inputs[0].samplesPerChannel == inputs[1].samplesPerChannel);
assert(inputs[0].samplesPerChannel == outputs[0].samplesPerChannel);
// Now with all known quantities we can memcpy the data
int samplesPerChannel = inputs[0].samplesPerChannel;
memcpy(outputs[0].data, inputs[0].data, samplesPerChannel * sizeof(float));
memcpy(outputs[0].data + samplesPerChannel, inputs[1].data, samplesPerChannel * sizeof(float));
return true;
}

// Audio processor created, now register the audio callback
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
if (!success) {
printf("Audio worklet node creation failed\n");
return;
}
printf("Audio worklet processor created\n");
printf("Click to toggle audio playback\n");

// Stereo output, two inputs
int outputChannelCounts[2] = { 2 };
EmscriptenAudioWorkletNodeCreateOptions opts = {
.numberOfInputs = 2,
.numberOfOutputs = 1,
.outputChannelCounts = outputChannelCounts
};
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
emscripten_audio_node_connect(worklet, context, 0, 0);

// Create the two mono source nodes and connect them to the two inputs
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
beatID = createTrack(context, "audio_files/emscripten-beat-mono.mp3", true);
if (beatID) {
emscripten_audio_node_connect(beatID, worklet, 0, 0);
}
bassID = createTrack(context, "audio_files/emscripten-bass-mono.mp3", true);
if (bassID) {
emscripten_audio_node_connect(bassID, worklet, 0, 1);
}

// Register a click to start playback
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);

// Register the counter that exits the test after one second of mixing
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
}
72 changes: 72 additions & 0 deletions test/webaudio/audioworklet_2x_in_out_stereo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>

#include <emscripten/em_js.h>
#include <emscripten/webaudio.h>

// Tests two stereo audio inputs being copied to two stereo outputs.

// This needs to be big enough for the 2x stereo outputs, 2x inputs and the worker stack
#define AUDIO_STACK_SIZE 6144

// Shared file playback and bootstrap
#include "audioworklet_test_shared.inc"

// Callback to process and copy the audio tracks
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
audioProcessedCount++;

// Twin stereo in and out
assert(numInputs == 2 && numOutputs == 2);
assert(inputs[0].numberOfChannels == 2 && inputs[1].numberOfChannels == 2);
assert(outputs[0].numberOfChannels == 2 && outputs[1].numberOfChannels == 2);
// All with the same number of samples
assert(inputs[0].samplesPerChannel == inputs[1].samplesPerChannel);
assert(inputs[0].samplesPerChannel == outputs[0].samplesPerChannel);
assert(outputs[0].samplesPerChannel == outputs[1].samplesPerChannel);
// Now with all known quantities we can memcpy the data
int totalSamples = outputs[0].samplesPerChannel * outputs[0].numberOfChannels;
memcpy(outputs[0].data, inputs[0].data, totalSamples * sizeof(float));
memcpy(outputs[1].data, inputs[1].data, totalSamples * sizeof(float));
return true;
}

// Audio processor created, now register the audio callback
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
if (!success) {
printf("Audio worklet node creation failed\n");
return;
}
printf("Audio worklet processor created\n");
printf("Click to toggle audio playback\n");

// Two stereo outputs, two inputs
int outputChannelCounts[2] = { 2, 2 };
EmscriptenAudioWorkletNodeCreateOptions opts = {
.numberOfInputs = 2,
.numberOfOutputs = 2,
.outputChannelCounts = outputChannelCounts
};
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
// Both outputs connected to the context
emscripten_audio_node_connect(worklet, context, 0, 0);
emscripten_audio_node_connect(worklet, context, 1, 0);

// Create the two stereo source nodes and connect them to the two inputs
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
beatID = createTrack(context, "audio_files/emscripten-beat.mp3", true);
if (beatID) {
emscripten_audio_node_connect(beatID, worklet, 0, 0);
}
bassID = createTrack(context, "audio_files/emscripten-bass.mp3", true);
if (bassID) {
emscripten_audio_node_connect(bassID, worklet, 0, 1);
}

// Register a click to start playback
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);

// Register the counter that exits the test after one second of mixing
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
}
82 changes: 82 additions & 0 deletions test/webaudio/audioworklet_in_out_mono.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>

#include <emscripten/em_js.h>
#include <emscripten/webaudio.h>

// Tests processing two mono audio inputs being mixed to a single mono audio
// output in process() (by adding the inputs together).

// This needs to be big enough for the mono output, 2x inputs and the worker stack
#define AUDIO_STACK_SIZE 2048

// Shared file playback and bootstrap
#include "audioworklet_test_shared.inc"

// Callback to process and mix the audio tracks
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
audioProcessedCount++;

// Single mono output
assert(numOutputs == 1 && outputs[0].numberOfChannels == 1);
for (int n = 0; n < numInputs; n++) {
// And all inputs are also stereo
assert(inputs[n].numberOfChannels == 1 || inputs[n].numberOfChannels == 0);
// This should always be the case
assert(inputs[n].samplesPerChannel == outputs[0].samplesPerChannel);
}
// We can now do a quick mix since we know the layouts
if (numInputs > 0) {
int totalSamples = outputs[0].samplesPerChannel * outputs[0].numberOfChannels;
float* outputData = outputs[0].data;
memcpy(outputData, inputs[0].data, totalSamples * sizeof(float));
for (int n = 1; n < numInputs; n++) {
// It's possible to have an input with no channels
if (inputs[n].numberOfChannels == 1) {
float* inputData = inputs[n].data;
for (int i = totalSamples - 1; i >= 0; i--) {
outputData[i] += inputData[i];
}
}
}
}
return true;
}

// Audio processor created, now register the audio callback
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
if (!success) {
printf("Audio worklet node creation failed\n");
return;
}
printf("Audio worklet processor created\n");
printf("Click to toggle audio playback\n");

// Mono output, two inputs
int outputChannelCounts[1] = { 1 };
EmscriptenAudioWorkletNodeCreateOptions opts = {
.numberOfInputs = 2,
.numberOfOutputs = 1,
.outputChannelCounts = outputChannelCounts
};
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
emscripten_audio_node_connect(worklet, context, 0, 0);

// Create the two mono source nodes and connect them to the two inputs
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
beatID = createTrack(context, "audio_files/emscripten-beat-mono.mp3", true);
if (beatID) {
emscripten_audio_node_connect(beatID, worklet, 0, 0);
}
bassID = createTrack(context, "audio_files/emscripten-bass-mono.mp3", true);
if (bassID) {
emscripten_audio_node_connect(bassID, worklet, 0, 1);
}

// Register a click to start playback
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);

// Register the counter that exits the test after one second of mixing
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
}
82 changes: 82 additions & 0 deletions test/webaudio/audioworklet_in_out_stereo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>

#include <emscripten/em_js.h>
#include <emscripten/webaudio.h>

// Tests processing two stereo audio inputs being mixed to a single stereo audio
// output in process() (by adding the inputs together).

// This needs to be big enough for the stereo output, 2x inputs and the worker stack
#define AUDIO_STACK_SIZE 4096

// Shared file playback and bootstrap
#include "audioworklet_test_shared.inc"

// Callback to process and mix the audio tracks
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
audioProcessedCount++;

// Single stereo output
assert(numOutputs == 1 && outputs[0].numberOfChannels == 2);
for (int n = 0; n < numInputs; n++) {
// And all inputs are also stereo
assert(inputs[n].numberOfChannels == 2 || inputs[n].numberOfChannels == 0);
// This should always be the case
assert(inputs[n].samplesPerChannel == outputs[0].samplesPerChannel);
}
// We can now do a quick mix since we know the layouts
if (numInputs > 0) {
int totalSamples = outputs[0].samplesPerChannel * outputs[0].numberOfChannels;
float* outputData = outputs[0].data;
memcpy(outputData, inputs[0].data, totalSamples * sizeof(float));
for (int n = 1; n < numInputs; n++) {
// It's possible to have an input with no channels
if (inputs[n].numberOfChannels == 2) {
float* inputData = inputs[n].data;
for (int i = totalSamples - 1; i >= 0; i--) {
outputData[i] += inputData[i];
}
}
}
}
return true;
}

// Audio processor created, now register the audio callback
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
if (!success) {
printf("Audio worklet node creation failed\n");
return;
}
printf("Audio worklet processor created\n");
printf("Click to toggle audio playback\n");

// Stereo output, two inputs
int outputChannelCounts[1] = { 2 };
EmscriptenAudioWorkletNodeCreateOptions opts = {
.numberOfInputs = 2,
.numberOfOutputs = 1,
.outputChannelCounts = outputChannelCounts
};
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
emscripten_audio_node_connect(worklet, context, 0, 0);

// Create the two stereo source nodes and connect them to the two inputs
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
beatID = createTrack(context, "audio_files/emscripten-beat.mp3", true);
if (beatID) {
emscripten_audio_node_connect(beatID, worklet, 0, 0);
}
bassID = createTrack(context, "audio_files/emscripten-bass.mp3", true);
if (bassID) {
emscripten_audio_node_connect(bassID, worklet, 0, 1);
}

// Register a click to start playback
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);

// Register the counter that exits the test after one second of mixing
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
}
Loading

0 comments on commit bcad96d

Please sign in to comment.