src/audio/emscripten/SDL_emscriptenaudio.c
author Ryan C. Gordon <icculus@icculus.org>
Wed, 31 Aug 2016 16:10:04 -0400
changeset 10301 a25d9c643cfb
parent 10283 1b8594db77f1
child 10322 6167a989223c
permissions -rw-r--r--
emscripten: Be more aggressive when closing audio capture devices.

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