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: */
SDL_memset
#define SDL_memset
Definition: SDL_dynapi_overrides.h:386
c
const GLubyte * c
Definition: SDL_opengl_glext.h:11093
SDL_FirstAudioFormat
SDL_AudioFormat SDL_FirstAudioFormat(SDL_AudioFormat format)
Definition: SDL_audio.c:1647
SDL_AudioSpec::channels
Uint8 channels
Definition: SDL_audio.h:182
SDL_AudioDriverImpl::HasCaptureSupport
int HasCaptureSupport
Definition: SDL_sysaudio.h:90
SDL_AudioDriverImpl::ProvidesOwnCallbackThread
int ProvidesOwnCallbackThread
Definition: SDL_sysaudio.h:88
NULL
#define NULL
Definition: begin_code.h:167
SDL_AudioStreamAvailable
#define SDL_AudioStreamAvailable
Definition: SDL_dynapi_overrides.h:647
handle
EGLImageKHR EGLint EGLint * handle
Definition: eglext.h:937
SDL_AudioSpec::samples
Uint16 samples
Definition: SDL_audio.h:184
SDL_zerop
#define SDL_zerop(x)
Definition: SDL_stdinc.h:417
SDL_log.h
SDL_AudioSpec::format
SDL_AudioFormat format
Definition: SDL_audio.h:181
SDL_AudioDriverImpl::SkipMixerLock
int SkipMixerLock
Definition: SDL_sysaudio.h:89
stream
GLuint GLuint stream
Definition: SDL_opengl_glext.h:1776
SDL_AudioDriverImpl::OpenDevice
int(* OpenDevice)(_THIS, void *handle, const char *devname, int iscapture)
Definition: SDL_sysaudio.h:68
SDL_AudioStreamGet
#define SDL_AudioStreamGet
Definition: SDL_dynapi_overrides.h:645
callback
static Uint32 callback(Uint32 interval, void *param)
Definition: testtimer.c:34
x1
GLuint GLfloat GLfloat GLfloat x1
Definition: SDL_opengl_glext.h:8583
SDL_NextAudioFormat
SDL_AudioFormat SDL_NextAudioFormat(void)
Definition: SDL_audio.c:1659
SDL_AudioFormat
Uint16 SDL_AudioFormat
Audio format flags.
Definition: SDL_audio.h:64
result
GLuint64EXT * result
Definition: SDL_opengl_glext.h:9432
AudioBootStrap
Definition: SDL_sysaudio.h:176
SDL_PrivateAudioData
Definition: SDL_alsa_audio.h:33
EMSCRIPTENAUDIO_bootstrap
AudioBootStrap EMSCRIPTENAUDIO_bootstrap
buf
GLenum GLuint GLenum GLsizei const GLchar * buf
Definition: SDL_opengl_glext.h:2480
SDL_FALSE
@ SDL_FALSE
Definition: SDL_stdinc.h:163
SDL_audio.h
SDL_AudioDriverImpl
Definition: SDL_sysaudio.h:65
SDL_free
#define SDL_free
Definition: SDL_dynapi_overrides.h:377
SDL_AudioDriverImpl::OnlyHasDefaultOutputDevice
int OnlyHasDefaultOutputDevice
Definition: SDL_sysaudio.h:91
SDL_assert.h
SDL_CalculateAudioSpec
void SDL_CalculateAudioSpec(SDL_AudioSpec *spec)
Definition: SDL_audio.c:1668
_THIS
#define _THIS
Definition: SDL_alsa_audio.h:31
SDL_AudioStreamPut
#define SDL_AudioStreamPut
Definition: SDL_dynapi_overrides.h:644
SDL_AUDIO_BITSIZE
#define SDL_AUDIO_BITSIZE(x)
Definition: SDL_audio.h:75
SDL_emscriptenaudio.h
SDL_AudioSpec::freq
int freq
Definition: SDL_audio.h:180
SDL_AudioDriverImpl::OnlyHasDefaultCaptureDevice
int OnlyHasDefaultCaptureDevice
Definition: SDL_sysaudio.h:92
SDL_assert
#define SDL_assert(condition)
Definition: SDL_assert.h:169
SDL_AudioSpec::silence
Uint8 silence
Definition: SDL_audio.h:183
SDL_OutOfMemory
#define SDL_OutOfMemory()
Definition: SDL_error.h:52
spec
SDL_AudioSpec spec
Definition: loopwave.c:31
available
static int available()
Definition: video.c:356
SDL_AudioSpec::size
Uint32 size
Definition: SDL_audio.h:186
SDL_PrivateAudioData::audio
PP_Resource audio
Definition: SDL_naclaudio.h:38
SDL_TRUE
@ SDL_TRUE
Definition: SDL_stdinc.h:164
SDL_AudioStreamClear
#define SDL_AudioStreamClear
Definition: SDL_dynapi_overrides.h:646
SDL_SetError
#define SDL_SetError
Definition: SDL_dynapi_overrides.h:30
j
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
e
const SDL_PRINTF_FORMAT_STRING char int const SDL_PRINTF_FORMAT_STRING char int const SDL_PRINTF_FORMAT_STRING char int const SDL_PRINTF_FORMAT_STRING char const char const SDL_SCANF_FORMAT_STRING 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
Definition: SDL_dynapi_procs.h:117
enabled
GLenum GLenum GLsizei const GLuint GLboolean enabled
Definition: SDL_opengl_glext.h:2479
paused
int paused
Definition: testoverlay2.c:147
SDL_AtomicSet
#define SDL_AtomicSet
Definition: SDL_dynapi_overrides.h:67
SDL_malloc
#define SDL_malloc
Definition: SDL_dynapi_overrides.h:374
SDL_AtomicGet
#define SDL_AtomicGet
Definition: SDL_dynapi_overrides.h:68
SDL_AudioCallback
void(* SDL_AudioCallback)(void *userdata, Uint8 *stream, int len)
Definition: SDL_audio.h:163
AUDIO_F32
#define AUDIO_F32
Definition: SDL_audio.h:114
SDL_AudioDriverImpl::CloseDevice
void(* CloseDevice)(_THIS)
Definition: SDL_sysaudio.h:78
SDL_bool
SDL_bool
Definition: SDL_stdinc.h:161