Skip to content

Commit 5e6f012

Browse files
committed
Standalone multichannel audio tests
1 parent d1efd6c commit 5e6f012

11 files changed

+691
-0
lines changed

test/test_browser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5433,6 +5433,18 @@ def test_audio_worklet_post_function(self, args):
54335433
def test_audio_worklet_modularize(self, args):
54345434
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)
54355435

5436+
# Tests multiple inputs, forcing a larger stack (note: passing BROWSER_TEST is
5437+
# specific to this test to allow it to exit rather than play forever).
5438+
@parameterized({
5439+
'': ([],),
5440+
'minimal_with_closure': (['-sMINIMAL_RUNTIME', '--closure=1', '-Oz'],),
5441+
})
5442+
def test_audio_worklet_stereo_io(self, args):
5443+
os.mkdir('audio_files')
5444+
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
5445+
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
5446+
self.btest_exit('webaudio/audioworklet_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-DBROWSER_TEST'] + args)
5447+
54365448
def test_error_reporting(self):
54375449
# Test catching/reporting Error objects
54385450
create_file('post.js', 'throw new Error("oops");')

test/test_interactive.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,34 @@ def test_audio_worklet_tone_generator(self):
306306
def test_audio_worklet_modularize(self):
307307
self.btest('webaudio/audioworklet.c', expected='0', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-sMINIMAL_RUNTIME', '-sMODULARIZE'])
308308

309+
# Tests an AudioWorklet with multiple stereo inputs mixing in the processor to a single stereo output (4kB stack)
310+
def test_audio_worklet_stereo_io(self):
311+
os.mkdir('audio_files')
312+
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
313+
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
314+
self.btest_exit('webaudio/audioworklet_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])
315+
316+
# Tests an AudioWorklet with multiple stereo inputs copying in the processor to multiple stereo outputs (6kB stack)
317+
def test_audio_worklet_2x_stereo_io(self):
318+
os.mkdir('audio_files')
319+
shutil.copy(test_file('webaudio/audio_files/emscripten-beat.mp3'), 'audio_files/')
320+
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
321+
self.btest_exit('webaudio/audioworklet_2x_in_out_stereo.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])
322+
323+
# Tests an AudioWorklet with multiple mono inputs mixing in the processor to a single mono output (2kB stack)
324+
def test_audio_worklet_mono_io(self):
325+
os.mkdir('audio_files')
326+
shutil.copy(test_file('webaudio/audio_files/emscripten-beat-mono.mp3'), 'audio_files/')
327+
shutil.copy(test_file('webaudio/audio_files/emscripten-bass-mono.mp3'), 'audio_files/')
328+
self.btest_exit('webaudio/audioworklet_in_out_mono.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])
329+
330+
# Tests an AudioWorklet with multiple mono inputs copying in the processor to L+R stereo outputs (3kB stack)
331+
def test_audio_worklet_2x_hard_pan_io(self):
332+
os.mkdir('audio_files')
333+
shutil.copy(test_file('webaudio/audio_files/emscripten-beat-mono.mp3'), 'audio_files/')
334+
shutil.copy(test_file('webaudio/audio_files/emscripten-bass-mono.mp3'), 'audio_files/')
335+
self.btest_exit('webaudio/audioworklet_2x_in_hard_pan.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS'])
336+
309337

310338
class interactive64(interactive):
311339
def setUp(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Emscripten Beat and Emscripten Bass by [CoLD SToRAGE](https://www.coldstorage.org.uk) (Tim Wright).
2+
3+
Released under the [Creative Commons Zero (CC0)](https://creativecommons.org/publicdomain/zero/1.0/) Public Domain Dedication.
4+
5+
To the extent possible under law, OGP Phonogramatica has waived all copyright and related or neighbouring rights to these works.
126 KB
Binary file not shown.
258 KB
Binary file not shown.
125 KB
Binary file not shown.
246 KB
Binary file not shown.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#include <assert.h>
2+
#include <string.h>
3+
#include <stdio.h>
4+
5+
#include <emscripten/em_js.h>
6+
#include <emscripten/webaudio.h>
7+
8+
// Tests two mono audio inputs being copied to the left and right channels of a
9+
// single stereo output (with a hard pan).
10+
11+
// This needs to be big enough for the stereo output, 2x mono inputs and the worker stack
12+
#define AUDIO_STACK_SIZE 3072
13+
14+
// Helper for MEMORY64 to cast an audio context or type to a void*
15+
#define WA_2_VOIDP(ctx) ((void*) (intptr_t) ctx)
16+
// Helper for MEMORY64 to cast a void* to an audio context or type
17+
#define VOIDP_2_WA(ptr) ((EMSCRIPTEN_WEBAUDIO_T) (intptr_t) ptr)
18+
19+
20+
// Count the audio callbacks and return after 375 frames (1 second with the default 128 size)
21+
//
22+
// *** Remove this in your own code ***
23+
//
24+
volatile int audioProcessedCount = 0;
25+
bool playedAndMixed(double time, void* data) {
26+
if (audioProcessedCount >= 375) {
27+
emscripten_force_exit(0);
28+
return false;
29+
}
30+
return true;
31+
}
32+
33+
// ID to the beat and bass loops
34+
EMSCRIPTEN_WEBAUDIO_T beatID = 0;
35+
EMSCRIPTEN_WEBAUDIO_T bassID = 0;
36+
37+
// Creates a MediaElementAudioSourceNode with the supplied URL (which is
38+
// registered as an internal audio object and the ID returned).
39+
EM_JS(EMSCRIPTEN_WEBAUDIO_T, createTrack, (EMSCRIPTEN_WEBAUDIO_T ctxID, const char* url, bool looping), {
40+
var context = emscriptenGetAudioObject(ctxID);
41+
if (context) {
42+
var audio = document.createElement('audio');
43+
audio.src = UTF8ToString(url);
44+
audio.loop = looping;
45+
var track = context.createMediaElementSource(audio);
46+
return emscriptenRegisterAudioObject(track);
47+
}
48+
return 0;
49+
});
50+
51+
// Toggles the play/pause of a MediaElementAudioSourceNode given its ID
52+
EM_JS(void, toggleTrack, (EMSCRIPTEN_WEBAUDIO_T srcID), {
53+
var source = emscriptenGetAudioObject(srcID);
54+
if (source) {
55+
var audio = source.mediaElement;
56+
if (audio) {
57+
if (audio.paused) {
58+
audio.currentTime = 0;
59+
audio.play();
60+
} else {
61+
audio.pause();
62+
}
63+
}
64+
}
65+
});
66+
67+
// Callback to process and copy the audio tracks
68+
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
69+
audioProcessedCount++;
70+
71+
// Twin mono in, single stereo out
72+
assert(numInputs == 2 && numOutputs == 1);
73+
assert(inputs[0].numberOfChannels == 1 && inputs[1].numberOfChannels == 1);
74+
assert(outputs[0].numberOfChannels == 2);
75+
// All with the same number of samples
76+
assert(inputs[0].samplesPerChannel == inputs[1].samplesPerChannel);
77+
assert(inputs[0].samplesPerChannel == outputs[0].samplesPerChannel);
78+
// Now with all known quantities we can memcpy the data
79+
int samplesPerChannel = inputs[0].samplesPerChannel;
80+
memcpy(outputs[0].data, inputs[0].data, samplesPerChannel * sizeof(float));
81+
memcpy(outputs[0].data + samplesPerChannel, inputs[1].data, samplesPerChannel * sizeof(float));
82+
return true;
83+
}
84+
85+
// Registered click even to (1) enable audio playback and (2) toggle playing the tracks
86+
bool onClick(int type, const EmscriptenMouseEvent* e, void* data) {
87+
EMSCRIPTEN_WEBAUDIO_T ctx = VOIDP_2_WA(data);
88+
if (emscripten_audio_context_state(ctx) != AUDIO_CONTEXT_STATE_RUNNING) {
89+
printf("Resuming playback\n");
90+
emscripten_resume_audio_context_sync(ctx);
91+
}
92+
printf("Toggling audio playback\n");
93+
toggleTrack(beatID);
94+
toggleTrack(bassID);
95+
return false;
96+
}
97+
98+
// Audio processor created, now register the audio callback
99+
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
100+
if (success) {
101+
printf("Audio worklet processor created\n");
102+
printf("Click to toggle audio playback\n");
103+
104+
// Stereo output, two inputs
105+
int outputChannelCounts[2] = { 2 };
106+
EmscriptenAudioWorkletNodeCreateOptions opts = {
107+
.numberOfInputs = 2,
108+
.numberOfOutputs = 1,
109+
.outputChannelCounts = outputChannelCounts
110+
};
111+
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
112+
emscripten_audio_node_connect(worklet, context, 0, 0);
113+
114+
// Create the two mono source nodes and connect them to the two inputs
115+
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
116+
beatID = createTrack(context, "audio_files/emscripten-beat-mono.mp3", true);
117+
if (beatID) {
118+
emscripten_audio_node_connect(beatID, worklet, 0, 0);
119+
}
120+
bassID = createTrack(context, "audio_files/emscripten-bass-mono.mp3", true);
121+
if (bassID) {
122+
emscripten_audio_node_connect(bassID, worklet, 0, 1);
123+
}
124+
125+
// Register a click to start playback
126+
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);
127+
128+
// Register the counter that exits the test after one second of mixing
129+
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
130+
} else {
131+
printf("Audio worklet node creation failed\n");
132+
}
133+
}
134+
135+
// Worklet thread inited, now create the audio processor
136+
void initialised(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
137+
if (success) {
138+
printf("Audio worklet initialised\n");
139+
140+
WebAudioWorkletProcessorCreateOptions opts = {
141+
.name = "mixer",
142+
};
143+
emscripten_create_wasm_audio_worklet_processor_async(context, &opts, &processorCreated, NULL);
144+
} else {
145+
printf("Audio worklet failed to initialise\n");
146+
}
147+
}
148+
149+
int main() {
150+
static char workletStack[AUDIO_STACK_SIZE];
151+
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL);
152+
emscripten_start_wasm_audio_worklet_thread_async(context, workletStack, sizeof workletStack, &initialised, NULL);
153+
emscripten_runtime_keepalive_push();
154+
return 0;
155+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#include <assert.h>
2+
#include <string.h>
3+
#include <stdio.h>
4+
5+
#include <emscripten/em_js.h>
6+
#include <emscripten/webaudio.h>
7+
8+
// Tests two stereo audio inputs being copied to two stereo outputs.
9+
10+
// This needs to be big enough for the 2x stereo outputs, 2x inputs and the worker stack
11+
#define AUDIO_STACK_SIZE 6144
12+
13+
// Helper for MEMORY64 to cast an audio context or type to a void*
14+
#define WA_2_VOIDP(ctx) ((void*) (intptr_t) ctx)
15+
// Helper for MEMORY64 to cast a void* to an audio context or type
16+
#define VOIDP_2_WA(ptr) ((EMSCRIPTEN_WEBAUDIO_T) (intptr_t) ptr)
17+
18+
// Count the audio callbacks and return after 375 frames (1 second with the default 128 size)
19+
//
20+
// *** Remove this in your own code ***
21+
//
22+
volatile int audioProcessedCount = 0;
23+
bool playedAndMixed(double time, void* data) {
24+
if (audioProcessedCount >= 375) {
25+
emscripten_force_exit(0);
26+
return false;
27+
}
28+
return true;
29+
}
30+
31+
// ID to the beat and bass loops
32+
EMSCRIPTEN_WEBAUDIO_T beatID = 0;
33+
EMSCRIPTEN_WEBAUDIO_T bassID = 0;
34+
35+
// Creates a MediaElementAudioSourceNode with the supplied URL (which is
36+
// registered as an internal audio object and the ID returned).
37+
EM_JS(EMSCRIPTEN_WEBAUDIO_T, createTrack, (EMSCRIPTEN_WEBAUDIO_T ctxID, const char* url, bool looping), {
38+
var context = emscriptenGetAudioObject(ctxID);
39+
if (context) {
40+
var audio = document.createElement('audio');
41+
audio.src = UTF8ToString(url);
42+
audio.loop = looping;
43+
var track = context.createMediaElementSource(audio);
44+
return emscriptenRegisterAudioObject(track);
45+
}
46+
return 0;
47+
});
48+
49+
// Toggles the play/pause of a MediaElementAudioSourceNode given its ID
50+
EM_JS(void, toggleTrack, (EMSCRIPTEN_WEBAUDIO_T srcID), {
51+
var source = emscriptenGetAudioObject(srcID);
52+
if (source) {
53+
var audio = source.mediaElement;
54+
if (audio) {
55+
if (audio.paused) {
56+
audio.currentTime = 0;
57+
audio.play();
58+
} else {
59+
audio.pause();
60+
}
61+
}
62+
}
63+
});
64+
65+
// Callback to process and copy the audio tracks
66+
bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) {
67+
audioProcessedCount++;
68+
69+
// Twin stereo in and out
70+
assert(numInputs == 2 && numOutputs == 2);
71+
assert(inputs[0].numberOfChannels == 2 && inputs[1].numberOfChannels == 2);
72+
assert(outputs[0].numberOfChannels == 2 && outputs[1].numberOfChannels == 2);
73+
// All with the same number of samples
74+
assert(inputs[0].samplesPerChannel == inputs[1].samplesPerChannel);
75+
assert(inputs[0].samplesPerChannel == outputs[0].samplesPerChannel);
76+
assert(outputs[0].samplesPerChannel == outputs[1].samplesPerChannel);
77+
// Now with all known quantities we can memcpy the data
78+
int totalSamples = outputs[0].samplesPerChannel * outputs[0].numberOfChannels;
79+
memcpy(outputs[0].data, inputs[0].data, totalSamples * sizeof(float));
80+
memcpy(outputs[1].data, inputs[1].data, totalSamples * sizeof(float));
81+
return true;
82+
}
83+
84+
// Registered click even to (1) enable audio playback and (2) toggle playing the tracks
85+
bool onClick(int type, const EmscriptenMouseEvent* e, void* data) {
86+
EMSCRIPTEN_WEBAUDIO_T ctx = VOIDP_2_WA(data);
87+
if (emscripten_audio_context_state(ctx) != AUDIO_CONTEXT_STATE_RUNNING) {
88+
printf("Resuming playback\n");
89+
emscripten_resume_audio_context_sync(ctx);
90+
}
91+
printf("Toggling audio playback\n");
92+
toggleTrack(beatID);
93+
toggleTrack(bassID);
94+
return false;
95+
}
96+
97+
// Audio processor created, now register the audio callback
98+
void processorCreated(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
99+
if (success) {
100+
printf("Audio worklet processor created\n");
101+
printf("Click to toggle audio playback\n");
102+
103+
// Two stereo outputs, two inputs
104+
int outputChannelCounts[2] = { 2, 2 };
105+
EmscriptenAudioWorkletNodeCreateOptions opts = {
106+
.numberOfInputs = 2,
107+
.numberOfOutputs = 2,
108+
.outputChannelCounts = outputChannelCounts
109+
};
110+
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(context, "mixer", &opts, &process, NULL);
111+
// Both outputs connected to the context
112+
emscripten_audio_node_connect(worklet, context, 0, 0);
113+
emscripten_audio_node_connect(worklet, context, 1, 0);
114+
115+
// Create the two stereo source nodes and connect them to the two inputs
116+
// Note: we can connect the sources to the same input and it'll get mixed for us, but that's not the point
117+
beatID = createTrack(context, "audio_files/emscripten-beat.mp3", true);
118+
if (beatID) {
119+
emscripten_audio_node_connect(beatID, worklet, 0, 0);
120+
}
121+
bassID = createTrack(context, "audio_files/emscripten-bass.mp3", true);
122+
if (bassID) {
123+
emscripten_audio_node_connect(bassID, worklet, 0, 1);
124+
}
125+
126+
// Register a click to start playback
127+
emscripten_set_click_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, WA_2_VOIDP(context), false, &onClick);
128+
129+
// Register the counter that exits the test after one second of mixing
130+
emscripten_set_timeout_loop(&playedAndMixed, 16, NULL);
131+
} else {
132+
printf("Audio worklet node creation failed\n");
133+
}
134+
}
135+
136+
// Worklet thread inited, now create the audio processor
137+
void initialised(EMSCRIPTEN_WEBAUDIO_T context, bool success, void* data) {
138+
if (success) {
139+
printf("Audio worklet initialised\n");
140+
141+
WebAudioWorkletProcessorCreateOptions opts = {
142+
.name = "mixer",
143+
};
144+
emscripten_create_wasm_audio_worklet_processor_async(context, &opts, &processorCreated, NULL);
145+
} else {
146+
printf("Audio worklet failed to initialise\n");
147+
}
148+
}
149+
150+
int main() {
151+
static char workletStack[AUDIO_STACK_SIZE];
152+
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL);
153+
emscripten_start_wasm_audio_worklet_thread_async(context, workletStack, sizeof workletStack, &initialised, NULL);
154+
emscripten_runtime_keepalive_push();
155+
return 0;
156+
}

0 commit comments

Comments
 (0)