src/audio/emscripten/SDL_emscriptenaudio.c
author Ryan C. Gordon <icculus@icculus.org>
Fri, 06 Jan 2017 01:02:58 -0500
changeset 10764 f9bf759e9dd1
parent 10761 6afae547c925
child 10765 61312c8c59fe
permissions -rw-r--r--
audio: Fixed SDL_AudioStreamGet() function parameters.

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