1 /*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2016 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20 */
21 #include "../../SDL_internal.h"
22
23 #if SDL_AUDIO_DRIVER_EMSCRIPTEN
24
25 #include "SDL_audio.h"
26 #include "SDL_log.h"
27 #include "../SDL_audio_c.h"
28 #include "SDL_emscriptenaudio.h"
29
30 #include <emscripten/emscripten.h>
31
32 static int
copyData(_THIS)33 copyData(_THIS)
34 {
35 int byte_len;
36
37 if (this->hidden->write_off + this->convert.len_cvt > this->hidden->mixlen) {
38 if (this->hidden->write_off > this->hidden->read_off) {
39 SDL_memmove(this->hidden->mixbuf,
40 this->hidden->mixbuf + this->hidden->read_off,
41 this->hidden->mixlen - this->hidden->read_off);
42 this->hidden->write_off = this->hidden->write_off - this->hidden->read_off;
43 } else {
44 this->hidden->write_off = 0;
45 }
46 this->hidden->read_off = 0;
47 }
48
49 SDL_memcpy(this->hidden->mixbuf + this->hidden->write_off,
50 this->convert.buf,
51 this->convert.len_cvt);
52 this->hidden->write_off += this->convert.len_cvt;
53 byte_len = this->hidden->write_off - this->hidden->read_off;
54
55 return byte_len;
56 }
57
58 static void
HandleAudioProcess(_THIS)59 HandleAudioProcess(_THIS)
60 {
61 Uint8 *buf = NULL;
62 int byte_len = 0;
63 int bytes = SDL_AUDIO_BITSIZE(this->spec.format) / 8;
64
65 /* Only do something if audio is enabled */
66 if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
67 return;
68 }
69
70 if (this->convert.needed) {
71 const int bytes_in = SDL_AUDIO_BITSIZE(this->convert.src_format) / 8;
72
73 if (this->hidden->conv_in_len != 0) {
74 this->convert.len = this->hidden->conv_in_len * bytes_in * this->spec.channels;
75 }
76
77 (*this->spec.callback) (this->spec.userdata,
78 this->convert.buf,
79 this->convert.len);
80 SDL_ConvertAudio(&this->convert);
81 buf = this->convert.buf;
82 byte_len = this->convert.len_cvt;
83
84 /* size mismatch*/
85 if (byte_len != this->spec.size) {
86 if (!this->hidden->mixbuf) {
87 this->hidden->mixlen = this->spec.size > byte_len ? this->spec.size * 2 : byte_len * 2;
88 this->hidden->mixbuf = SDL_malloc(this->hidden->mixlen);
89 }
90
91 /* copy existing data */
92 byte_len = copyData(this);
93
94 /* read more data*/
95 while (byte_len < this->spec.size) {
96 (*this->spec.callback) (this->spec.userdata,
97 this->convert.buf,
98 this->convert.len);
99 SDL_ConvertAudio(&this->convert);
100 byte_len = copyData(this);
101 }
102
103 byte_len = this->spec.size;
104 buf = this->hidden->mixbuf + this->hidden->read_off;
105 this->hidden->read_off += byte_len;
106 }
107
108 } else {
109 if (!this->hidden->mixbuf) {
110 this->hidden->mixlen = this->spec.size;
111 this->hidden->mixbuf = SDL_malloc(this->hidden->mixlen);
112 }
113 (*this->spec.callback) (this->spec.userdata,
114 this->hidden->mixbuf,
115 this->hidden->mixlen);
116 buf = this->hidden->mixbuf;
117 byte_len = this->hidden->mixlen;
118 }
119
120 if (buf) {
121 EM_ASM_ARGS({
122 var numChannels = SDL2.audio.currentOutputBuffer['numberOfChannels'];
123 for (var c = 0; c < numChannels; ++c) {
124 var channelData = SDL2.audio.currentOutputBuffer['getChannelData'](c);
125 if (channelData.length != $1) {
126 throw 'Web Audio output buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
127 }
128
129 for (var j = 0; j < $1; ++j) {
130 channelData[j] = HEAPF32[$0 + ((j*numChannels + c) << 2) >> 2];
131 }
132 }
133 }, buf, byte_len / bytes / this->spec.channels);
134 }
135 }
136
137 static void
HandleCaptureProcess(_THIS)138 HandleCaptureProcess(_THIS)
139 {
140 Uint8 *buf;
141 int buflen;
142
143 /* Only do something if audio is enabled */
144 if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
145 return;
146 }
147
148 if (this->convert.needed) {
149 buf = this->convert.buf;
150 buflen = this->convert.len_cvt;
151 } else {
152 if (!this->hidden->mixbuf) {
153 this->hidden->mixbuf = (Uint8 *) SDL_malloc(this->spec.size);
154 if (!this->hidden->mixbuf) {
155 return; /* oh well. */
156 }
157 }
158 buf = this->hidden->mixbuf;
159 buflen = this->spec.size;
160 }
161
162 EM_ASM_ARGS({
163 var numChannels = SDL2.capture.currentCaptureBuffer.numberOfChannels;
164 if (numChannels == 1) { /* fastpath this a little for the common (mono) case. */
165 var channelData = SDL2.capture.currentCaptureBuffer.getChannelData(0);
166 if (channelData.length != $1) {
167 throw 'Web Audio capture buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
168 }
169 for (var j = 0; j < $1; ++j) {
170 setValue($0 + (j * 4), channelData[j], 'float');
171 }
172 } else {
173 for (var c = 0; c < numChannels; ++c) {
174 var channelData = SDL2.capture.currentCaptureBuffer.getChannelData(c);
175 if (channelData.length != $1) {
176 throw 'Web Audio capture buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
177 }
178
179 for (var j = 0; j < $1; ++j) {
180 setValue($0 + (((j * numChannels) + c) * 4), channelData[j], 'float');
181 }
182 }
183 }
184 }, buf, (this->spec.size / sizeof (float)) / this->spec.channels);
185
186 /* okay, we've got an interleaved float32 array in C now. */
187
188 if (this->convert.needed) {
189 SDL_ConvertAudio(&this->convert);
190 }
191
192 /* Send it to the app. */
193 (*this->spec.callback) (this->spec.userdata, buf, buflen);
194 }
195
196
197
198 static void
EMSCRIPTENAUDIO_CloseDevice(_THIS)199 EMSCRIPTENAUDIO_CloseDevice(_THIS)
200 {
201 EM_ASM_({
202 if ($0) {
203 if (SDL2.capture.silenceTimer !== undefined) {
204 clearTimeout(SDL2.capture.silenceTimer);
205 }
206 if (SDL2.capture.stream !== undefined) {
207 var tracks = SDL2.capture.stream.getAudioTracks();
208 for (var i = 0; i < tracks.length; i++) {
209 SDL2.capture.stream.removeTrack(tracks[i]);
210 }
211 SDL2.capture.stream = undefined;
212 }
213 if (SDL2.capture.scriptProcessorNode !== undefined) {
214 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {};
215 SDL2.capture.scriptProcessorNode.disconnect();
216 SDL2.capture.scriptProcessorNode = undefined;
217 }
218 if (SDL2.capture.mediaStreamNode !== undefined) {
219 SDL2.capture.mediaStreamNode.disconnect();
220 SDL2.capture.mediaStreamNode = undefined;
221 }
222 if (SDL2.capture.silenceBuffer !== undefined) {
223 SDL2.capture.silenceBuffer = undefined
224 }
225 SDL2.capture = undefined;
226 } else {
227 if (SDL2.audio.scriptProcessorNode != undefined) {
228 SDL2.audio.scriptProcessorNode.disconnect();
229 SDL2.audio.scriptProcessorNode = undefined;
230 }
231 SDL2.audio = undefined;
232 }
233 if ((SDL2.audioContext !== undefined) && (SDL2.audio === undefined) && (SDL2.capture === undefined)) {
234 SDL2.audioContext.close();
235 SDL2.audioContext = undefined;
236 }
237 }, this->iscapture);
238
239 SDL_free(this->hidden->mixbuf);
240 SDL_free(this->hidden);
241 }
242
243 static int
EMSCRIPTENAUDIO_OpenDevice(_THIS,void * handle,const char * devname,int iscapture)244 EMSCRIPTENAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
245 {
246 SDL_bool valid_format = SDL_FALSE;
247 SDL_AudioFormat test_format;
248 int i;
249 float f;
250 int result;
251
252 /* based on parts of library_sdl.js */
253
254 /* create context (TODO: this puts stuff in the global namespace...)*/
255 result = EM_ASM_INT({
256 if(typeof(SDL2) === 'undefined') {
257 SDL2 = {};
258 }
259 if (!$0) {
260 SDL2.audio = {};
261 } else {
262 SDL2.capture = {};
263 }
264
265 if (!SDL2.audioContext) {
266 if (typeof(AudioContext) !== 'undefined') {
267 SDL2.audioContext = new AudioContext();
268 } else if (typeof(webkitAudioContext) !== 'undefined') {
269 SDL2.audioContext = new webkitAudioContext();
270 }
271 }
272 return SDL2.audioContext === undefined ? -1 : 0;
273 }, iscapture);
274 if (result < 0) {
275 return SDL_SetError("Web Audio API is not available!");
276 }
277
278 test_format = SDL_FirstAudioFormat(this->spec.format);
279 while ((!valid_format) && (test_format)) {
280 switch (test_format) {
281 case AUDIO_F32: /* web audio only supports floats */
282 this->spec.format = test_format;
283
284 valid_format = SDL_TRUE;
285 break;
286 }
287 test_format = SDL_NextAudioFormat();
288 }
289
290 if (!valid_format) {
291 /* Didn't find a compatible format :( */
292 return SDL_SetError("No compatible audio format!");
293 }
294
295 /* Initialize all variables that we clean on shutdown */
296 this->hidden = (struct SDL_PrivateAudioData *)
297 SDL_malloc((sizeof *this->hidden));
298 if (this->hidden == NULL) {
299 return SDL_OutOfMemory();
300 }
301 SDL_zerop(this->hidden);
302
303 /* limit to native freq */
304 const int sampleRate = EM_ASM_INT_V({
305 return SDL2.audioContext.sampleRate;
306 });
307
308 if(this->spec.freq != sampleRate) {
309 for (i = this->spec.samples; i > 0; i--) {
310 f = (float)i / (float)sampleRate * (float)this->spec.freq;
311 if (SDL_floor(f) == f) {
312 this->hidden->conv_in_len = SDL_floor(f);
313 break;
314 }
315 }
316
317 this->spec.freq = sampleRate;
318 }
319
320 SDL_CalculateAudioSpec(&this->spec);
321
322 if (iscapture) {
323 /* The idea is to take the capture media stream, hook it up to an
324 audio graph where we can pass it through a ScriptProcessorNode
325 to access the raw PCM samples and push them to the SDL app's
326 callback. From there, we "process" the audio data into silence
327 and forget about it. */
328
329 /* This should, strictly speaking, use MediaRecorder for capture, but
330 this API is cleaner to use and better supported, and fires a
331 callback whenever there's enough data to fire down into the app.
332 The downside is that we are spending CPU time silencing a buffer
333 that the audiocontext uselessly mixes into any output. On the
334 upside, both of those things are not only run in native code in
335 the browser, they're probably SIMD code, too. MediaRecorder
336 feels like it's a pretty inefficient tapdance in similar ways,
337 to be honest. */
338
339 EM_ASM_({
340 var have_microphone = function(stream) {
341 //console.log('SDL audio capture: we have a microphone! Replacing silence callback.');
342 if (SDL2.capture.silenceTimer !== undefined) {
343 clearTimeout(SDL2.capture.silenceTimer);
344 SDL2.capture.silenceTimer = undefined;
345 }
346 SDL2.capture.mediaStreamNode = SDL2.audioContext.createMediaStreamSource(stream);
347 SDL2.capture.scriptProcessorNode = SDL2.audioContext.createScriptProcessor($1, $0, 1);
348 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {
349 if ((SDL2 === undefined) || (SDL2.capture === undefined)) { return; }
350 audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0);
351 SDL2.capture.currentCaptureBuffer = audioProcessingEvent.inputBuffer;
352 Runtime.dynCall('vi', $2, [$3]);
353 };
354 SDL2.capture.mediaStreamNode.connect(SDL2.capture.scriptProcessorNode);
355 SDL2.capture.scriptProcessorNode.connect(SDL2.audioContext.destination);
356 SDL2.capture.stream = stream;
357 };
358
359 var no_microphone = function(error) {
360 //console.log('SDL audio capture: we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.');
361 };
362
363 /* we write silence to the audio callback until the microphone is available (user approves use, etc). */
364 SDL2.capture.silenceBuffer = SDL2.audioContext.createBuffer($0, $1, SDL2.audioContext.sampleRate);
365 SDL2.capture.silenceBuffer.getChannelData(0).fill(0.0);
366 var silence_callback = function() {
367 SDL2.capture.currentCaptureBuffer = SDL2.capture.silenceBuffer;
368 Runtime.dynCall('vi', $2, [$3]);
369 };
370
371 SDL2.capture.silenceTimer = setTimeout(silence_callback, ($1 / SDL2.audioContext.sampleRate) * 1000);
372
373 if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) {
374 navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone);
375 } else if (navigator.webkitGetUserMedia !== undefined) {
376 navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone);
377 }
378 }, this->spec.channels, this->spec.samples, HandleCaptureProcess, this);
379 } else {
380 /* setup a ScriptProcessorNode */
381 EM_ASM_ARGS({
382 SDL2.audio.scriptProcessorNode = SDL2.audioContext['createScriptProcessor']($1, 0, $0);
383 SDL2.audio.scriptProcessorNode['onaudioprocess'] = function (e) {
384 if ((SDL2 === undefined) || (SDL2.audio === undefined)) { return; }
385 SDL2.audio.currentOutputBuffer = e['outputBuffer'];
386 Runtime.dynCall('vi', $2, [$3]);
387 };
388 SDL2.audio.scriptProcessorNode['connect'](SDL2.audioContext['destination']);
389 }, this->spec.channels, this->spec.samples, HandleAudioProcess, this);
390 }
391
392 return 0;
393 }
394
395 static int
EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl)396 EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl)
397 {
398 /* Set the function pointers */
399 impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice;
400 impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice;
401
402 impl->OnlyHasDefaultOutputDevice = 1;
403
404 /* no threads here */
405 impl->SkipMixerLock = 1;
406 impl->ProvidesOwnCallbackThread = 1;
407
408 /* check availability */
409 const int available = EM_ASM_INT_V({
410 if (typeof(AudioContext) !== 'undefined') {
411 return 1;
412 } else if (typeof(webkitAudioContext) !== 'undefined') {
413 return 1;
414 }
415 return 0;
416 });
417
418 if (!available) {
419 SDL_SetError("No audio context available");
420 }
421
422 const int capture_available = available && EM_ASM_INT_V({
423 if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) {
424 return 1;
425 } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') {
426 return 1;
427 }
428 return 0;
429 });
430
431 impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE;
432 impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE;
433
434 return available;
435 }
436
437 AudioBootStrap EMSCRIPTENAUDIO_bootstrap = {
438 "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, 0
439 };
440
441 #endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */
442
443 /* vi: set ts=4 sw=4 expandtab: */
444