src/audio/emscripten/SDL_emscriptenaudio.c
author Alon Zakai <alonzakai@gmail.com>
Tue, 29 Jan 2019 12:21:22 +0000
changeset 12581 62f26ad46749
parent 12580 e5c1992fcd8b
permissions -rw-r--r--
Emscripten: No need for Runtime. for dynCalls
     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) {
    62             SDL_AudioStreamClear(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) {
    75                 SDL_AudioStreamClear(this->stream);
    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)) {
    99         SDL_AudioStreamClear(this->stream);
   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 
   261     SDL_CalculateAudioSpec(&this->spec);
   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 
   383 AudioBootStrap EMSCRIPTENAUDIO_bootstrap = {
   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: */