src/audio/emscripten/SDL_emscriptenaudio.c
author Ryan C. Gordon <icculus@icculus.org>
Wed, 10 May 2017 16:18:43 -0400
changeset 10997 b6315b6bc32f
parent 10765 61312c8c59fe
child 11034 fcb1f3161d2e
permissions -rw-r--r--
audio: fixed more "spec" references that should have been "callbackspec".

This should catch all the ones for audio targets that have provided their
own audio threads.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2017 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 numChannels = SDL2.audio.currentOutputBuffer['numberOfChannels'];
    39         for (var c = 0; c < numChannels; ++c) {
    40             var channelData = SDL2.audio.currentOutputBuffer['getChannelData'](c);
    41             if (channelData.length != $1) {
    42                 throw 'Web Audio output buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
    43             }
    44 
    45             for (var j = 0; j < $1; ++j) {
    46                 channelData[j] = HEAPF32[$0 + ((j*numChannels + c) << 2) >> 2];  /* !!! FIXME: why are these shifts here? */
    47             }
    48         }
    49     }, buf, buflen / framelen);
    50 }
    51 
    52 static void
    53 HandleAudioProcess(_THIS)
    54 {
    55     SDL_AudioCallback callback = this->callbackspec.callback;
    56     const int stream_len = this->callbackspec.size;
    57 
    58     /* Only do something if audio is enabled */
    59     if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
    60         if (this->stream) {
    61             SDL_AudioStreamClear(this->stream);
    62         }
    63         return;
    64     }
    65 
    66     if (this->stream == NULL) {  /* no conversion necessary. */
    67         SDL_assert(this->spec.size == stream_len);
    68         callback(this->callbackspec.userdata, this->work_buffer, stream_len);
    69     } else {  /* streaming/converting */
    70         int got;
    71         while (SDL_AudioStreamAvailable(this->stream) < ((int) this->spec.size)) {
    72             callback(this->callbackspec.userdata, this->work_buffer, stream_len);
    73             if (SDL_AudioStreamPut(this->stream, this->work_buffer, stream_len) == -1) {
    74                 SDL_AudioStreamClear(this->stream);
    75                 SDL_AtomicSet(&this->enabled, 0);
    76                 break;
    77             }
    78         }
    79 
    80         got = SDL_AudioStreamGet(this->stream, this->work_buffer, this->spec.size);
    81         SDL_assert((got < 0) || (got == this->spec.size));
    82         if (got != this->spec.size) {
    83             SDL_memset(this->work_buffer, this->spec.silence, this->spec.size);
    84         }
    85     }
    86 
    87     FeedAudioDevice(this, this->work_buffer, this->spec.size);
    88 }
    89 
    90 static void
    91 HandleCaptureProcess(_THIS)
    92 {
    93     SDL_AudioCallback callback = this->callbackspec.callback;
    94     const int stream_len = this->callbackspec.size;
    95 
    96     /* Only do something if audio is enabled */
    97     if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) {
    98         SDL_AudioStreamClear(this->stream);
    99         return;
   100     }
   101 
   102     EM_ASM_ARGS({
   103         var numChannels = SDL2.capture.currentCaptureBuffer.numberOfChannels;
   104         for (var c = 0; c < numChannels; ++c) {
   105             var channelData = SDL2.capture.currentCaptureBuffer.getChannelData(c);
   106             if (channelData.length != $1) {
   107                 throw 'Web Audio capture buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!';
   108             }
   109 
   110             if (numChannels == 1) {  /* fastpath this a little for the common (mono) case. */
   111                 for (var j = 0; j < $1; ++j) {
   112                     setValue($0 + (j * 4), channelData[j], 'float');
   113                 }
   114             } else {
   115                 for (var j = 0; j < $1; ++j) {
   116                     setValue($0 + (((j * numChannels) + c) * 4), channelData[j], 'float');
   117                 }
   118             }
   119         }
   120     }, this->work_buffer, (this->spec.size / sizeof (float)) / this->spec.channels);
   121 
   122     /* okay, we've got an interleaved float32 array in C now. */
   123 
   124     if (this->stream == NULL) {  /* no conversion necessary. */
   125         SDL_assert(this->spec.size == stream_len);
   126         callback(this->callbackspec.userdata, this->work_buffer, stream_len);
   127     } else {  /* streaming/converting */
   128         if (SDL_AudioStreamPut(this->stream, this->work_buffer, this->spec.size) == -1) {
   129             SDL_AtomicSet(&this->enabled, 0);
   130         }
   131 
   132         while (SDL_AudioStreamAvailable(this->stream) >= stream_len) {
   133             const int got = SDL_AudioStreamGet(this->stream, this->work_buffer, stream_len);
   134             SDL_assert((got < 0) || (got == stream_len));
   135             if (got != stream_len) {
   136                 SDL_memset(this->work_buffer, this->callbackspec.silence, stream_len);
   137             }
   138             callback(this->callbackspec.userdata, this->work_buffer, stream_len);  /* Send it to the app. */
   139         }
   140     }
   141 }
   142 
   143 
   144 static void
   145 EMSCRIPTENAUDIO_CloseDevice(_THIS)
   146 {
   147     EM_ASM_({
   148         if ($0) {
   149             if (SDL2.capture.silenceTimer !== undefined) {
   150                 clearTimeout(SDL2.capture.silenceTimer);
   151             }
   152             if (SDL2.capture.stream !== undefined) {
   153                 var tracks = SDL2.capture.stream.getAudioTracks();
   154                 for (var i = 0; i < tracks.length; i++) {
   155                     SDL2.capture.stream.removeTrack(tracks[i]);
   156                 }
   157                 SDL2.capture.stream = undefined;
   158             }
   159             if (SDL2.capture.scriptProcessorNode !== undefined) {
   160                 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {};
   161                 SDL2.capture.scriptProcessorNode.disconnect();
   162                 SDL2.capture.scriptProcessorNode = undefined;
   163             }
   164             if (SDL2.capture.mediaStreamNode !== undefined) {
   165                 SDL2.capture.mediaStreamNode.disconnect();
   166                 SDL2.capture.mediaStreamNode = undefined;
   167             }
   168             if (SDL2.capture.silenceBuffer !== undefined) {
   169                 SDL2.capture.silenceBuffer = undefined
   170             }
   171             SDL2.capture = undefined;
   172         } else {
   173             if (SDL2.audio.scriptProcessorNode != undefined) {
   174                 SDL2.audio.scriptProcessorNode.disconnect();
   175                 SDL2.audio.scriptProcessorNode = undefined;
   176             }
   177             SDL2.audio = undefined;
   178         }
   179         if ((SDL2.audioContext !== undefined) && (SDL2.audio === undefined) && (SDL2.capture === undefined)) {
   180             SDL2.audioContext.close();
   181             SDL2.audioContext = undefined;
   182         }
   183     }, this->iscapture);
   184 
   185 #if 0  /* !!! FIXME: currently not used. Can we move some stuff off the SDL2 namespace? --ryan. */
   186     SDL_free(this->hidden);
   187 #endif
   188 }
   189 
   190 static int
   191 EMSCRIPTENAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
   192 {
   193     SDL_bool valid_format = SDL_FALSE;
   194     SDL_AudioFormat test_format;
   195     int result;
   196 
   197     /* based on parts of library_sdl.js */
   198 
   199     /* create context (TODO: this puts stuff in the global namespace...)*/
   200     result = EM_ASM_INT({
   201         if(typeof(SDL2) === 'undefined') {
   202             SDL2 = {};
   203         }
   204         if (!$0) {
   205             SDL2.audio = {};
   206         } else {
   207             SDL2.capture = {};
   208         }
   209 
   210         if (!SDL2.audioContext) {
   211             if (typeof(AudioContext) !== 'undefined') {
   212                 SDL2.audioContext = new AudioContext();
   213             } else if (typeof(webkitAudioContext) !== 'undefined') {
   214                 SDL2.audioContext = new webkitAudioContext();
   215             }
   216         }
   217         return SDL2.audioContext === undefined ? -1 : 0;
   218     }, iscapture);
   219     if (result < 0) {
   220         return SDL_SetError("Web Audio API is not available!");
   221     }
   222 
   223     test_format = SDL_FirstAudioFormat(this->spec.format);
   224     while ((!valid_format) && (test_format)) {
   225         switch (test_format) {
   226         case AUDIO_F32: /* web audio only supports floats */
   227             this->spec.format = test_format;
   228 
   229             valid_format = SDL_TRUE;
   230             break;
   231         }
   232         test_format = SDL_NextAudioFormat();
   233     }
   234 
   235     if (!valid_format) {
   236         /* Didn't find a compatible format :( */
   237         return SDL_SetError("No compatible audio format!");
   238     }
   239 
   240     /* Initialize all variables that we clean on shutdown */
   241 #if 0  /* !!! FIXME: currently not used. Can we move some stuff off the SDL2 namespace? --ryan. */
   242     this->hidden = (struct SDL_PrivateAudioData *)
   243         SDL_malloc((sizeof *this->hidden));
   244     if (this->hidden == NULL) {
   245         return SDL_OutOfMemory();
   246     }
   247     SDL_zerop(this->hidden);
   248 #endif
   249 
   250     /* limit to native freq */
   251     this->spec.freq = EM_ASM_INT_V({ return SDL2.audioContext.sampleRate; });
   252 
   253     SDL_CalculateAudioSpec(&this->spec);
   254 
   255     if (iscapture) {
   256         /* The idea is to take the capture media stream, hook it up to an
   257            audio graph where we can pass it through a ScriptProcessorNode
   258            to access the raw PCM samples and push them to the SDL app's
   259            callback. From there, we "process" the audio data into silence
   260            and forget about it. */
   261 
   262         /* This should, strictly speaking, use MediaRecorder for capture, but
   263            this API is cleaner to use and better supported, and fires a
   264            callback whenever there's enough data to fire down into the app.
   265            The downside is that we are spending CPU time silencing a buffer
   266            that the audiocontext uselessly mixes into any output. On the
   267            upside, both of those things are not only run in native code in
   268            the browser, they're probably SIMD code, too. MediaRecorder
   269            feels like it's a pretty inefficient tapdance in similar ways,
   270            to be honest. */
   271 
   272         EM_ASM_({
   273             var have_microphone = function(stream) {
   274                 //console.log('SDL audio capture: we have a microphone! Replacing silence callback.');
   275                 if (SDL2.capture.silenceTimer !== undefined) {
   276                     clearTimeout(SDL2.capture.silenceTimer);
   277                     SDL2.capture.silenceTimer = undefined;
   278                 }
   279                 SDL2.capture.mediaStreamNode = SDL2.audioContext.createMediaStreamSource(stream);
   280                 SDL2.capture.scriptProcessorNode = SDL2.audioContext.createScriptProcessor($1, $0, 1);
   281                 SDL2.capture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {
   282                     if ((SDL2 === undefined) || (SDL2.capture === undefined)) { return; }
   283                     audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0);
   284                     SDL2.capture.currentCaptureBuffer = audioProcessingEvent.inputBuffer;
   285                     Runtime.dynCall('vi', $2, [$3]);
   286                 };
   287                 SDL2.capture.mediaStreamNode.connect(SDL2.capture.scriptProcessorNode);
   288                 SDL2.capture.scriptProcessorNode.connect(SDL2.audioContext.destination);
   289                 SDL2.capture.stream = stream;
   290             };
   291 
   292             var no_microphone = function(error) {
   293                 //console.log('SDL audio capture: we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.');
   294             };
   295 
   296             /* we write silence to the audio callback until the microphone is available (user approves use, etc). */
   297             SDL2.capture.silenceBuffer = SDL2.audioContext.createBuffer($0, $1, SDL2.audioContext.sampleRate);
   298             SDL2.capture.silenceBuffer.getChannelData(0).fill(0.0);
   299             var silence_callback = function() {
   300                 SDL2.capture.currentCaptureBuffer = SDL2.capture.silenceBuffer;
   301                 Runtime.dynCall('vi', $2, [$3]);
   302             };
   303 
   304             SDL2.capture.silenceTimer = setTimeout(silence_callback, ($1 / SDL2.audioContext.sampleRate) * 1000);
   305 
   306             if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) {
   307                 navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone);
   308             } else if (navigator.webkitGetUserMedia !== undefined) {
   309                 navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone);
   310             }
   311         }, this->spec.channels, this->spec.samples, HandleCaptureProcess, this);
   312     } else {
   313         /* setup a ScriptProcessorNode */
   314         EM_ASM_ARGS({
   315             SDL2.audio.scriptProcessorNode = SDL2.audioContext['createScriptProcessor']($1, 0, $0);
   316             SDL2.audio.scriptProcessorNode['onaudioprocess'] = function (e) {
   317                 if ((SDL2 === undefined) || (SDL2.audio === undefined)) { return; }
   318                 SDL2.audio.currentOutputBuffer = e['outputBuffer'];
   319                 Runtime.dynCall('vi', $2, [$3]);
   320             };
   321             SDL2.audio.scriptProcessorNode['connect'](SDL2.audioContext['destination']);
   322         }, this->spec.channels, this->spec.samples, HandleAudioProcess, this);
   323     }
   324 
   325     return 0;
   326 }
   327 
   328 static int
   329 EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl)
   330 {
   331     /* Set the function pointers */
   332     impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice;
   333     impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice;
   334 
   335     impl->OnlyHasDefaultOutputDevice = 1;
   336 
   337     /* no threads here */
   338     impl->SkipMixerLock = 1;
   339     impl->ProvidesOwnCallbackThread = 1;
   340 
   341     /* check availability */
   342     const int available = EM_ASM_INT_V({
   343         if (typeof(AudioContext) !== 'undefined') {
   344             return 1;
   345         } else if (typeof(webkitAudioContext) !== 'undefined') {
   346             return 1;
   347         }
   348         return 0;
   349     });
   350 
   351     if (!available) {
   352         SDL_SetError("No audio context available");
   353     }
   354 
   355     const int capture_available = available && EM_ASM_INT_V({
   356         if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) {
   357             return 1;
   358         } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') {
   359             return 1;
   360         }
   361         return 0;
   362     });
   363 
   364     impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE;
   365     impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE;
   366 
   367     return available;
   368 }
   369 
   370 AudioBootStrap EMSCRIPTENAUDIO_bootstrap = {
   371     "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, 0
   372 };
   373 
   374 #endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */
   375 
   376 /* vi: set ts=4 sw=4 expandtab: */