diff options
Diffstat (limited to 'src/library_sdl.js')
-rw-r--r-- | src/library_sdl.js | 280 |
1 files changed, 188 insertions, 92 deletions
diff --git a/src/library_sdl.js b/src/library_sdl.js index d292f753..2860669c 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -704,7 +704,7 @@ var LibrarySDL = { // since the browser engine handles that for us. Therefore, in JS we just // maintain a list of channels and return IDs for them to the SDL consumer. allocateChannels: function(num) { // called from Mix_AllocateChannels and init - if (SDL.numChannels && SDL.numChannels >= num) return; + if (SDL.numChannels && SDL.numChannels >= num && num != 0) return; SDL.numChannels = num; SDL.channels = []; for (var i = 0; i < num; i++) { @@ -1454,105 +1454,200 @@ var LibrarySDL = { // SDL_Audio - // TODO fix SDL_OpenAudio, and add some tests for it. It's currently broken. SDL_OpenAudio: function(desired, obtained) { - SDL.allocateChannels(32); - - SDL.audio = { - freq: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.freq', 'i32', 0, 1) }}}, - format: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.format', 'i16', 0, 1) }}}, - channels: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.channels', 'i8', 0, 1) }}}, - samples: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.samples', 'i16', 0, 1) }}}, - callback: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.callback', 'void*', 0, 1) }}}, - userdata: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.userdata', 'void*', 0, 1) }}}, - soundSource: new Array(), - nextSoundSource: 0, - lastSoundSource: -1, - nextPlayTime: 0, - paused: true, - timer: null - }; - - if (obtained) { - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.freq', 'SDL.audio.freq', 'i32') }}}; // no good way for us to know if the browser can really handle this - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.format', 33040, 'i16') }}}; // float, signed, 16-bit - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.channels', 'SDL.audio.channels', 'i8') }}}; - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.silence', makeGetValue('desired', 'SDL.structs.AudioSpec.silence', 'i8', 0, 1), 'i8') }}}; // unclear if browsers can provide this - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.samples', 'SDL.audio.samples', 'i16') }}}; - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.callback', 'SDL.audio.callback', '*') }}}; - {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.userdata', 'SDL.audio.userdata', '*') }}}; - } - - var totalSamples = SDL.audio.samples*SDL.audio.channels; - SDL.audio.bufferSize = totalSamples*2; // hardcoded 16-bit audio - SDL.audio.buffer = _malloc(SDL.audio.bufferSize); - SDL.audio.caller = function() { - Runtime.dynCall('viii', SDL.audio.callback, [SDL.audio.userdata, SDL.audio.buffer, SDL.audio.bufferSize]); - SDL.audio.pushAudio(SDL.audio.buffer, SDL.audio.bufferSize); - }; - // Mozilla Audio API/WebAudioAPI + // On Firefox, we prefer Mozilla Audio API. Otherwise, use WebAudioAPI. try { + SDL.audio = { + freq: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.freq', 'i32', 0, 1) }}}, + format: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.format', 'i16', 0, 1) }}}, + channels: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.channels', 'i8', 0, 1) }}}, + samples: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.samples', 'i16', 0, 1) }}}, // Samples in the CB buffer per single sound channel. + callback: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.callback', 'void*', 0, 1) }}}, + userdata: {{{ makeGetValue('desired', 'SDL.structs.AudioSpec.userdata', 'void*', 0, 1) }}}, + paused: true, + timer: null + }; + // The .silence field tells the constant sample value that corresponds to the safe un-skewed silence value for the wave data. + if (SDL.audio.format == 0x0008 /*AUDIO_U8*/) { + SDL.audio.silence = 128; // Audio ranges in [0, 255], so silence is half-way in between. + } else if (SDL.audio.format == 0x8010 /*AUDIO_S16LSB*/) { + SDL.audio.silence = 0; // Signed data in range [-32768, 32767], silence is 0. + } else { + throw 'Invalid SDL audio format ' + SDL.audio.format + '!'; + } + // Round the desired audio frequency up to the next 'common' frequency value. + // Web Audio API spec states 'An implementation must support sample-rates in at least the range 22050 to 96000.' + if (SDL.audio.freq <= 0) { + throw 'Unsupported sound frequency ' + SDL.audio.freq + '!'; + } else if (SDL.audio.freq <= 22050) { + SDL.audio.freq = 22050; // Take it safe and clamp everything lower than 22kHz to that. + } else if (SDL.audio.freq <= 32000) { + SDL.audio.freq = 32000; + } else if (SDL.audio.freq <= 44100) { + SDL.audio.freq = 44100; + } else if (SDL.audio.freq <= 48000) { + SDL.audio.freq = 48000; + } else if (SDL.audio.freq <= 96000) { + SDL.audio.freq = 96000; + } else { + throw 'Unsupported sound frequency ' + SDL.audio.freq + '!'; + } + if (SDL.audio.channels == 0) { + SDL.audio.channels = 1; // In SDL both 0 and 1 mean mono. + } else if (SDL.audio.channels < 0 || SDL.audio.channels > 32) { + throw 'Unsupported number of audio channels for SDL audio: ' + SDL.audio.channels + '!'; + } else if (SDL.audio.channels != 1 && SDL.audio.channels != 2) { // Unsure what SDL audio spec supports. Web Audio spec supports up to 32 channels. + console.log('Warning: Using untested number of audio channels ' + SDL.audio.channels); + } + if (SDL.audio.samples < 1024 || SDL.audio.samples > 524288 /* arbitrary cap */) { + throw 'Unsupported audio callback buffer size ' + SDL.audio.samples + '!'; + } else if ((SDL.audio.samples & (SDL.audio.samples-1)) != 0) { + throw 'Audio callback buffer size ' + SDL.audio.samples + ' must be a power-of-two!'; + } + + var totalSamples = SDL.audio.samples*SDL.audio.channels; + SDL.audio.bytesPerSample = (SDL.audio.format == 0x0008 /*AUDIO_U8*/ || SDL.audio.format == 0x8008 /*AUDIO_S8*/) ? 1 : 2; + SDL.audio.bufferSize = totalSamples*SDL.audio.bytesPerSample; + SDL.audio.buffer = _malloc(SDL.audio.bufferSize); + + // Create a callback function that will be routinely called to ask more audio data from the user application. + SDL.audio.caller = function() { + if (!SDL.audio) { + return; + } + Runtime.dynCall('viii', SDL.audio.callback, [SDL.audio.userdata, SDL.audio.buffer, SDL.audio.bufferSize]); + SDL.audio.pushAudio(SDL.audio.buffer, SDL.audio.bufferSize); + }; + SDL.audio.audioOutput = new Audio(); - SDL.audio.hasWebAudio = ((typeof(AudioContext) === 'function')||(typeof(webkitAudioContext) === 'function')); - if(!SDL.audio.hasWebAudio&&(typeof(SDL.audio.audioOutput['mozSetup'])==='function')){ - SDL.audio.audioOutput['mozSetup'](SDL.audio.channels, SDL.audio.freq); // use string attributes on mozOutput for closure compiler - SDL.audio.mozBuffer = new Float32Array(totalSamples); - SDL.audio.pushAudio = function(ptr, size) { - var mozBuffer = SDL.audio.mozBuffer; + if (typeof(SDL.audio.audioOutput['mozSetup'])==='function') { // Primarily use Mozilla Audio Data API if available. + SDL.audio.audioOutput['mozSetup'](SDL.audio.channels, SDL.audio.freq); // use string attributes on mozOutput for closure compiler + SDL.audio.mozBuffer = new Float32Array(totalSamples); + SDL.audio.pushAudio = function(ptr, size) { + var mozBuffer = SDL.audio.mozBuffer; + // The input audio data for SDL audio is either 8-bit or 16-bit interleaved across channels, output for Mozilla Audio Data API + // needs to be Float32 interleaved, so perform a sample conversion. + if (SDL.audio.format == 0x8010 /*AUDIO_S16LSB*/) { + for (var i = 0; i < totalSamples; i++) { + mozBuffer[i] = ({{{ makeGetValue('ptr', 'i*2', 'i16', 0, 0) }}}) / 0x8000; + } + } else if (SDL.audio.format == 0x0008 /*AUDIO_U8*/) { for (var i = 0; i < totalSamples; i++) { - mozBuffer[i] = ({{{ makeGetValue('ptr', 'i*2', 'i16', 0, 0) }}}) / 0x8000; // hardcoded 16-bit audio, signed (TODO: reSign if not ta2?) + var v = ({{{ makeGetValue('ptr', 'i', 'i8', 0, 0) }}}); + mozBuffer[i] = ((v >= 0) ? v-128 : v+128) /128; } - SDL.audio.audioOutput['mozWriteAudio'](mozBuffer); } - }else{ - if (typeof(AudioContext) === 'function') { - SDL.audio.context = new AudioContext(); - } else if (typeof(webkitAudioContext) === 'function') { - SDL.audio.context = new webkitAudioContext(); - } else { - throw 'no sound!'; - } - SDL.audio.nextSoundSource = 0; - SDL.audio.soundSource = new Array(); - SDL.audio.nextPlayTime = 0; - SDL.audio.pushAudio=function(ptr,size){ - if(SDL.audio.lastSoundSource>-1){ - if(SDL.audio.soundSource[SDL.audio.lastSoundSource].playbackState === 3){ - SDL.audio.soundSource = new Array(); - SDL.audio.nextPlayTime = 0; - SDL.audio.lastSoundSource = -1; - SDL.audio.nextSoundSource = 0; - } - } - SDL.audio.soundSource[SDL.audio.nextSoundSource] = SDL.audio.context.createBufferSource(); - SDL.audio.soundSource[SDL.audio.nextSoundSource].connect(SDL.audio.context.destination); - SDL.audio.soundSource[SDL.audio.nextSoundSource].buffer = SDL.audio.context.createBuffer(SDL.audio.channels,(size / totalSamples),SDL.audio.freq); - for(var j = 0; j<SDL.audio.channels; j++){ - var channelData = SDL.audio.SoundSource[SDL.audio.nextSoundSource].buffer.getChannelData(j); - var samples = SDL.audio.samples; - for(var i = 0; i<samples; i++){ - channelData[i] = ({{{ makeGetValue('ptr', '(i+samples*j)*2', 'i16', 0, 0) }}}) / 0x8000; // hardcoded 16-bit audio, signed (TODO: reSign if not ta2?) - } - } - SDL.audio.nextPlayTime = SDL.audio.context.currentTime+SDL.audio.soundSource[SDL.audio.nextSoundSource].buffer.duration; - - - SDL.audio.soundSource[SDL.audio.nextSoundSource].start(SDL.audio.nextPlayTime); - - SDL.audio.lastSoundSource = SDL.Audio.nextSoundSource; - SDL.Audio.nextSoundSource++; + SDL.audio.audioOutput['mozWriteAudio'](mozBuffer); + } + } else { + // Initialize Web Audio API if we haven't done so yet. Note: Only initialize Web Audio context ever once on the web page, + // since initializing multiple times fails on Chrome saying 'audio resources have been exhausted'. + if (!SDL.audioContext) { + if (typeof(AudioContext) === 'function') { + SDL.audioContext = new AudioContext(); + } else if (typeof(webkitAudioContext) === 'function') { + SDL.audioContext = new webkitAudioContext(); + } else { + throw 'Web Audio API is not available!'; + } + } + SDL.audio.soundSource = new Array(); // Use an array of sound sources as a ring buffer to queue blocks of synthesized audio to Web Audio API. + SDL.audio.nextSoundSource = 0; // Index of the next sound buffer in the ring buffer queue to play. + SDL.audio.nextPlayTime = 0; // Time in seconds when the next audio block is due to start. + + // The pushAudio function with a new audio buffer whenever there is new audio data to schedule to be played back on the device. + SDL.audio.pushAudio=function(ptr,sizeBytes) { + try { + var sizeSamples = sizeBytes / SDL.audio.bytesPerSample; // How many samples fit in the callback buffer? + var sizeSamplesPerChannel = sizeSamples / SDL.audio.channels; // How many samples per a single channel fit in the cb buffer? + if (sizeSamplesPerChannel != SDL.audio.samples) { + throw 'Received mismatching audio buffer size!'; } + // Allocate new sound buffer to be played. + var source = SDL.audioContext.createBufferSource(); + SDL.audio.soundSource[SDL.audio.nextSoundSource] = source; + source.buffer = SDL.audioContext.createBuffer(SDL.audio.channels,sizeSamplesPerChannel,SDL.audio.freq); + SDL.audio.soundSource[SDL.audio.nextSoundSource].connect(SDL.audioContext.destination); + + // The input audio data is interleaved across the channels, i.e. [L, R, L, R, L, R, ...] and is either 8-bit or 16-bit as + // supported by the SDL API. The output audio wave data for Web Audio API must be in planar buffers of [-1,1]-normalized Float32 data, + // so perform a buffer conversion for the data. + var numChannels = SDL.audio.channels; + for(var i = 0; i < numChannels; ++i) { + var channelData = SDL.audio.soundSource[SDL.audio.nextSoundSource].buffer.getChannelData(i); + if (channelData.length != sizeSamplesPerChannel) { + throw 'Web Audio output buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + sizeSamplesPerChannel + ' samples!'; + } + if (SDL.audio.format == 0x8010 /*AUDIO_S16LSB*/) { + for(var j = 0; j < sizeSamplesPerChannel; ++j) { + channelData[j] = ({{{ makeGetValue('ptr', '(j*numChannels + i)*2', 'i16', 0, 0) }}}) / 0x8000; + } + } else if (SDL.audio.format == 0x0008 /*AUDIO_U8*/) { + for(var j = 0; j < sizeSamplesPerChannel; ++j) { + var v = ({{{ makeGetValue('ptr', 'j*numChannels + i', 'i8', 0, 0) }}}); + channelData[j] = ((v >= 0) ? v-128 : v+128) /128; + } + } + } + + // Schedule the generated sample buffer to be played out at the correct time right after the previously scheduled + // sample buffer has finished. + var curtime = SDL.audioContext.currentTime; + if (curtime > SDL.audio.nextPlayTime && SDL.audio.nextPlayTime != 0) { + console.log('warning: Audio callback had starved sending audio by ' + (curtime - SDL.audio.nextPlayTime) + ' seconds.'); + // Immediately queue up an extra buffer to force the sound feeding to be ahead by one sample block: + Browser.safeSetTimeout(SDL.audio.caller, 1); + } + var playtime = Math.max(curtime, SDL.audio.nextPlayTime); + SDL.audio.soundSource[SDL.audio.nextSoundSource].start(playtime); + SDL.audio.nextPlayTime = playtime + SDL.audio.soundSource[SDL.audio.nextSoundSource].buffer.duration; + SDL.audio.nextSoundSource = (SDL.audio.nextSoundSource + 1) % 4; + } catch(e) { + console.log('Web Audio API error playing back audio: ' + e.toString()); + } + } } + + if (obtained) { + // Report back the initialized audio parameters. + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.freq', 'SDL.audio.freq', 'i32') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.format', 'SDL.audio.format', 'i16') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.channels', 'SDL.audio.channels', 'i8') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.silence', makeGetValue('desired', 'SDL.structs.AudioSpec.silence', 'i8', 0, 1), 'i8') }}}; // unclear if browsers can provide this + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.samples', 'SDL.audio.samples', 'i16') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.callback', 'SDL.audio.callback', '*') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.userdata', 'SDL.audio.userdata', '*') }}}; + } + SDL.allocateChannels(32); + } catch(e) { + console.log('Initializing SDL audio threw an exception: "' + e.toString() + '"! Continuing without audio.'); SDL.audio = null; + SDL.allocateChannels(0); + if (obtained) { + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.freq', 0, 'i32') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.format', 0, 'i16') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.channels', 0, 'i8') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.silence', 0, 'i8') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.samples', 0, 'i16') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.callback', 0, '*') }}}; + {{{ makeSetValue('obtained', 'SDL.structs.AudioSpec.userdata', 0, '*') }}}; + } + } + if (!SDL.audio) { + return -1; } - if (!SDL.audio) return -1; return 0; }, SDL_PauseAudio: function(pauseOn) { + if (!SDL.audio) { + return; + } if (SDL.audio.paused !== pauseOn) { - SDL.audio.timer = pauseOn ? SDL.audio.timer && clearInterval(SDL.audio.timer) : Browser.safeSetInterval(SDL.audio.caller, 1/35); + SDL.audio.timer = pauseOn ? SDL.audio.timer && clearInterval(SDL.audio.timer) : Browser.safeSetInterval(SDL.audio.caller, 1000 * SDL.audio.samples / SDL.audio.freq); + // Immediately queue up a buffer to make the sound feeding to be ahead by one sample block. + Browser.safeSetTimeout(SDL.audio.caller, 1); } SDL.audio.paused = pauseOn; }, @@ -1560,17 +1655,18 @@ var LibrarySDL = { SDL_CloseAudio__deps: ['SDL_PauseAudio', 'free'], SDL_CloseAudio: function() { if (SDL.audio) { - try{ - for(var i = 0; i<SDL.audio.soundSource.length;i++){ - if(!(typeof(SDL.audio.soundSource[i]==='undefined'))){ - SDL.audio.soundSource[i].stop(0); - } - } - }catch(e){} - SDL.audo.soundSource = null; + try{ + for(var i = 0; i < SDL.audio.soundSource.length; ++i) { + if (!(typeof(SDL.audio.soundSource[i]==='undefined'))) { + SDL.audio.soundSource[i].stop(0); + } + } + } catch(e) {} + SDL.audio.soundSource = null; _SDL_PauseAudio(1); _free(SDL.audio.buffer); SDL.audio = null; + SDL.allocateChannels(0); } }, |