src/audio/emscripten/SDL_emscriptenaudio.c
author Ryan C. Gordon <icculus@icculus.org>
Fri, 12 Aug 2016 00:03:58 -0400
changeset 10283 1b8594db77f1
parent 10281 2a002e96888f
child 10301 a25d9c643cfb
permissions -rw-r--r--
emscripten audio: check for an "undefined" object, remove some console.log().
     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 EMSCRIPTENAUDIO_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 EMSCRIPTENAUDIO_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                 //console.log('SDL audio capture: we have a microphone! Replacing silence callback.');
   334                 if (SDL2.capture.silenceTimer !== undefined) {
   335                     clearTimeout(SDL2.capture.silenceTimer);
   336                     SDL2.capture.silenceTimer = undefined;
   337                 }
   338                 SDL2.capture.mediaStreamNode = SDL2.audioContext.createMediaStreamSource(stream);
   339                 SDL2.capture.scriptProcessorNode = SDL2.audioContext.createScriptProcessor($1, $0, 1);
   340                 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {
   341                     audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0);
   342                     SDL2.capture.currentCaptureBuffer = audioProcessingEvent.inputBuffer;
   343                     Runtime.dynCall('vi', $2, [$3]);
   344                 };
   345                 SDL2.capture.mediaStreamNode.connect(SDL2.capture.scriptProcessorNode);
   346                 SDL2.capture.scriptProcessorNode.connect(SDL2.audioContext.destination);
   347             };
   348 
   349             var no_microphone = function(error) {
   350                 //console.log('SDL audio capture: we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.');
   351             };
   352 
   353             /* we write silence to the audio callback until the microphone is available (user approves use, etc). */
   354             SDL2.capture.silenceBuffer = SDL2.audioContext.createBuffer($0, $1, SDL2.audioContext.sampleRate);
   355             SDL2.capture.silenceBuffer.getChannelData(0).fill(0.0);
   356             var silence_callback = function() {
   357                 SDL2.capture.currentCaptureBuffer = SDL2.capture.silenceBuffer;
   358                 Runtime.dynCall('vi', $2, [$3]);
   359             };
   360 
   361             SDL2.capture.silenceTimer = setTimeout(silence_callback, ($1 / SDL2.audioContext.sampleRate) * 1000);
   362 
   363             if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) {
   364                 navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone);
   365             } else if (navigator.webkitGetUserMedia !== undefined) {
   366                 navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone);
   367             }
   368         }, this->spec.channels, this->spec.samples, HandleCaptureProcess, this);
   369     } else {
   370         /* setup a ScriptProcessorNode */
   371         EM_ASM_ARGS({
   372             SDL2.audio.scriptProcessorNode = SDL2.audioContext['createScriptProcessor']($1, 0, $0);
   373             SDL2.audio.scriptProcessorNode['onaudioprocess'] = function (e) {
   374                 SDL2.audio.currentOutputBuffer = e['outputBuffer'];
   375                 Runtime.dynCall('vi', $2, [$3]);
   376             };
   377             SDL2.audio.scriptProcessorNode['connect'](SDL2.audioContext['destination']);
   378         }, this->spec.channels, this->spec.samples, HandleAudioProcess, this);
   379     }
   380 
   381     return 0;
   382 }
   383 
   384 static int
   385 EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl)
   386 {
   387     /* Set the function pointers */
   388     impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice;
   389     impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice;
   390 
   391     impl->OnlyHasDefaultOutputDevice = 1;
   392 
   393     /* no threads here */
   394     impl->SkipMixerLock = 1;
   395     impl->ProvidesOwnCallbackThread = 1;
   396 
   397     /* check availability */
   398     const int available = EM_ASM_INT_V({
   399         if (typeof(AudioContext) !== 'undefined') {
   400             return 1;
   401         } else if (typeof(webkitAudioContext) !== 'undefined') {
   402             return 1;
   403         }
   404         return 0;
   405     });
   406 
   407     if (!available) {
   408         SDL_SetError("No audio context available");
   409     }
   410 
   411     const int capture_available = available && EM_ASM_INT_V({
   412         if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) {
   413             return 1;
   414         } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') {
   415             return 1;
   416         }
   417         return 0;
   418     });
   419 
   420     impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE;
   421     impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE;
   422 
   423     return available;
   424 }
   425 
   426 AudioBootStrap EMSCRIPTENAUDIO_bootstrap = {
   427     "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, 0
   428 };
   429 
   430 #endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */
   431 
   432 /* vi: set ts=4 sw=4 expandtab: */