SDL  2.0
SDL_emscriptenaudio.c
Go to the documentation of this file.
1 /*
2  Simple DirectMedia Layer
3  Copyright (C) 1997-2018 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 #include "SDL_assert.h"
30 
31 #include <emscripten/emscripten.h>
32 
33 static void
34 FeedAudioDevice(_THIS, const void *buf, const int buflen)
35 {
36  const int framelen = (SDL_AUDIO_BITSIZE(this->spec.format) / 8) * this->spec.channels;
37  EM_ASM_ARGS({
38  var numChannels = SDL2.audio.currentOutputBuffer['numberOfChannels'];
39  for (var c = 0; c < numChannels; ++c) {
40  var channelData = SDL2.audio.currentOutputBuffer['getChannelData'](c);
41  if (channelData.length != $1) {
42  throw 'Web Audio output buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
43  }
44 
45  for (var j = 0; j < $1; ++j) {
46  channelData[j] = HEAPF32[$0 + ((j*numChannels + c) << 2) >> 2]; /* !!! FIXME: why are these shifts here? */
47  }
48  }
49  }, buf, buflen / framelen);
50 }
51 
52 static void
53 HandleAudioProcess(_THIS)
54 {
55  SDL_AudioCallback callback = this->callbackspec.callback;
56  const int stream_len = this->callbackspec.size;
57 
58  /* Only do something if audio is enabled */
59  if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
60  if (this->stream) {
62  }
63  return;
64  }
65 
66  if (this->stream == NULL) { /* no conversion necessary. */
67  SDL_assert(this->spec.size == stream_len);
68  callback(this->callbackspec.userdata, this->work_buffer, stream_len);
69  } else { /* streaming/converting */
70  int got;
71  while (SDL_AudioStreamAvailable(this->stream) < ((int) this->spec.size)) {
72  callback(this->callbackspec.userdata, this->work_buffer, stream_len);
73  if (SDL_AudioStreamPut(this->stream, this->work_buffer, stream_len) == -1) {
75  SDL_AtomicSet(&this->enabled, 0);
76  break;
77  }
78  }
79 
80  got = SDL_AudioStreamGet(this->stream, this->work_buffer, this->spec.size);
81  SDL_assert((got < 0) || (got == this->spec.size));
82  if (got != this->spec.size) {
83  SDL_memset(this->work_buffer, this->spec.silence, this->spec.size);
84  }
85  }
86 
87  FeedAudioDevice(this, this->work_buffer, this->spec.size);
88 }
89 
90 static void
91 HandleCaptureProcess(_THIS)
92 {
93  SDL_AudioCallback callback = this->callbackspec.callback;
94  const int stream_len = this->callbackspec.size;
95 
96  /* Only do something if audio is enabled */
97  if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
99  return;
100  }
101 
102  EM_ASM_ARGS({
103  var numChannels = SDL2.capture.currentCaptureBuffer.numberOfChannels;
104  for (var c = 0; c < numChannels; ++c) {
105  var channelData = SDL2.capture.currentCaptureBuffer.getChannelData(c);
106  if (channelData.length != $1) {
107  throw 'Web Audio capture buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
108  }
109 
110  if (numChannels == 1) { /* fastpath this a little for the common (mono) case. */
111  for (var j = 0; j < $1; ++j) {
112  setValue($0 + (j * 4), channelData[j], 'float');
113  }
114  } else {
115  for (var j = 0; j < $1; ++j) {
116  setValue($0 + (((j * numChannels) + c) * 4), channelData[j], 'float');
117  }
118  }
119  }
120  }, this->work_buffer, (this->spec.size / sizeof (float)) / this->spec.channels);
121 
122  /* okay, we've got an interleaved float32 array in C now. */
123 
124  if (this->stream == NULL) { /* no conversion necessary. */
125  SDL_assert(this->spec.size == stream_len);
126  callback(this->callbackspec.userdata, this->work_buffer, stream_len);
127  } else { /* streaming/converting */
128  if (SDL_AudioStreamPut(this->stream, this->work_buffer, this->spec.size) == -1) {
129  SDL_AtomicSet(&this->enabled, 0);
130  }
131 
132  while (SDL_AudioStreamAvailable(this->stream) >= stream_len) {
133  const int got = SDL_AudioStreamGet(this->stream, this->work_buffer, stream_len);
134  SDL_assert((got < 0) || (got == stream_len));
135  if (got != stream_len) {
136  SDL_memset(this->work_buffer, this->callbackspec.silence, stream_len);
137  }
138  callback(this->callbackspec.userdata, this->work_buffer, stream_len); /* Send it to the app. */
139  }
140  }
141 }
142 
143 
144 static void
145 EMSCRIPTENAUDIO_CloseDevice(_THIS)
146 {
147  EM_ASM_({
148  if ($0) {
149  if (SDL2.capture.silenceTimer !== undefined) {
150  clearTimeout(SDL2.capture.silenceTimer);
151  }
152  if (SDL2.capture.stream !== undefined) {
153  var tracks = SDL2.capture.stream.getAudioTracks();
154  for (var i = 0; i < tracks.length; i++) {
155  SDL2.capture.stream.removeTrack(tracks[i]);
156  }
157  SDL2.capture.stream = undefined;
158  }
159  if (SDL2.capture.scriptProcessorNode !== undefined) {
160  SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {};
161  SDL2.capture.scriptProcessorNode.disconnect();
162  SDL2.capture.scriptProcessorNode = undefined;
163  }
164  if (SDL2.capture.mediaStreamNode !== undefined) {
165  SDL2.capture.mediaStreamNode.disconnect();
166  SDL2.capture.mediaStreamNode = undefined;
167  }
168  if (SDL2.capture.silenceBuffer !== undefined) {
169  SDL2.capture.silenceBuffer = undefined
170  }
171  SDL2.capture = undefined;
172  } else {
173  if (SDL2.audio.scriptProcessorNode != undefined) {
174  SDL2.audio.scriptProcessorNode.disconnect();
175  SDL2.audio.scriptProcessorNode = undefined;
176  }
177  SDL2.audio = undefined;
178  }
179  if ((SDL2.audioContext !== undefined) && (SDL2.audio === undefined) && (SDL2.capture === undefined)) {
180  SDL2.audioContext.close();
181  SDL2.audioContext = undefined;
182  }
183  }, this->iscapture);
184 
185 #if 0 /* !!! FIXME: currently not used. Can we move some stuff off the SDL2 namespace? --ryan. */
186  SDL_free(this->hidden);
187 #endif
188 }
189 
190 static int
191 EMSCRIPTENAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
192 {
193  SDL_bool valid_format = SDL_FALSE;
194  SDL_AudioFormat test_format;
195  int result;
196 
197  /* based on parts of library_sdl.js */
198 
199  /* create context (TODO: this puts stuff in the global namespace...)*/
200  result = EM_ASM_INT({
201  if(typeof(SDL2) === 'undefined') {
202  SDL2 = {};
203  }
204  if (!$0) {
205  SDL2.audio = {};
206  } else {
207  SDL2.capture = {};
208  }
209 
210  if (!SDL2.audioContext) {
211  if (typeof(AudioContext) !== 'undefined') {
212  SDL2.audioContext = new AudioContext();
213  } else if (typeof(webkitAudioContext) !== 'undefined') {
214  SDL2.audioContext = new webkitAudioContext();
215  }
216  }
217  return SDL2.audioContext === undefined ? -1 : 0;
218  }, iscapture);
219  if (result < 0) {
220  return SDL_SetError("Web Audio API is not available!");
221  }
222 
223  test_format = SDL_FirstAudioFormat(this->spec.format);
224  while ((!valid_format) && (test_format)) {
225  switch (test_format) {
226  case AUDIO_F32: /* web audio only supports floats */
227  this->spec.format = test_format;
228 
229  valid_format = SDL_TRUE;
230  break;
231  }
232  test_format = SDL_NextAudioFormat();
233  }
234 
235  if (!valid_format) {
236  /* Didn't find a compatible format :( */
237  return SDL_SetError("No compatible audio format!");
238  }
239 
240  /* Initialize all variables that we clean on shutdown */
241 #if 0 /* !!! FIXME: currently not used. Can we move some stuff off the SDL2 namespace? --ryan. */
242  this->hidden = (struct SDL_PrivateAudioData *)
243  SDL_malloc((sizeof *this->hidden));
244  if (this->hidden == NULL) {
245  return SDL_OutOfMemory();
246  }
247  SDL_zerop(this->hidden);
248 #endif
249 
250  /* limit to native freq */
251  this->spec.freq = EM_ASM_INT_V({ return SDL2.audioContext.sampleRate; });
252 
254 
255  if (iscapture) {
256  /* The idea is to take the capture media stream, hook it up to an
257  audio graph where we can pass it through a ScriptProcessorNode
258  to access the raw PCM samples and push them to the SDL app's
259  callback. From there, we "process" the audio data into silence
260  and forget about it. */
261 
262  /* This should, strictly speaking, use MediaRecorder for capture, but
263  this API is cleaner to use and better supported, and fires a
264  callback whenever there's enough data to fire down into the app.
265  The downside is that we are spending CPU time silencing a buffer
266  that the audiocontext uselessly mixes into any output. On the
267  upside, both of those things are not only run in native code in
268  the browser, they're probably SIMD code, too. MediaRecorder
269  feels like it's a pretty inefficient tapdance in similar ways,
270  to be honest. */
271 
272  EM_ASM_({
273  var have_microphone = function(stream) {
274  //console.log('SDL audio capture: we have a microphone! Replacing silence callback.');
275  if (SDL2.capture.silenceTimer !== undefined) {
276  clearTimeout(SDL2.capture.silenceTimer);
277  SDL2.capture.silenceTimer = undefined;
278  }
279  SDL2.capture.mediaStreamNode = SDL2.audioContext.createMediaStreamSource(stream);
280  SDL2.capture.scriptProcessorNode = SDL2.audioContext.createScriptProcessor($1, $0, 1);
281  SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {
282  if ((SDL2 === undefined) || (SDL2.capture === undefined)) { return; }
283  audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0);
284  SDL2.capture.currentCaptureBuffer = audioProcessingEvent.inputBuffer;
285  Runtime.dynCall('vi', $2, [$3]);
286  };
287  SDL2.capture.mediaStreamNode.connect(SDL2.capture.scriptProcessorNode);
288  SDL2.capture.scriptProcessorNode.connect(SDL2.audioContext.destination);
289  SDL2.capture.stream = stream;
290  };
291 
292  var no_microphone = function(error) {
293  //console.log('SDL audio capture: we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.');
294  };
295 
296  /* we write silence to the audio callback until the microphone is available (user approves use, etc). */
297  SDL2.capture.silenceBuffer = SDL2.audioContext.createBuffer($0, $1, SDL2.audioContext.sampleRate);
298  SDL2.capture.silenceBuffer.getChannelData(0).fill(0.0);
299  var silence_callback = function() {
300  SDL2.capture.currentCaptureBuffer = SDL2.capture.silenceBuffer;
301  Runtime.dynCall('vi', $2, [$3]);
302  };
303 
304  SDL2.capture.silenceTimer = setTimeout(silence_callback, ($1 / SDL2.audioContext.sampleRate) * 1000);
305 
306  if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) {
307  navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone);
308  } else if (navigator.webkitGetUserMedia !== undefined) {
309  navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone);
310  }
311  }, this->spec.channels, this->spec.samples, HandleCaptureProcess, this);
312  } else {
313  /* setup a ScriptProcessorNode */
314  EM_ASM_ARGS({
315  SDL2.audio.scriptProcessorNode = SDL2.audioContext['createScriptProcessor']($1, 0, $0);
316  SDL2.audio.scriptProcessorNode['onaudioprocess'] = function (e) {
317  if ((SDL2 === undefined) || (SDL2.audio === undefined)) { return; }
318  SDL2.audio.currentOutputBuffer = e['outputBuffer'];
319  Runtime.dynCall('vi', $2, [$3]);
320  };
321  SDL2.audio.scriptProcessorNode['connect'](SDL2.audioContext['destination']);
322  }, this->spec.channels, this->spec.samples, HandleAudioProcess, this);
323  }
324 
325  return 0;
326 }
327 
328 static int
329 EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl)
330 {
331  int available;
332  int capture_available;
333 
334  /* Set the function pointers */
335  impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice;
336  impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice;
337 
338  impl->OnlyHasDefaultOutputDevice = 1;
339 
340  /* no threads here */
341  impl->SkipMixerLock = 1;
342  impl->ProvidesOwnCallbackThread = 1;
343 
344  /* check availability */
345  available = EM_ASM_INT_V({
346  if (typeof(AudioContext) !== 'undefined') {
347  return 1;
348  } else if (typeof(webkitAudioContext) !== 'undefined') {
349  return 1;
350  }
351  return 0;
352  });
353 
354  if (!available) {
355  SDL_SetError("No audio context available");
356  }
357 
358  capture_available = available && EM_ASM_INT_V({
359  if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) {
360  return 1;
361  } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') {
362  return 1;
363  }
364  return 0;
365  });
366 
367  impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE;
368  impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE;
369 
370  return available;
371 }
372 
374  "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, 0
375 };
376 
377 #endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */
378 
379 /* vi: set ts=4 sw=4 expandtab: */
AudioBootStrap EMSCRIPTENAUDIO_bootstrap
#define SDL_AudioStreamAvailable
SDL_AudioFormat SDL_FirstAudioFormat(SDL_AudioFormat format)
Definition: SDL_audio.c:1584
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
Uint8 silence
Definition: SDL_audio.h:182
static int available()
Definition: video.c:356
#define SDL_AudioStreamGet
Uint16 samples
Definition: SDL_audio.h:183
Uint16 SDL_AudioFormat
Audio format flags.
Definition: SDL_audio.h:64
#define SDL_zerop(x)
Definition: SDL_stdinc.h:417
SDL_AudioFormat SDL_NextAudioFormat(void)
Definition: SDL_audio.c:1596
SDL_AudioSpec spec
Definition: loopwave.c:31
EGLImageKHR EGLint EGLint * handle
Definition: eglext.h:937
GLuint GLuint stream
void(* SDL_AudioCallback)(void *userdata, Uint8 *stream, int len)
Definition: SDL_audio.h:162
Uint8 channels
Definition: SDL_audio.h:181
#define _THIS
#define SDL_free
#define SDL_AUDIO_BITSIZE(x)
Definition: SDL_audio.h:75
#define SDL_AudioStreamPut
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
static Uint32 callback(Uint32 interval, void *param)
Definition: testtimer.c:34
int paused
Definition: testoverlay2.c:147
void SDL_CalculateAudioSpec(SDL_AudioSpec *spec)
Definition: SDL_audio.c:1605
GLenum GLuint GLenum GLsizei const GLchar * buf
GLenum GLenum GLsizei const GLuint GLboolean enabled
Uint32 size
Definition: SDL_audio.h:185
#define SDL_assert(condition)
Definition: SDL_assert.h:169
int(* OpenDevice)(_THIS, void *handle, const char *devname, int iscapture)
Definition: SDL_sysaudio.h:68
#define NULL
Definition: begin_code.h:164
#define SDL_OutOfMemory()
Definition: SDL_error.h:52
SDL_bool
Definition: SDL_stdinc.h:139
#define SDL_SetError
void(* CloseDevice)(_THIS)
Definition: SDL_sysaudio.h:79
SDL_AudioFormat format
Definition: SDL_audio.h:180
#define SDL_AtomicSet
#define SDL_AudioStreamClear
#define SDL_AtomicGet
#define SDL_malloc
#define AUDIO_F32
Definition: SDL_audio.h:114
#define SDL_memset