Atlas - SDL_emscriptenaudio.c
Home / ext / SDL2 / src / audio / emscripten Lines: 1 | Size: 15102 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2018 Sam Lantinga <[email protected]> 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 33static void 34FeedAudioDevice(_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 52static void 53HandleAudioProcess(_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 90static void 91HandleCaptureProcess(_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 144static void 145EMSCRIPTENAUDIO_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 190static int 191EMSCRIPTENAUDIO_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 328static int 329EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl * impl) 330{ 331 int available; 332 int capture_available; 333 334 /* Set the function pointers */ 335 impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice; 336 impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice; 337 338 impl->OnlyHasDefaultOutputDevice = 1; 339 340 /* no threads here */ 341 impl->SkipMixerLock = 1; 342 impl->ProvidesOwnCallbackThread = 1; 343 344 /* check availability */ 345 available = EM_ASM_INT_V({ 346 if (typeof(AudioContext) !== 'undefined') { 347 return 1; 348 } else if (typeof(webkitAudioContext) !== 'undefined') { 349 return 1; 350 } 351 return 0; 352 }); 353 354 if (!available) { 355 SDL_SetError("No audio context available"); 356 } 357 358 capture_available = available && EM_ASM_INT_V({ 359 if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) { 360 return 1; 361 } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') { 362 return 1; 363 } 364 return 0; 365 }); 366 367 impl->HasCaptureSupport = capture_available ? SDL_TRUE : SDL_FALSE; 368 impl->OnlyHasDefaultCaptureDevice = capture_available ? SDL_TRUE : SDL_FALSE; 369 370 return available; 371} 372 373AudioBootStrap EMSCRIPTENAUDIO_bootstrap = { 374 "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, 0 375}; 376 377#endif /* SDL_AUDIO_DRIVER_EMSCRIPTEN */ 378 379/* vi: set ts=4 sw=4 expandtab: */ 380[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.