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