SDL  2.0
SDL_emscriptenaudio.c
Go to the documentation of this file.
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
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
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
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
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
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 
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
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 
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: */
SDL_AudioFormat SDL_FirstAudioFormat(SDL_AudioFormat format)
Definition: SDL_audio.c:1611
GLuint64EXT * result
SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char int SDL_PRINTF_FORMAT_STRING const char const char SDL_SCANF_FORMAT_STRING const char return SDL_ThreadFunction const char void return Uint32 return Uint32 SDL_AssertionHandler void SDL_SpinLock SDL_atomic_t int int return SDL_atomic_t return void void void return void return int return SDL_AudioSpec SDL_AudioSpec return int int return return int SDL_RWops int SDL_AudioSpec Uint8 Uint32 * e
GLuint GLuint stream
Uint16 samples
Definition: SDL_audio.h:174
GLfloat f
Uint16 SDL_AudioFormat
Audio format flags.
Definition: SDL_audio.h:64
#define SDL_floor
#define SDL_zerop(x)
Definition: SDL_stdinc.h:360
SDL_AudioFormat SDL_NextAudioFormat(void)
Definition: SDL_audio.c:1623
SDL_AudioSpec spec
Definition: loopwave.c:35
#define SDL_memcpy
#define SDL_ConvertAudio
Uint8 channels
Definition: SDL_audio.h:172
#define _THIS
uint8_t Uint8
An unsigned 8-bit integer type.
Definition: SDL_stdinc.h:143
void SDL_free(void *mem)
#define SDL_AUDIO_BITSIZE(x)
Definition: SDL_audio.h:75
return Display return Display Bool Bool int int int return Display XEvent Bool(*) XPointer return Display return Display Drawable _Xconst char unsigned int unsigned int return Display Pixmap Pixmap XColor XColor unsigned int unsigned int return Display _Xconst char char int char return Display Visual unsigned int int int char unsigned int unsigned int int in j)
Definition: SDL_x11sym.h:50
const GLubyte * c
int paused
Definition: testoverlay2.c:149
#define SDL_memmove
void SDL_CalculateAudioSpec(SDL_AudioSpec *spec)
Definition: SDL_audio.c:1632
SDL_AudioCallback callback
Definition: SDL_audio.h:177
GLenum GLuint GLenum GLsizei const GLchar * buf
GLenum GLenum GLsizei const GLuint GLboolean enabled
return Display return Display Bool Bool int int int return Display XEvent Bool(*) XPointer return Display return Display Drawable _Xconst char unsigned int unsigned int return Display Pixmap Pixmap XColor XColor unsigned int unsigned int return Display _Xconst char char int char return Display Visual unsigned int int int char unsigned int unsigned int in i)
Definition: SDL_x11sym.h:50
Uint32 size
Definition: SDL_audio.h:176
int(* OpenDevice)(_THIS, void *handle, const char *devname, int iscapture)
Definition: SDL_sysaudio.h:76
#define NULL
Definition: begin_code.h:143
#define SDL_OutOfMemory()
Definition: SDL_error.h:52
SDL_bool
Definition: SDL_stdinc.h:130
#define SDL_SetError
void(* CloseDevice)(_THIS)
Definition: SDL_sysaudio.h:85
SDL_AudioFormat format
Definition: SDL_audio.h:171
AudioBootStrap EMSCRIPTENAUDIO_bootstrap
#define SDL_AtomicGet
void * userdata
Definition: SDL_audio.h:178
#define SDL_malloc
#define AUDIO_F32
Definition: SDL_audio.h:114