src/audio/emscripten/SDL_emscriptenaudio.c
author Ryan C. Gordon <icculus@icculus.org>
Tue, 09 Aug 2016 16:58:32 -0400
changeset 10274 cc6461b9c5bc
parent 10257 f17581d00c26
child 10276 470698f90685
permissions -rw-r--r--
emscripten audio: Added audio capture support.
     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] = getValue($0 + (j*numChannels + c)*4, 'float');
   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 Emscripten_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.scriptProcessorNode !== undefined) {
   207                 SDL2.capture.scriptProcessorNode.disconnect();
   208                 SDL2.capture.scriptProcessorNode = undefined;
   209             }
   210             if (SDL2.capture.mediaStreamNode !== undefined) {
   211                 SDL2.capture.mediaStreamNode.disconnect();
   212                 SDL2.capture.mediaStreamNode = undefined;
   213             }
   214             if (SDL2.capture.silenceBuffer !== undefined) {
   215                 SDL2.capture.silenceBuffer = undefined
   216             }
   217             SDL2.capture = undefined;
   218         } else {
   219             if (SDL2.audio.scriptProcessorNode != undefined) {
   220                 SDL2.audio.scriptProcessorNode.disconnect();
   221                 SDL2.audio.scriptProcessorNode = undefined;
   222             }
   223             SDL2.audio = undefined;
   224         }
   225         if ((SDL2.audioContext !== undefined) && (SDL2.audio === undefined) && (SDL2.capture === undefined)) {
   226             SDL2.audioContext.close();
   227             SDL2.audioContext = undefined;
   228         }
   229     }, this->iscapture);
   230 
   231     SDL_free(this->hidden->mixbuf);
   232     SDL_free(this->hidden);
   233 }
   234 
   235 static int
   236 Emscripten_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
   237 {
   238     SDL_bool valid_format = SDL_FALSE;
   239     SDL_AudioFormat test_format;
   240     int i;
   241     float f;
   242     int result;
   243 
   244     /* based on parts of library_sdl.js */
   245 
   246     /* create context (TODO: this puts stuff in the global namespace...)*/
   247     result = EM_ASM_INT({
   248         if(typeof(SDL2) === 'undefined') {
   249             SDL2 = {};
   250         }
   251         if (!$0) {
   252             SDL2.audio = {};
   253         } else {
   254             SDL2.capture = {};
   255         }
   256 
   257         if (!SDL2.audioContext) {
   258             if (typeof(AudioContext) !== 'undefined') {
   259                 SDL2.audioContext = new AudioContext();
   260             } else if (typeof(webkitAudioContext) !== 'undefined') {
   261                 SDL2.audioContext = new webkitAudioContext();
   262             }
   263         }
   264         return SDL2.audioContext === undefined ? -1 : 0;
   265     }, iscapture);
   266     if (result < 0) {
   267         return SDL_SetError("Web Audio API is not available!");
   268     }
   269 
   270     test_format = SDL_FirstAudioFormat(this->spec.format);
   271     while ((!valid_format) && (test_format)) {
   272         switch (test_format) {
   273         case AUDIO_F32: /* web audio only supports floats */
   274             this->spec.format = test_format;
   275 
   276             valid_format = SDL_TRUE;
   277             break;
   278         }
   279         test_format = SDL_NextAudioFormat();
   280     }
   281 
   282     if (!valid_format) {
   283         /* Didn't find a compatible format :( */
   284         return SDL_SetError("No compatible audio format!");
   285     }
   286 
   287     /* Initialize all variables that we clean on shutdown */
   288     this->hidden = (struct SDL_PrivateAudioData *)
   289         SDL_malloc((sizeof *this->hidden));
   290     if (this->hidden == NULL) {
   291         return SDL_OutOfMemory();
   292     }
   293     SDL_zerop(this->hidden);
   294 
   295     /* limit to native freq */
   296     const int sampleRate = EM_ASM_INT_V({
   297         return SDL2.audioContext.sampleRate;
   298     });
   299 
   300     if(this->spec.freq != sampleRate) {
   301         for (i = this->spec.samples; i > 0; i--) {
   302             f = (float)i / (float)sampleRate * (float)this->spec.freq;
   303             if (SDL_floor(f) == f) {
   304                 this->hidden->conv_in_len = SDL_floor(f);
   305                 break;
   306             }
   307         }
   308 
   309         this->spec.freq = sampleRate;
   310     }
   311 
   312     SDL_CalculateAudioSpec(&this->spec);
   313 
   314     if (iscapture) {
   315         /* The idea is to take the capture media stream, hook it up to an
   316            audio graph where we can pass it through a ScriptProcessorNode
   317            to access the raw PCM samples and push them to the SDL app's
   318            callback. From there, we "process" the audio data into silence
   319            and forget about it. */
   320 
   321         /* This should, strictly speaking, use MediaRecorder for capture, but
   322            this API is cleaner to use and better supported, and fires a
   323            callback whenever there's enough data to fire down into the app.
   324            The downside is that we are spending CPU time silencing a buffer
   325            that the audiocontext uselessly mixes into any output. On the
   326            upside, both of those things are not only run in native code in
   327            the browser, they're probably SIMD code, too. MediaRecorder
   328            feels like it's a pretty inefficient tapdance in similar ways,
   329            to be honest. */
   330 
   331         EM_ASM_({
   332             var have_microphone = function(stream) {
   333                 clearTimeout(SDL2.capture.silenceTimer);
   334                 SDL2.capture.silenceTimer = undefined;
   335                 SDL2.capture.mediaStreamNode = SDL2.audioContext.createMediaStreamSource(stream);
   336                 SDL2.capture.scriptProcessorNode = SDL2.audioContext.createScriptProcessor($1, $0, 1);
   337                 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {
   338                     audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0);
   339                     SDL2.capture.currentCaptureBuffer = audioProcessingEvent.inputBuffer;
   340                     Runtime.dynCall('vi', $2, [$3]);
   341                 };
   342                 SDL2.capture.mediaStreamNode.connect(SDL2.capture.scriptProcessorNode);
   343                 SDL2.capture.scriptProcessorNode.connect(SDL2.audioContext.destination);
   344             };
   345 
   346             var no_microphone = function(error) {
   347                 console.log('we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.');
   348             };
   349 
   350             /* we write silence to the audio callback until the microphone is available (user approves use, etc). */
   351             SDL2.capture.silenceBuffer = SDL2.audioContext.createBuffer($0, $1, SDL2.audioContext.sampleRate);
   352             SDL2.capture.silenceBuffer.getChannelData(0).fill(0.0);
   353 
   354             var silence_callback = function() {
   355                 SDL2.capture.currentCaptureBuffer = SDL2.capture.silenceBuffer;
   356                 Runtime.dynCall('vi', $2, [$3]);
   357             };
   358 
   359             SDL2.capture.silenceTimer = setTimeout(silence_callback, $1 / SDL2.audioContext.sampleRate);
   360 
   361             if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) {
   362                 navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone);
   363             } else if (navigator.webkitGetUserMedia !== undefined) {
   364                 navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone);
   365             }
   366         }, this->spec.channels, this->spec.samples, HandleCaptureProcess, this);
   367     } else {
   368         /* setup a ScriptProcessorNode */
   369         EM_ASM_ARGS({
   370             SDL2.audio.scriptProcessorNode = SDL2.audioContext['createScriptProcessor']($1, 0, $0);
   371             SDL2.audio.scriptProcessorNode['onaudioprocess'] = function (e) {
   372                 SDL2.audio.currentOutputBuffer = e['outputBuffer'];
   373                 Runtime.dynCall('vi', $2, [$3]);
   374             };
   375             SDL2.audio.scriptProcessorNode['connect'](SDL2.audioContext['destination']);
   376         }, this->spec.channels, this->spec.samples, HandleAudioProcess, this);
   377     }
   378 
   379     return 0;
   380 }
   381 
   382 static int
   383 Emscripten_Init(SDL_AudioDriverImpl * impl)
   384 {
   385     /* Set the function pointers */
   386     impl->OpenDevice = Emscripten_OpenDevice;
   387     impl->CloseDevice = Emscripten_CloseDevice;
   388 
   389     impl->OnlyHasDefaultOutputDevice = 1;
   390 
   391     /* no threads here */
   392     impl->SkipMixerLock = 1;
   393     impl->ProvidesOwnCallbackThread = 1;
   394 
   395     /* check availability */
   396     const int available = EM_ASM_INT_V({
   397         if (typeof(AudioContext) !== 'undefined') {
   398             return 1;
   399         } else if (typeof(webkitAudioContext) !== 'undefined') {
   400             return 1;
   401         }
   402         return 0;
   403     });
   404 
   405     if (!available) {
   406         SDL_SetError("No audio context available");
   407     }
   408 
   409     const int capture_available = available && EM_ASM_INT_V({
   410         if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) {
   411             return 1;
   412         } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') {
   413             return 1;
   414         }
   415         return 0;
   416     });
   417 
   418     impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE;
   419     impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE;
   420 
   421     return available;
   422 }
   423 
   424 AudioBootStrap EmscriptenAudio_bootstrap = {
   425     "emscripten", "SDL emscripten audio driver", Emscripten_Init, 0
   426 };
   427 
   428 #endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */
   429 
   430 /* vi: set ts=4 sw=4 expandtab: */