diff --git a/AUTHORS b/AUTHORS index e9e31489f0ee5..1cfa87d957b6a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -309,3 +309,5 @@ a license to everyone to use it as detailed in LICENSE.) * Jonathan Hale * Etienne Brateau * Zhiming Wang +* Jameson Ernst +* Yoan Lecoq diff --git a/site/source/docs/porting/Audio.rst b/site/source/docs/porting/Audio.rst new file mode 100644 index 0000000000000..241559efbf74b --- /dev/null +++ b/site/source/docs/porting/Audio.rst @@ -0,0 +1,125 @@ +.. _Audio: + +===== +Audio +===== + +Emscripten ships with its own implementation of the OpenAL 1.1 API, using the Web Audio API as a backend. + +You can reasonably expect ported OpenAL applications to "just work" with no additional effort. Just link with the ``-lopenal`` linker flag. + +There are some implementation-specific aspects which are worth considering and are documented here. + +.. warning:: There used to be no additional flag to pass to the compiler in order to use OpenAL. However, specifying ``-lopenal`` as mentioned above should be considered mandatory (at some point in the future, it **will** be)! + + +.. _Audio-openal-extensions-g: + +Supported OpenAL extensions +=========================== + +The following extensions are supported by Emscripten's OpenAL implementation. + +- ``ALC_SOFT_pause_device``; +- ``ALC_SOFT_HRTF``; +- ``AL_EXT_float32``; +- ``AL_SOFT_loop_points``; +- ``AL_SOFT_source_length``; +- ``AL_EXT_source_distance_model``; +- ``AL_SOFT_source_spatialize``; + +.. warning:: This doesn't mean you should just assume their presence ! For correctness, you should **always** check if an extension is supported before using it, like a good application would do. + + +.. _Audio-guidelines-g: + +Guidelines for Audio on Emscripten +================================== + +Know that your application needs to yield to the Javascript main loop for audio processing to take place (See :ref:`Browser main loop `). + +Put simply, this kind of code will block indefinitely : + +.. code-block:: c + + while(nframes < THE_NUMBER_OF_FRAMES_WE_WANT) + alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, 1, &nframes); + +The above snippet usually works in native applications because most OpenAL implementations own and manage one or more separate threads. This is **not** the case in Emscripten. + + +What you must do instead is perform each such query only once per "main loop iteration" (i.e the callback you provide via :c:func:`emscripten_set_main_loop` or :c:func:`emscripten_set_main_loop_arg`). + + +.. _Audio-openal-capture-behavior-g: + +Emscripten-specific capture behavior +==================================== + +Attempting to open an input stream to the user's audio capture device +results in the asynchronous appearance of a small browser-specific dialog +asking for the user's permission, and on some browsers, the capture device to choose. + + +With this in mind, when ``alcCaptureOpenDevice()`` is called with valid and +supported parameters, a "proxy" device is returned, which successfully +captures no sample until the user clicks "Allow" on said dialog. + +That means, when calling ``alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, 1, &nframes)``, ``nframes`` remains set to zero until the user clicks "Allow". You might want to make your app handle this specific behavior. + +If the user clicks "Deny", the device is invalidated (because this is somewhat +similar to unplugging the physical device) and calls to ``alcCapture*`` functions on that +device then consistently fail with ``ALC_INVALID_DEVICE``. +Your application should be prepared to handle this properly. + +.. note:: + Some browsers "remember" this choice and apply it automatically every time it would be asked again instead. + There's no way for the implementation to detect this behavior. + + +.. _Audio-openal-capture-details-g: + +Useful implementation details of OpenAL capture +=============================================== + +Internally, Web Audio's capture data is always backed by a Javascript ``Float32Array``. +Thus, ``AL_FORMAT_MONO_FLOAT32`` and ``AL_FORMAT_STEREO_FLOAT32`` are the only formats which do not require converting acquired samples from their initial type to another. + +Also, the actual sample rate at which samples are acquired from the device is currently dictated by the browser and hardware, instead of user code. If this sample rate does not match the one your app requests, the implementation is required to perform resampling on your behalf. + +That sample rate is given by ``audioCtx.sampleRate``, where ``audioCtx`` is the ``AudioContext`` object used internally by the relevant capture ``ALCdevice``. +Currently, Emscripten provides no direct way for applications to access this value, but this might be provided through an Emscripten-specific OpenAL extension (which is not here yet because it requires registration). + +Right now though there's a quick and *de facto* reliable way to do this (C example): + +.. code-block:: c + + #ifdef __EMSCRIPTEN__ + + #include + + // Avoid calling this more than once! Caching the value is up to you. + unsigned query_sample_rate_of_audiocontexts() { + return EM_ASM_INT_V({ + var AudioContext = window.AudioContext || window.webkitAudioContext; + var ctx = new AudioContext(); + var sr = ctx.sampleRate; + ctx.close(); + return sr; + }); + } + #endif + +It is reasonable to expect this sample rate to be either 44100Hz or 48000Hz. You can test it right now by copying the javascript part into your browser's console if you're curious. + + +.. _Audio-improving-g: + +Improving and extending the implementation +========================================== + +Currently, the OpenAL capture implementation performs naïve linear resampling because it is simple and the small loss of quality was deemed acceptable. + +However, you're welcome to improve this by contributing if you so wish! See `this issue `_ , for instance. + +Likewise, if you'd like to see a particlar extension implemented (whether it is registered or not), then your best bet is to either file an issue (provided no related prior issue existed for this), or contribute! See :ref:`Contributing ` for guidelines. diff --git a/site/source/docs/porting/index.rst b/site/source/docs/porting/index.rst index c1d3e4355cda7..9627c394c102b 100644 --- a/site/source/docs/porting/index.rst +++ b/site/source/docs/porting/index.rst @@ -14,6 +14,7 @@ The topics in this section cover the main integration points that you need to co connecting_cpp_and_javascript/index files/index multimedia_and_graphics/index + Audio Debugging pthreads simd diff --git a/src/deps_info.json b/src/deps_info.json index 391ce20f508bd..1d694eeef1514 100644 --- a/src/deps_info.json +++ b/src/deps_info.json @@ -36,6 +36,10 @@ "eglGetProcAddress": ["emscripten_GetProcAddress"], "glfwGetProcAddress": ["emscripten_GetProcAddress"], "emscripten_GetProcAddress": ["strstr"], + "alcGetProcAddress": ["emscripten_GetAlcProcAddress"], + "alGetProcAddress": ["emscripten_GetAlProcAddress"], + "emscripten_GetAlcProcAddress": ["strcmp"], + "emscripten_GetAlProcAddress": ["strcmp"], "emscripten_get_preloaded_image_data_from_FILE": ["fileno"], "__cxa_begin_catch": ["__cxa_can_catch", "__cxa_is_pointer_type"], "emscripten_log": ["strlen"], diff --git a/src/library_openal.js b/src/library_openal.js index 9d869f04e7ca9..1fcf570b16fd5 100644 --- a/src/library_openal.js +++ b/src/library_openal.js @@ -1,100 +1,190 @@ -//"use strict"; +//'use strict'; var LibraryOpenAL = { + // ************************************************************************ + // ** INTERNALS + // ************************************************************************ + $AL__deps: ['$Browser'], $AL: { - contexts: [], - currentContext: null, + // ------------------------------------------------------ + // -- Constants + // ------------------------------------------------------ - alcErr: 0, + QUEUE_INTERVAL: 25, + QUEUE_LOOKAHEAD: 100.0 / 1000.0, - stringCache: {}, - alcStringCache: {}, + DEVICE_NAME: 'Emscripten OpenAL', + CAPTURE_DEVICE_NAME: 'Emscripten OpenAL capture', - QUEUE_INTERVAL: 25, - QUEUE_LOOKAHEAD: 100, + ALC_EXTENSIONS: { + // TODO: 'ALC_EXT_EFX': true, + 'ALC_SOFT_pause_device': true, + 'ALC_SOFT_HRTF': true + }, + AL_EXTENSIONS: { + 'AL_EXT_float32': true, + 'AL_SOFT_loop_points': true, + 'AL_SOFT_source_length': true, + 'AL_EXT_source_distance_model': true, + 'AL_SOFT_source_spatialize': true + }, - newSrcId: 1, + // ------------------------------------------------------ + // -- ALC Fields + // ------------------------------------------------------ -#if OPENAL_DEBUG - //This function is slow and used only for debugging purposes - srcIdBySrc: function srcIdBySrc(src) { - var idx = 0; - for (var srcId in AL.currentContext.src) { - if (AL.currentContext.src[srcId] == src) { - idx = srcId; - break; - } + _alcErr: 0, + get alcErr() { + return this._alcErr; + }, + set alcErr(val) { + // Errors should not be overwritten by later errors until they are cleared by a query. + if (this._alcErr === 0 /* ALC_NO_ERROR */ || val === 0 /* ALC_NO_ERROR */) { + this._alcErr = val; } - return idx; }, -#endif - updateSources: function updateSources(context) { + deviceRefCounts: {}, + alcStringCache: {}, + paused: false, + + // ------------------------------------------------------ + // -- AL Fields + // ------------------------------------------------------ + + stringCache: {}, + contexts: {}, + currentCtx: null, + buffers: { + // The zero buffer is legal to use, so create a placeholder for it + '0': { + id: 0, + refCount: 0, + audioBuf: null, + frequency: 0, + bytesPerSample: 2, + channels: 1, + length: 0 + } + }, + paramArray: [], // Used to prevent allocating a new array for each param call + + _nextId: 1, + newId: function() { + return AL.freeIds.length > 0 ? AL.freeIds.pop() : AL._nextId++; + }, + freeIds: [], + + // ------------------------------------------------------ + // -- Mixing Logic + // ------------------------------------------------------ + + scheduleContextAudio: function(ctx) { // If we are animating using the requestAnimationFrame method, then the main loop does not run when in the background. // To give a perfect glitch-free audio stop when switching from foreground to background, we need to avoid updating // audio altogether when in the background, so detect that case and kill audio buffer streaming if so. - if (Browser.mainLoop.timingMode == 1/*EM_TIMING_RAF*/ && document['visibilityState'] != 'visible') return; + if (Browser.mainLoop.timingMode === 1 /* EM_TIMING_RAF */ && document['visibilityState'] != 'visible') { + return; + } - for (var srcId in context.src) { - AL.updateSource(context.src[srcId]); + for (var i in ctx.sources) { + AL.scheduleSourceAudio(ctx.sources[i]); } }, - updateSource: function updateSource(src) { - // See comment on updateSources above. - if (Browser.mainLoop.timingMode == 1/*EM_TIMING_RAF*/ && document['visibilityState'] != 'visible') return; - -#if OPENAL_DEBUG - var idx = AL.srcIdBySrc(src); -#endif + // This function is the core scheduler that queues web-audio buffers for output. + // src.bufQueue represents the abstract OpenAL buffer queue, which is taversed to schedule + // corresponding web-audio buffers. These buffers are stored in src.audioQueue, which + // represents the queue of buffers scheduled for physical playback. These two queues are + // distinct because of the differing semantics of OpenAL and web audio. Some changes + // to OpenAL parameters, such as pitch, may require the web audio queue to be flushed and rescheduled. + scheduleSourceAudio: function(src, lookahead) { + // See comment on scheduleContextAudio above. + if (Browser.mainLoop.timingMode === 1 /*EM_TIMING_RAF*/ && document['visibilityState'] != 'visible') { + return; + } if (src.state !== 0x1012 /* AL_PLAYING */) { return; } - var currentTime = src.context.ctx.currentTime; - var startTime = src.bufferPosition; + var currentTime = AL.updateSourceTime(src); + + var startTime = src.bufStartTime; + var startOffset = src.bufOffset; + var bufCursor = src.bufsProcessed; - for (var i = src.buffersPlayed; i < src.queue.length; i++) { - var entry = src.queue[i]; + // Advance past any audio that is already scheduled + for (var i = 0; i < src.audioQueue.length; i++) { + var audioSrc = src.audioQueue[i]; + startTime = audioSrc._startTime + audioSrc._duration; + startOffset = 0.0; + bufCursor += audioSrc._skipCount + 1; + } - var startOffset = (startTime - currentTime) / src.playbackRate; - var endTime; - if (entry.src) endTime = startTime + entry.src.duration; // n.b. entry.src.duration already factors in playbackRate, so no divide by src.playbackRate on it. - else endTime = startTime + entry.buffer.duration / src.playbackRate; + if (!lookahead) { + lookahead = AL.QUEUE_LOOKAHEAD; + } + var lookaheadTime = currentTime + lookahead; + var skipCount = 0; + while (startTime < lookaheadTime) { + if (bufCursor >= src.bufQueue.length) { + if (src.looping) { + bufCursor %= src.bufQueue.length; + } else { + break; + } + } - // Clean up old buffers. - if (currentTime >= endTime) { - // Update our location in the queue. - src.bufferPosition = endTime; - src.buffersPlayed = i + 1; + var buf = src.bufQueue[bufCursor % src.bufQueue.length]; + // If the buffer contains no data, skip it + if (buf.length === 0) { + skipCount++; + // If we've gone through the whole queue and everything is 0 length, just give up + if (skipCount === src.bufQueue.length) { + break; + } + } else { + var audioSrc = src.context.audioCtx.createBufferSource(); + audioSrc.buffer = buf.audioBuf; + audioSrc.playbackRate.value = src.playbackRate; + if (buf.audioBuf._loopStart || buf.audioBuf._loopEnd) { + audioSrc.loopStart = buf.audioBuf._loopStart; + audioSrc.loopEnd = buf.audioBuf._loopEnd; + } - // Stop / restart the source when we hit the end. - if (src.buffersPlayed >= src.queue.length) { - if (src.loop) { - AL.setSourceState(src, 0x1012 /* AL_PLAYING */); - } else { - AL.setSourceState(src, 0x1014 /* AL_STOPPED */); + var duration = 0.0; + // If the source is a looping static buffer, use native looping for gapless playback + if (src.type === 0x1028 /* AL_STATIC */ && src.looping) { + duration = Number.POSITIVE_INFINITY; + audioSrc.loop = true; + if (buf.audioBuf._loopStart) { + audioSrc.loopStart = buf.audioBuf._loopStart; + } + if (buf.audioBuf._loopEnd) { + audioSrc.loopEnd = buf.audioBuf._loopEnd; } + } else { + duration = (buf.audioBuf.duration - startOffset) / src.playbackRate; } - } - // Process all buffers that'll be played before the next tick. - else if (startOffset < (AL.QUEUE_LOOKAHEAD / 1000) && !entry.src) { - // If the start offset is negative, we need to offset the actual buffer. - var offset = Math.abs(Math.min(startOffset, 0)); - - entry.src = src.context.ctx.createBufferSource(); - entry.src.buffer = entry.buffer; - entry.src.connect(src.gain); - if (src.playbackRate != 1.0) entry.src.playbackRate.value = src.playbackRate; - entry.src.duration = entry.buffer.duration / src.playbackRate; - if (typeof(entry.src.start) !== 'undefined') { - entry.src.start(startTime, offset); - } else if (typeof(entry.src.noteOn) !== 'undefined') { - entry.src.noteOn(startTime); -#if OPENAL_DEBUG - if (offset > 0) { - Runtime.warnOnce('The current browser does not support AudioBufferSourceNode.start(when, offset); method, so cannot play back audio with an offset '+offset+' secs! Audio glitches will occur!'); + + audioSrc._startOffset = startOffset; + audioSrc._duration = duration; + audioSrc._skipCount = skipCount; + skipCount = 0; + + audioSrc.connect(src.gain); + + if (typeof(audioSrc.start) !== 'undefined') { + // Sample the current time as late as possible to mitigate drift + startTime = Math.max(startTime, src.context.audioCtx.currentTime); + audioSrc.start(startTime, startOffset); + } else if (typeof(audioSrc.noteOn) !== 'undefined') { + startTime = Math.max(startTime, src.context.audioCtx.currentTime); + audioSrc.noteOn(startTime); +#if OPENAL_DEBUG + if (offset > 0.0) { + Runtime.warnOnce('The current browser does not support AudioBufferSourceNode.start(when, offset); method, so cannot play back audio with an offset '+startOffset+' secs! Audio glitches will occur!'); } #endif } @@ -103,898 +193,4005 @@ var LibraryOpenAL = { Runtime.warnOnce('Unable to start AudioBufferSourceNode playback! Not supported by the browser?'); } - console.log('updateSource queuing buffer ' + i + ' for source ' + idx + ' at ' + startTime + ' (offset by ' + offset + ')'); + console.log('scheduleSourceAudio() queuing buffer ' + buf.id + ' for source ' + src.id + ' at ' + startTime + ' (offset by ' + startOffset + ')'); #endif + audioSrc._startTime = startTime; + src.audioQueue.push(audioSrc); + + startTime += duration; } - startTime = endTime; + startOffset = 0.0; + bufCursor++; } }, - setSourceState: function setSourceState(src, state) { -#if OPENAL_DEBUG - var idx = AL.srcIdBySrc(src); -#endif + // Advance the state of a source forward to the current time + updateSourceTime: function(src) { + var currentTime = src.context.audioCtx.currentTime; + if (src.state !== 0x1012 /* AL_PLAYING */) { + return currentTime; + } + + // if the start time is unset, determine it based on the current offset. + // This will be the case when a source is resumed after being paused, and + // allows us to pretend that the source actually started playing some time + // in the past such that it would just now have reached the stored offset. + if (!isFinite(src.bufStartTime)) { + src.bufStartTime = currentTime - src.bufOffset / src.playbackRate; + src.bufOffset = 0.0; + } + + var nextStartTime = 0.0; + while (src.audioQueue.length) { + var audioSrc = src.audioQueue[0]; + src.bufsProcessed += audioSrc._skipCount; + nextStartTime = audioSrc._startTime + audioSrc._duration; // n.b. audioSrc._duration already factors in playbackRate, so no divide by src.playbackRate on it. + + if (currentTime < nextStartTime) { + break; + } + + src.audioQueue.shift(); + src.bufStartTime = nextStartTime; + src.bufOffset = 0.0; + src.bufsProcessed++; + } + + if (src.bufsProcessed >= src.bufQueue.length && !src.looping) { + // The source has played its entire queue and is non-looping, so just mark it as stopped. + AL.setSourceState(src, 0x1014 /* AL_STOPPED */); + } else if (src.type === 0x1028 /* AL_STATIC */ && src.looping) { + // If the source is a looping static buffer, determine the buffer offset based on the loop points + var buf = src.bufQueue[0]; + if (buf.length === 0) { + src.bufOffset = 0.0; + } else { + var delta = (currentTime - src.bufStartTime) * src.playbackRate; + var loopStart = buf.audioBuf._loopStart || 0.0; + var loopEnd = buf.audioBuf._loopEnd || buf.audioBuf.duration; + if (loopEnd <= loopStart) { + loopEnd = buf.audioBuf.duration; + } + + if (delta < loopEnd) { + src.bufOffset = delta; + } else { + src.bufOffset = loopStart + (delta - loopStart) % (loopEnd - loopStart); + } + } + } else if (src.audioQueue[0]) { + // The source is still actively playing, so we just need to calculate where we are in the current buffer + // so it can be remembered if the source gets paused. + src.bufOffset = (currentTime - src.audioQueue[0]._startTime) * src.playbackRate; + } else { + // The source hasn't finished yet, but there is no scheduled audio left for it. This can be because + // the source has just been started/resumed, or due to an underrun caused by a long blocking operation. + // We need to determine what state we would be in by this point in time so that when we next schedule + // audio playback, it will be just as if no underrun occurred. + + if (src.type !== 0x1028 /* AL_STATIC */ && src.looping) { + // if the source is a looping buffer queue, let's first calculate the queue duration, so we can + // quickly fast forward past any full loops of the queue and only worry about the remainder. + var srcDuration = AL.sourceDuration(src) / src.playbackRate; + if (srcDuration > 0.0) { + src.bufStartTime += Math.floor((currentTime - src.bufStartTime) / srcDuration) * srcDuration; + } + } + + // Since we've already skipped any full-queue loops if there were any, we just need to find + // out where in the queue the remaining time puts us, which won't require stepping through the + // entire queue more than once. + for (var i = 0; i < src.bufQueue.length; i++) { + if (src.bufsProcessed >= src.bufQueue.length) { + if (src.looping) { + src.bufsProcessed %= src.bufQueue.length; + } else { + AL.setSourceState(src, 0x1014 /* AL_STOPPED */); + break; + } + } + + var buf = src.bufQueue[src.bufsProcessed]; + if (buf.length > 0) { + nextStartTime = src.bufStartTime + buf.audioBuf.duration / src.playbackRate; + + if (currentTime < nextStartTime) { + src.bufOffset = (currentTime - src.bufStartTime) * src.playbackRate; + break; + } + + src.bufStartTime = nextStartTime; + } + + src.bufOffset = 0.0; + src.bufsProcessed++; + } + } + + return currentTime; + }, + + cancelPendingSourceAudio: function(src) { + AL.updateSourceTime(src); + + for (var i = 1; i < src.audioQueue.length; i++) { + var audioSrc = src.audioQueue[i]; + audioSrc.stop(); + } + + if (src.audioQueue.length > 1) { + src.audioQueue.length = 1; + } + }, + + stopSourceAudio: function(src) { + for (var i = 0; i < src.audioQueue.length; i++) { + src.audioQueue[i].stop(); + } + src.audioQueue.length = 0; + }, + + setSourceState: function(src, state) { if (state === 0x1012 /* AL_PLAYING */) { - if (src.state !== 0x1013 /* AL_PAUSED */) { - src.state = 0x1012 /* AL_PLAYING */; - // Reset our position. - src.bufferPosition = AL.currentContext.ctx.currentTime; - src.buffersPlayed = 0; + if (src.state === 0x1012 /* AL_PLAYING */ || src.state == 0x1014 /* AL_STOPPED */) { + src.bufsProcessed = 0; + src.bufOffset = 0.0; #if OPENAL_DEBUG - console.log('setSourceState resetting and playing source ' + idx); + console.log('setSourceState() resetting and playing source ' + src.id); #endif } else { - src.state = 0x1012 /* AL_PLAYING */; - // Use the current offset from src.bufferPosition to resume at the correct point. - src.bufferPosition = AL.currentContext.ctx.currentTime - src.bufferPosition; #if OPENAL_DEBUG - console.log('setSourceState resuming source ' + idx + ' at ' + src.bufferPosition.toFixed(4)); + console.log('setSourceState() playing source ' + src.id + ' at ' + src.bufOffset); #endif } - AL.stopSourceQueue(src); - AL.updateSource(src); + + AL.stopSourceAudio(src); + + src.state = 0x1012 /* AL_PLAYING */; + src.bufStartTime = Number.NEGATIVE_INFINITY; + AL.scheduleSourceAudio(src); } else if (state === 0x1013 /* AL_PAUSED */) { if (src.state === 0x1012 /* AL_PLAYING */) { - src.state = 0x1013 /* AL_PAUSED */; // Store off the current offset to restore with on resume. - src.bufferPosition = AL.currentContext.ctx.currentTime - src.bufferPosition; - AL.stopSourceQueue(src); + AL.updateSourceTime(src); + AL.stopSourceAudio(src); + + src.state = 0x1013 /* AL_PAUSED */; #if OPENAL_DEBUG - console.log('setSourceState pausing source ' + idx + ' at ' + src.bufferPosition.toFixed(4)); + console.log('setSourceState() pausing source ' + src.id + ' at ' + src.bufOffset); #endif } } else if (state === 0x1014 /* AL_STOPPED */) { if (src.state !== 0x1011 /* AL_INITIAL */) { src.state = 0x1014 /* AL_STOPPED */; - src.buffersPlayed = src.queue.length; - AL.stopSourceQueue(src); + src.bufsProcessed = src.bufQueue.length; + src.bufStartTime = Number.NEGATIVE_INFINITY; + src.bufOffset = 0.0; + AL.stopSourceAudio(src); #if OPENAL_DEBUG - console.log('setSourceState stopping source ' + idx); + console.log('setSourceState() stopping source ' + src.id); #endif } - } else if (state == 0x1011 /* AL_INITIAL */) { + } else if (state === 0x1011 /* AL_INITIAL */) { if (src.state !== 0x1011 /* AL_INITIAL */) { src.state = 0x1011 /* AL_INITIAL */; - src.bufferPosition = 0; - src.buffersPlayed = 0; + src.bufsProcessed = 0; + src.bufStartTime = Number.NEGATIVE_INFINITY; + src.bufOffset = 0.0; + AL.stopSourceAudio(src); #if OPENAL_DEBUG - console.log('setSourceState initializing source ' + idx); + console.log('setSourceState() initializing source ' + src.id); #endif } } }, - stopSourceQueue: function stopSourceQueue(src) { - for (var i = 0; i < src.queue.length; i++) { - var entry = src.queue[i]; - if (entry.src) { - entry.src.stop(0); - entry.src = null; + initSourcePanner: function(src) { + if (src.type === 0x1030 /* AL_UNDETERMINED */) { + return; + } + + // Find the first non-zero buffer in the queue to determine the proper format + var templateBuf = AL.buffers[0]; + for (var i = 0; i < src.bufQueue.length; i++) { + if (src.bufQueue[i].id !== 0) { + templateBuf = src.bufQueue[i]; + break; } } - } - }, + // Create a panner if AL_SOURCE_SPATIALIZE_SOFT is set to true, or alternatively if it's set to auto and the source is mono + if (src.spatialize === 1 /* AL_TRUE */ || (src.spatialize === 2 /* AL_AUTO_SOFT */ && templateBuf.channels === 1)) { + if (src.panner) { + return; + } + src.panner = src.context.audioCtx.createPanner(); - alcProcessContext: function(context) {}, - alcSuspendContext: function(context) {}, + AL.updateSourceGlobal(src); + AL.updateSourceSpace(src); - alcMakeContextCurrent: function(context) { - if (context == 0) { - AL.currentContext = null; - return 0; - } else { - AL.currentContext = AL.contexts[context - 1]; - return 1; - } - }, + src.panner.connect(src.context.gain); + src.gain.disconnect(); + src.gain.connect(src.panner); + } else { + if (!src.panner) { + return; + } - alcGetContextsDevice: function(context) { - if (context <= AL.contexts.length && context > 0) { - // Returns the only one audio device - return 1; - } - return 0; - }, + src.panner.disconnect(); + src.gain.disconnect(); + src.gain.connect(src.context.gain); + src.panner = null; + } + }, - alcGetCurrentContext: function() { - for (var i = 0; i < AL.contexts.length; ++i) { - if (AL.contexts[i] == AL.currentContext) { - return i + 1; + updateContextGlobal: function(ctx) { + for (var i in ctx.sources) { + AL.updateSourceGlobal(ctx.sources[i]); } - } - return 0; - }, + }, - alcDestroyContext: function(context) { - // Stop playback, etc - clearInterval(AL.contexts[context - 1].interval); - }, + updateSourceGlobal: function(src) { + var panner = src.panner; + if (!panner) { + return; + } - alcCloseDevice: function(device) { - // Stop playback, etc - }, + panner.refDistance = src.refDistance; + panner.maxDistance = src.maxDistance; + panner.rolloffFactor = src.rolloffFactor; - alcOpenDevice: function(deviceName) { - if (typeof(AudioContext) !== "undefined" || - typeof(webkitAudioContext) !== "undefined") { - return 1; // non-null pointer -- we just simulate one device - } else { - return 0; - } - }, + panner.panningModel = src.context.hrtf ? 'HRTF' : 'equalpower'; - alcCreateContext: function(device, attrList) { - if (device != 1) { - return 0; - } + // Use the source's distance model if AL_SOURCE_DISTANCE_MODEL is enabled + var distanceModel = src.context.sourceDistanceModel ? src.distanceModel : src.context.distanceModel; + switch (distanceModel) { + case 0 /* AL_NONE */: + panner.distanceModel = 'inverse'; + panner.refDistance = 3.40282e38 /* FLT_MAX */; + break; + case 0xd001 /* AL_INVERSE_DISTANCE */: + case 0xd002 /* AL_INVERSE_DISTANCE_CLAMPED */: + panner.distanceModel = 'inverse'; + break; + case 0xd003 /* AL_LINEAR_DISTANCE */: + case 0xd004 /* AL_LINEAR_DISTANCE_CLAMPED */: + panner.distanceModel = 'linear'; + break; + case 0xd005 /* AL_EXPONENT_DISTANCE */: + case 0xd006 /* AL_EXPONENT_DISTANCE_CLAMPED */: + panner.distanceModel = 'exponential'; + break; + } + }, - if (attrList) { + updateListenerSpace: function(ctx) { + var listener = ctx.audioCtx.listener; + if (listener.positionX) { + listener.positionX.value = listener._position[0]; + listener.positionY.value = listener._position[1]; + listener.positionZ.value = listener._position[2]; + } else { #if OPENAL_DEBUG - console.log("The attrList argument of alcCreateContext is not supported yet"); + Runtime.warnOnce('Listener position attributes are not present, falling back to setPosition()'); #endif - return 0; - } + listener.setPosition(listener._position[0], listener._position[1], listener._position[2]); + } + if (listener.forwardX) { + listener.forwardX.value = listener._direction[0]; + listener.forwardY.value = listener._direction[1]; + listener.forwardZ.value = listener._direction[2]; + listener.upX.value = listener._up[0]; + listener.upY.value = listener._up[1]; + listener.upZ.value = listener._up[2]; + } else { +#if OPENAL_DEBUG + Runtime.warnOnce('Listener orientation attributes are not present, falling back to setOrientation()'); +#endif + listener.setOrientation( + listener._direction[0], listener._direction[1], listener._direction[2], + listener._up[0], listener._up[1], listener._up[2]); + } - var ctx; - try { - ctx = new AudioContext(); - } catch (e) { - try { - ctx = new webkitAudioContext(); - } catch (e) {} - } - - if (ctx) { - // Old Web Audio API (e.g. Safari 6.0.5) had an inconsistently named createGainNode function. - if (typeof(ctx.createGain) === 'undefined') ctx.createGain = ctx.createGainNode; - - var gain = ctx.createGain(); - gain.connect(ctx.destination); - // Extend the Web Audio API AudioListener object with a few tracking values of our own. - ctx.listener._position = [0, 0, 0]; - ctx.listener._velocity = [0, 0, 0]; - ctx.listener._orientation = [0, 0, 0, 0, 0, 0]; - var context = { - ctx: ctx, - err: 0, - src: {}, - buf: [], - interval: setInterval(function() { AL.updateSources(context); }, AL.QUEUE_INTERVAL), - gain: gain - }; - AL.contexts.push(context); - return AL.contexts.length; - } else { - return 0; - } - }, + // Update sources that are relative to the listener + for (var i in ctx.sources) { + AL.updateSourceSpace(ctx.sources[i]); + } + }, - alGetError: function() { - if (!AL.currentContext) { - return 0xA004 /* AL_INVALID_OPERATION */; - } else { - // Reset error on get. - var err = AL.currentContext.err; - AL.currentContext.err = 0 /* AL_NO_ERROR */; - return err; - } - }, + updateSourceSpace: function(src) { + if (!src.panner) { + return; + } + var panner = src.panner; - alcGetError: function(device) { - var err = AL.alcErr; - AL.alcErr = 0; - return err; - }, + var posX = src.position[0]; + var posY = src.position[1]; + var posZ = src.position[2]; + var dirX = src.direction[0]; + var dirY = src.direction[1]; + var dirZ = src.direction[2]; - alcGetIntegerv: function(device, param, size, data) { - if (size == 0 || !data) { - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - return; - } + var listener = src.context.audioCtx.listener; + var lPosX = listener._position[0]; + var lPosY = listener._position[1]; + var lPosZ = listener._position[2]; - switch(param) { - case 0x1000 /* ALC_MAJOR_VERSION */: - {{{ makeSetValue('data', '0', '1', 'i32') }}}; - break; - case 0x1001 /* ALC_MINOR_VERSION */: - {{{ makeSetValue('data', '0', '1', 'i32') }}}; - break; - case 0x1002 /* ALC_ATTRIBUTES_SIZE */: - if (!device) { - AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - {{{ makeSetValue('data', '0', '1', 'i32') }}}; - break; - case 0x1003 /* ALC_ALL_ATTRIBUTES */: - if (!device) { - AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - {{{ makeSetValue('data', '0', '0', 'i32') }}}; - break; - case 0x1007 /* ALC_FREQUENCY */: - if (!device) { - AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - if (!AL.currentContext) { - AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; - return 0; - } - {{{ makeSetValue('data', '0', 'AL.currentContext.ctx.sampleRate', 'i32') }}}; - break; - case 0x1010 /* ALC_MONO_SOURCES */: - case 0x1011 /* ALC_STEREO_SOURCES */: - if (!device) { - AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - {{{ makeSetValue('data', '0', '0x7FFFFFFF', 'i32') }}}; - break; - case 0x20003 /* ALC_MAX_AUXILIARY_SENDS */: - if (!device) { - AL.currentContext.err = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - {{{ makeSetValue('data', '0', '1', 'i32') }}}; - default: -#if OPENAL_DEBUG - console.log("alcGetIntegerv with param " + param + " not implemented yet"); -#endif - AL.alcErr = 0xA003 /* ALC_INVALID_ENUM */; - break; - } - }, + // WebAudio does spatialization in world-space coordinates, meaning both the buffer sources and + // the listener position are in the same absolute coordinate system relative to a fixed origin. + // By default, OpenAL works this way as well, but it also provides a "listener relative" mode, where + // a buffer source's coordinate are interpreted not in absolute world space, but as being relative + // to the listener object itself, so as the listener moves the source appears to move with it + // with no update required. Since web audio does not support this mode, we must transform the source + // coordinates from listener-relative space to absolute world space. + // + // We do this via affine transformation matrices applied to the source position and source direction. + // A change-of-basis converts from listener-space displacements to world-space displacements, + // which must be done for both the source position and direction. Lastly, the source position must be + // added to the listener position to get the final source position, since the source position represents + // a displacement from the listener. + if (src.relative) { + // Negate the listener direction since forward is -Z. + var lBackX = -listener._direction[0]; + var lBackY = -listener._direction[1]; + var lBackZ = -listener._direction[2]; + var lUpX = listener._up[0]; + var lUpY = listener._up[1]; + var lUpZ = listener._up[2]; - alDeleteSources: function(count, sources) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alDeleteSources called without a valid context"); -#endif - return; - } - for (var i = 0; i < count; ++i) { - var sourceIdx = {{{ makeGetValue('sources', 'i*4', 'i32') }}}; - delete AL.currentContext.src[sourceIdx]; - } - }, + // Normalize the Back vector + var invMag = 1.0 / Math.sqrt(lBackX * lBackX + lBackY * lBackY + lBackZ * lBackZ); + lBackX *= invMag; + lBackY *= invMag; + lBackZ *= invMag; - alGenSources: function(count, sources) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alGenSources called without a valid context"); -#endif - return; - } - for (var i = 0; i < count; ++i) { - var gain = AL.currentContext.ctx.createGain(); - gain.connect(AL.currentContext.gain); - AL.currentContext.src[AL.newSrcId] = { - context: AL.currentContext, - state: 0x1011 /* AL_INITIAL */, - queue: [], - loop: false, - playbackRate: 1, - _position: [0, 0, 0], - _velocity: [0, 0, 0], - _direction: [0, 0, 0], - get refDistance() { - return this._refDistance || 1; - }, - set refDistance(val) { - this._refDistance = val; - if (this.panner) this.panner.refDistance = val; - }, - get maxDistance() { - return this._maxDistance || 10000; - }, - set maxDistance(val) { - this._maxDistance = val; - if (this.panner) this.panner.maxDistance = val; - }, - get rolloffFactor() { - return this._rolloffFactor || 1; - }, - set rolloffFactor(val) { - this._rolloffFactor = val; - if (this.panner) this.panner.rolloffFactor = val; - }, - get position() { - return this._position; - }, - set position(val) { - this._position[0] = val[0]; - this._position[1] = val[1]; - this._position[2] = val[2]; - if (this.panner) this.panner.setPosition(val[0], val[1], val[2]); - }, - get velocity() { - return this._velocity; - }, - set velocity(val) { - this._velocity[0] = val[0]; - this._velocity[1] = val[1]; - this._velocity[2] = val[2]; - // TODO: The velocity values are not currently used to implement a doppler effect. - // If support for doppler effect is reintroduced, compute the doppler - // speed pitch factor and apply it here. - }, - get direction() { - return this._direction; - }, - set direction(val) { - this._direction[0] = val[0]; - this._direction[1] = val[1]; - this._direction[2] = val[2]; - if (this.panner) this.panner.setOrientation(val[0], val[1], val[2]); - }, - get coneOuterGain() { - return this._coneOuterGain || 0.0; - }, - set coneOuterGain(val) { - this._coneOuterGain = val; - if (this.panner) this.panner.coneOuterGain = val; - }, - get coneInnerAngle() { - return this._coneInnerAngle || 360.0; - }, - set coneInnerAngle(val) { - this._coneInnerAngle = val; - if (this.panner) this.panner.coneInnerAngle = val; - }, - get coneOuterAngle() { - return this._coneOuterAngle || 360.0; - }, - set coneOuterAngle(val) { - this._coneOuterAngle = val; - if (this.panner) this.panner.coneOuterAngle = val; - }, - gain: gain, - panner: null, - buffersPlayed: 0, - bufferPosition: 0 - }; - {{{ makeSetValue('sources', 'i*4', 'AL.newSrcId', 'i32') }}}; - AL.newSrcId++; - } - }, + // ...and the Up vector + var invMag = 1.0 / Math.sqrt(lUpX * lUpX + lUpY * lUpY + lUpZ * lUpZ); + lUpX *= invMag; + lUpY *= invMag; + lUpZ *= invMag; - alIsSource: function(sourceId) { - if (!AL.currentContext) { - return false; - } + // Calculate the Right vector as the cross product of the Up and Back vectors + var lRightX = (lUpY * lBackZ - lUpZ * lBackY); + var lRightY = (lUpZ * lBackX - lUpX * lBackZ); + var lRightZ = (lUpX * lBackY - lUpY * lBackX); - if (!AL.currentContext.src[sourceId]) { - return false; - } else { - return true; - } - }, + // Back and Up might not be exactly perpendicular, so the cross product also needs normalization + var invMag = 1.0 / Math.sqrt(lRightX * lRightX + lRightY * lRightY + lRightZ * lRightZ); + lRightX *= invMag; + lRightY *= invMag; + lRightZ *= invMag; + + // Recompute Up from the now orthonormal Right and Back vectors so we have a fully orthonormal basis + var lUpX = (lBackY * lRightZ - lBackZ * lRightY); + var lUpY = (lBackZ * lRightX - lBackX * lRightZ); + var lUpZ = (lBackX * lRightY - lBackY * lRightX); + + var oldX = dirX; + var oldY = dirY; + var oldZ = dirZ; - alSourcei: function(source, param, value) { - if (!AL.currentContext) { + // Use our 3 vectors to apply a change-of-basis matrix to the source direction + dirX = oldX * lRightX + oldY * lUpX + oldZ * lBackX; + dirY = oldX * lRightY + oldY * lUpY + oldZ * lBackY; + dirZ = oldX * lRightZ + oldY * lUpZ + oldZ * lBackZ; + + var oldX = posX; + var oldY = posY; + var oldZ = posZ; + + // ...and to the source position + posX = oldX * lRightX + oldY * lUpX + oldZ * lBackX; + posY = oldX * lRightY + oldY * lUpY + oldZ * lBackY; + posZ = oldX * lRightZ + oldY * lUpZ + oldZ * lBackZ; + + // The change-of-basis corrects the orientation, but the origin is still the listener. + // Translate the source position by the listener position to finish. + posX += lPosX; + posY += lPosY; + posZ += lPosZ; + } + + if (panner.positionX) { + panner.positionX.value = posX; + panner.positionY.value = posY; + panner.positionZ.value = posZ; + } else { #if OPENAL_DEBUG - console.error("alSourcei called without a valid context"); + Runtime.warnOnce('Panner position attributes are not present, falling back to setPosition()'); #endif - return; - } - var src = AL.currentContext.src[source]; - if (!src) { + panner.setPosition(posX, posY, posZ); + } + if (panner.orientationX) { + panner.orientationX.value = dirX; + panner.orientationY.value = dirY; + panner.orientationZ.value = dirZ; + } else { #if OPENAL_DEBUG - console.error("alSourcei called with an invalid source"); + Runtime.warnOnce('Panner orientation attributes are not present, falling back to setOrientation()'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; - return; - } - switch (param) { - case 0x1001 /* AL_CONE_INNER_ANGLE */: - src.coneInnerAngle = value; - break; - case 0x1002 /* AL_CONE_OUTER_ANGLE */: - src.coneOuterAngle = value; + panner.setOrientation(dirX, dirY, dirZ); + } + + var oldShift = src.dopplerShift; + var velX = src.velocity[0]; + var velY = src.velocity[1]; + var velZ = src.velocity[2]; + var lVelX = listener._velocity[0]; + var lVelY = listener._velocity[1]; + var lVelZ = listener._velocity[2]; + if (posX === lPosX && posY === lPosY && posZ === lPosZ + || velX === lVelX && velY === lVelY && velZ === lVelZ) + { + src.dopplerShift = 1.0; + } else { + // Doppler algorithm from 1.1 spec + var speedOfSound = src.context.speedOfSound; + var dopplerFactor = src.context.dopplerFactor; + + var slX = lPosX - posX; + var slY = lPosY - posY; + var slZ = lPosZ - posZ; + + var magSl = Math.sqrt(slX * slX + slY * slY + slZ * slZ); + var vls = (slX * lVelX + slY * lVelY + slZ * lVelZ) / magSl; + var vss = (slX * velX + slY * velY + slZ * velZ) / magSl; + + vls = Math.min(vls, speedOfSound / dopplerFactor); + vss = Math.min(vss, speedOfSound / dopplerFactor); + + src.dopplerShift = (speedOfSound - dopplerFactor * vls) / (speedOfSound - dopplerFactor * vss); + } + if (src.dopplerShift !== oldShift) { + AL.updateSourceRate(src); + } + }, + + updateSourceRate: function(src) { + if (src.state === 0x1012 /* AL_PLAYING */) { + // clear scheduled buffers + AL.cancelPendingSourceAudio(src); + + var audioSrc = src.audioQueue[0]; + if (!audioSrc) { + return; // It is possible that AL.scheduleContextAudio() has not yet fed the next buffer, if so, skip. + } + + var duration; + if (src.type === 0x1028 /* AL_STATIC */ && src.looping) { + duration = Number.POSITIVE_INFINITY; + } else { + // audioSrc._duration is expressed after factoring in playbackRate, so when changing playback rate, need + // to recompute/rescale the rate to the new playback speed. + duration = (audioSrc.buffer.duration - audioSrc._startOffset) / src.playbackRate; + } + + audioSrc._duration = duration; + audioSrc.playbackRate.value = src.playbackRate; + + // reschedule buffers with the new playbackRate + AL.scheduleSourceAudio(src); + } + }, + + sourceDuration: function(src) { + var length = 0.0; + for (var i = 0; i < src.bufQueue.length; i++) { + var audioBuf = src.bufQueue[i].audioBuf; + length += audioBuf ? audioBuf.duration : 0.0; + } + return length; + }, + + sourceTell: function(src) { + AL.updateSourceTime(src); + + var offset = 0.0; + for (var i = 0; i < src.bufsProcessed; i++) { + offset += src.bufQueue[i].audioBuf.duration; + } + offset += src.bufOffset; + + return offset; + }, + + sourceSeek: function(src, offset) { + var playing = src.state == 0x1012 /* AL_PLAYING */; + if (playing) { + AL.setSourceState(src, 0x1011 /* AL_INITIAL */); + } + + src.bufsProcessed = 0; + while (offset > src.bufQueue[src.bufsProcessed].audioBuf.duration) { + offset -= src.bufQueue[src.bufsProcessed].audiobuf.duration; + src.bufsProcessed++; + } + + src.bufOffset = offset; + if (playing) { + AL.setSourceState(src, 0x1012 /* AL_PLAYING */); + } + }, + + // ------------------------------------------------------ + // -- Accessor Helpers + // ------------------------------------------------------ + + getGlobalParam: function(funcname, param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return null; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + return AL.currentCtx.dopplerFactor; + case 0xC003 /* AL_SPEED_OF_SOUND */: + return AL.currentCtx.speedOfSound; + case 0xD000 /* AL_DISTANCE_MODEL */: + return AL.currentCtx.distanceModel; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return null; + } + }, + + setGlobalParam: function(funcname, param, value) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + if (!Number.isFinite(value) || value < 0.0) { // Strictly negative values are disallowed +#if OPENAL_DEBUG + console.error(funcname + '() value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.currentCtx.dopplerFactor = value; + AL.updateListenerSpace(AL.currentCtx); + break; + case 0xC003 /* AL_SPEED_OF_SOUND */: + if (!Number.isFinite(value) || value <= 0.0) { // Negative or zero values are disallowed +#if OPENAL_DEBUG + console.error(funcname + '() value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.currentCtx.speedOfSound = value; + AL.updateListenerSpace(AL.currentCtx); + break; + case 0xD000 /* AL_DISTANCE_MODEL */: + switch (value) { + case 0 /* AL_NONE */: + case 0xd001 /* AL_INVERSE_DISTANCE */: + case 0xd002 /* AL_INVERSE_DISTANCE_CLAMPED */: + case 0xd003 /* AL_LINEAR_DISTANCE */: + case 0xd004 /* AL_LINEAR_DISTANCE_CLAMPED */: + case 0xd005 /* AL_EXPONENT_DISTANCE */: + case 0xd006 /* AL_EXPONENT_DISTANCE_CLAMPED */: + AL.currentCtx.distanceModel = value; + AL.updateContextGlobal(AL.currentCtx); + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + getListenerParam: function(funcname, param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return null; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + return AL.currentCtx.audioCtx.listener._position; + case 0x1006 /* AL_VELOCITY */: + return AL.currentCtx.audioCtx.listener._velocity; + case 0x100F /* AL_ORIENTATION */: + return AL.currentCtx.audioCtx.listener._direction.concat(AL.currentCtx.audioCtx.listener._up); + case 0x100A /* AL_GAIN */: + return AL.currentCtx.gain.gain.value; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return null; + } + }, + + setListenerParam: function(funcname, param, value) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return; + } + if (value === null) { +#if OPENAL_DEBUG + console.error(funcname + '(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + + var listener = AL.currentCtx.audioCtx.listener; + switch (param) { + case 0x1004 /* AL_POSITION */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2])) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_POSITION value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + listener._position[0] = value[0]; + listener._position[1] = value[1]; + listener._position[2] = value[2]; + AL.updateListenerSpace(AL.currentCtx); + break; + case 0x1006 /* AL_VELOCITY */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2])) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_VELOCITY value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + listener._velocity[0] = value[0]; + listener._velocity[1] = value[1]; + listener._velocity[2] = value[2]; + AL.updateListenerSpace(AL.currentCtx); + break; + case 0x100A /* AL_GAIN */: + if (!Number.isFinite(value) || value < 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_GAIN value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.currentCtx.gain.gain.value = value; + break; + case 0x100F /* AL_ORIENTATION */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2]) + || !Number.isFinite(value[3]) || !Number.isFinite(value[4]) || !Number.isFinite(value[5]) + ) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_ORIENTATION value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + listener._direction[0] = value[0]; + listener._direction[1] = value[1]; + listener._direction[2] = value[2]; + listener._up[0] = value[3]; + listener._up[1] = value[4]; + listener._up[2] = value[5]; + AL.updateListenerSpace(AL.currentCtx); + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + getBufferParam: function(funcname, bufferId, param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return; + } + var buf = AL.buffers[bufferId]; + if (!buf || bufferId === 0) { +#if OPENAL_DEBUG + console.error(funcname + '() called with an invalid buffer'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + + switch (param) { + case 0x2001 /* AL_FREQUENCY */: + return buf.frequency; + case 0x2002 /* AL_BITS */: + return buf.bytesPerSample * 8; + case 0x2003 /* AL_CHANNELS */: + return buf.channels; + case 0x2004 /* AL_SIZE */: + return buf.length * buf.bytesPerSample * buf.channels; + case 0x2015 /* AL_LOOP_POINTS_SOFT */: + if (buf.length === 0) { + return [0, 0]; + } else { + return [ + (buf.audioBuf._loopStart || 0.0) * buf.frequency, + (buf.audioBuf._loopEnd || buf.length) * buf.frequency + ]; + } + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return null; + } + }, + + setBufferParam: function(funcname, bufferId, param, value) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return; + } + var buf = AL.buffers[bufferId]; + if (!buf || bufferId === 0) { +#if OPENAL_DEBUG + console.error(funcname + '() called with an invalid buffer'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + if (value === null) { +#if OPENAL_DEBUG + console.error(funcname + '(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + + switch (param) { + case 0x2004 /* AL_SIZE */: + if (value !== 0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_SIZE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + // Per the spec, setting AL_SIZE to 0 is a legal NOP. + break; + case 0x2015 /* AL_LOOP_POINTS_SOFT */: + if (value[0] < 0 || value[0] > buf.length || value[1] < 0 || value[1] > buf.Length || value[0] >= value[1]) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_LOOP_POINTS_SOFT value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + if (buf.refCount > 0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_LOOP_POINTS_SOFT set on bound buffer'); +#endif + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; + return; + } + + if (buf.audioBuf) { + buf.audioBuf._loopStart = value[0] / buf.frequency; + buf.audioBuf._loopEnd = value[1] / buf.frequency; + } + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + getSourceParam: function(funcname, sourceId, param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return null; + } + var src = AL.currentCtx.sources[sourceId]; + if (!src) { +#if OPENAL_DEBUG + console.error(funcname + '() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return null; + } + + switch (param) { + case 0x202 /* AL_SOURCE_RELATIVE */: + return src.relative; + case 0x1001 /* AL_CONE_INNER_ANGLE */: + return src.coneInnerAngle; + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + return src.coneOuterAngle; + case 0x1003 /* AL_PITCH */: + return src.pitch; + case 0x1004 /* AL_POSITION */: + return src.position; + case 0x1005 /* AL_DIRECTION */: + return src.direction; + case 0x1006 /* AL_VELOCITY */: + return src.velocity; + case 0x1007 /* AL_LOOPING */: + return src.looping; + case 0x1009 /* AL_BUFFER */: + if (src.type === 0x1028 /* AL_STATIC */) { + return src.bufQueue[0].id; + } else { + return 0; + } + case 0x100A /* AL_GAIN */: + return src.gain.gain.value; + case 0x100D /* AL_MIN_GAIN */: + return src.minGain; + case 0x100E /* AL_MAX_GAIN */: + return src.maxGain; + case 0x1010 /* AL_SOURCE_STATE */: + return src.state; + case 0x1015 /* AL_BUFFERS_QUEUED */: + if (src.bufQueue.length === 1 && src.bufQueue[0].id === 0) { + return 0; + } else { + return src.bufQueue.length; + } + case 0x1016 /* AL_BUFFERS_PROCESSED */: + if ((src.bufQueue.length === 1 && src.bufQueue[0].id === 0) || src.looping) { + return 0; + } else { + return src.bufsProcessed; + } + case 0x1020 /* AL_REFERENCE_DISTANCE */: + return src.refDistance; + case 0x1021 /* AL_ROLLOFF_FACTOR */: + return src.rolloffFactor; + case 0x1022 /* AL_CONE_OUTER_GAIN */: + return src.coneOuterGain; + case 0x1023 /* AL_MAX_DISTANCE */: + return src.maxDistance; + case 0x1024 /* AL_SEC_OFFSET */: + return AL.sourceTell(src); + case 0x1025 /* AL_SAMPLE_OFFSET */: + var offset = AL.sourceTell(src); + if (offset > 0.0) { + offset *= src.bufQueue[0].frequency; + } + return offset; + case 0x1026 /* AL_BYTE_OFFSET */: + var offset = AL.sourceTell(src); + if (offset > 0.0) { + offset *= src.bufQueue[0].frequency * src.bufQueue[0].bytesPerSample; + } + return offset; + case 0x1027 /* AL_SOURCE_TYPE */: + return src.type; + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + return src.spatialize; + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + var length = 0; + var bytesPerFrame = 0; + for (var i = 0; i < src.bufQueue.length; i++) { + length += src.bufQueue[i].length; + if (src.bufQueue[i].id !== 0) { + bytesPerFrame = src.bufQueue[i].bytesPerSample * src.bufQueue[i].channels; + } + } + return length * bytesPerFrame; + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + var length = 0; + for (var i = 0; i < src.bufQueue.length; i++) { + length += src.bufQueue[i].length; + } + return length; + case 0x200B /* AL_SEC_LENGTH_SOFT */: + return AL.sourceDuration(src); + case 0xD000 /* AL_DISTANCE_MODEL */: + return src.distanceModel; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return null; + } + }, + + setSourceParam: function(funcname, sourceId, param, value) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error(funcname + '() called without a valid context'); +#endif + return; + } + var src = AL.currentCtx.sources[sourceId]; + if (!src) { +#if OPENAL_DEBUG + console.error('alSourcef() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + if (value === null) { +#if OPENAL_DEBUG + console.error(funcname + '(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + + switch (param) { + case 0x202 /* AL_SOURCE_RELATIVE */: + if (value === 1 /* AL_TRUE */) { + src.relative = true; + AL.updateSourceSpace(src); + } else if (value === 0 /* AL_FALSE */) { + src.relative = false; + AL.updateSourceSpace(src); + } else { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_SOURCE_RELATIVE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + break; + case 0x1001 /* AL_CONE_INNER_ANGLE */: + if (!Number.isFinite(value)) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_CONE_INNER_ANGLE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.coneInnerAngle = value; + if (src.panner) { + src.panner.coneInnerAngle = value % 360.0; + } + break; + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + if (!Number.isFinite(value)) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_CONE_OUTER_ANGLE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.coneOuterAngle = value; + if (src.panner) { + src.panner.coneOuterAngle = value % 360.0; + } + break; + case 0x1003 /* AL_PITCH */: + if (!Number.isFinite(value) || value <= 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_PITCH value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + if (src.pitch === value) { + break; + } + + src.pitch = value; + AL.updateSourceRate(src); + break; + case 0x1004 /* AL_POSITION */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2])) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_POSITION value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.position[0] = value[0]; + src.position[1] = value[1]; + src.position[2] = value[2]; + AL.updateSourceSpace(src); + break; + case 0x1005 /* AL_DIRECTION */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2])) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_DIRECTION value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.direction[0] = value[0]; + src.direction[1] = value[1]; + src.direction[2] = value[2]; + AL.updateSourceSpace(src); + break; + case 0x1006 /* AL_VELOCITY */: + if (!Number.isFinite(value[0]) || !Number.isFinite(value[1]) || !Number.isFinite(value[2])) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_VELOCITY value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.velocity[0] = value[0]; + src.velocity[1] = value[1]; + src.velocity[2] = value[2]; + AL.updateSourceSpace(src); + break; + case 0x1007 /* AL_LOOPING */: + if (value === 1 /* AL_TRUE */) { + src.looping = true; + AL.updateSourceTime(src); + if (src.type === 0x1028 /* AL_STATIC */ && src.audioQueue.length > 0) { + var audioSrc = src.audioQueue[0]; + audioSrc.loop = true; + audioSrc._duration = Number.POSITIVE_INFINITY; + } + } else if (value === 0 /* AL_FALSE */) { + src.looping = false; + var currentTime = AL.updateSourceTime(src); + if (src.type === 0x1028 /* AL_STATIC */ && src.audioQueue.length > 0) { + var audioSrc = src.audioQueue[0]; + audioSrc.loop = false; + audioSrc._duration = src.bufQueue[0].audioBuf.duration / src.playbackRate; + audioSrc._startTime = currentTime - src.bufOffset / src.playbackRate; + } + } else { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_LOOPING value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + break; + case 0x1009 /* AL_BUFFER */: + if (src.state === 0x1012 /* AL_PLAYING */ || src.state === 0x1013 /* AL_PAUSED */) { +#if OPENAL_DEBUG + console.error(funcname + '(AL_BUFFER) called while source is playing or paused'); +#endif + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; + return; + } + + if (value === 0) { + for (var i in src.bufQueue) { + src.bufQueue[i].refCount--; + } + src.bufQueue.length = 1; + src.bufQueue[0] = AL.buffers[0]; + + src.bufsProcessed = 0; + src.type = 0x1030 /* AL_UNDETERMINED */; + } else { + var buf = AL.buffers[value]; + if (!buf) { +#if OPENAL_DEBUG + console.error('alSourcei(AL_BUFFER) called with an invalid buffer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + for (var i in src.bufQueue) { + src.bufQueue[i].refCount--; + } + src.bufQueue.length = 0; + + buf.refCount++; + src.bufQueue = [buf]; + src.bufsProcessed = 0; + src.type = 0x1028 /* AL_STATIC */; + } + + AL.initSourcePanner(src); + AL.scheduleSourceAudio(src); + break; + case 0x100A /* AL_GAIN */: + if (!Number.isFinite(value) || value < 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_GAIN value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + src.gain.gain.value = value; + break; + case 0x100D /* AL_MIN_GAIN */: + if (!Number.isFinite(value) || value < 0.0 || value > Math.min(src.maxGain, 1.0)) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_MIN_GAIN value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } +#if OPENAL_DEBUG + Runtime.warnOnce('AL_MIN_GAIN is not currently supported'); +#endif + src.minGain = value; + break; + case 0x100E /* AL_MAX_GAIN */: + if (!Number.isFinite(value) || value < Math.max(0.0, src.minGain) || value > 1.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_MAX_GAIN value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } +#if OPENAL_DEBUG + Runtime.warnOnce('AL_MAX_GAIN is not currently supported'); +#endif + src.maxGain = value; + break; + case 0x1020 /* AL_REFERENCE_DISTANCE */: + if (!Number.isFinite(value) || value < 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_REFERENCE_DISTANCE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + src.refDistance = value; + if (src.panner) { + src.panner.refDistance = value; + } + break; + case 0x1021 /* AL_ROLLOFF_FACTOR */: + if (!Number.isFinite(value) || value < 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_ROLLOFF_FACTOR value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + src.rolloffFactor = value; + if (src.panner) { + src.panner.rolloffFactor = value; + } + break; + case 0x1022 /* AL_CONE_OUTER_GAIN */: + if (!Number.isFinite(value) || value < 0.0 || value > 1.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_CORE_OUTER_GAIN value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + src.coneOuterGain = value; + if (src.panner) { + src.panner.coneOuterGain = value; + } + break; + case 0x1023 /* AL_MAX_DISTANCE */: + if (!Number.isFinite(value) || value < 0.0) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_MAX_DISTANCE value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + src.maxDistance = value; + if (src.panner) { + src.panner.maxDistance = value; + } + break; + case 0x1024 /* AL_SEC_OFFSET */: + if (value < 0.0 || value > AL.sourceDuration(src)) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_SEC_OFFSET value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.sourceSeek(src, value); + break; + case 0x1025 /* AL_SAMPLE_OFFSET */: + var srcLen = AL.sourceDuration(src); + if (srcLen > 0.0) { + var frequency; + for (var bufId in src.bufQueue) { + if (bufId !== 0) { + frequency = src.bufQueue[bufId].frequency; + break; + } + } + value /= frequency; + } + if (value < 0.0 || value > srcLen) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_SAMPLE_OFFSET value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.sourceSeek(src, value); + break; + case 0x1026 /* AL_BYTE_OFFSET */: + var srcLen = AL.sourceDuration(src); + if (srcLen > 0.0) { + var bytesPerSec; + for (var bufId in src.bufQueue) { + if (bufId !== 0) { + var buf = src.bufQueue[bufId]; + bytesPerSec = buf.frequency * buf.bytesPerSample * buf.channels; + break; + } + } + value /= bytesPerSec; + } + if (value < 0.0 || value > srcLen) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_BYTE_OFFSET value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + AL.sourceSeek(src, value); + break; + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + if (value !== 0 /* AL_FALSE */ && value !== 1 /* AL_TRUE */ && value !== 2 /* AL_AUTO_SOFT */) { +#if OPENAL_DEBUG + console.error(funcname + '() param AL_SOURCE_SPATIALIZE_SOFT value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + src.spatialize = value; + AL.initSourcePanner(src); + break; + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + case 0x200B /* AL_SEC_LENGTH_SOFT */: +#if OPENAL_DEBUG + console.error(funcname + '() param AL_*_LENGTH_SOFT is read only'); +#endif + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; + break; + case 0xD000 /* AL_DISTANCE_MODEL */: + switch (value) { + case 0 /* AL_NONE */: + case 0xd001 /* AL_INVERSE_DISTANCE */: + case 0xd002 /* AL_INVERSE_DISTANCE_CLAMPED */: + case 0xd003 /* AL_LINEAR_DISTANCE */: + case 0xd004 /* AL_LINEAR_DISTANCE_CLAMPED */: + case 0xd005 /* AL_EXPONENT_DISTANCE */: + case 0xd006 /* AL_EXPONENT_DISTANCE_CLAMPED */: + src.distanceModel = value; + if (AL.currentCtx.sourceDistanceModel) { + AL.updateContextGlobal(AL.currentCtx); + } + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param AL_DISTANCE_MODEL value ' + value + ' is out of range'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + break; + default: +#if OPENAL_DEBUG + console.error(funcname + '() param 0x' + param.toString(16) + ' is unknown or not implemented'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + // ------------------------------------------------------- + // -- Capture + // ------------------------------------------------------- + + // A map of 'capture device contexts'. + captures: {}, + + sharedCaptureAudioCtx: null, + + // Helper which: + // - Asserts that deviceId is both non-NULL AND a known device ID; + // - Returns a reference to it, or null if not found. + // - Sets alcErr accordingly. + // Treat NULL and separately because careless + // people might assume that most alcCapture functions + // accept NULL as a 'use the default' device. + requireValidCaptureDevice: function(deviceId, funcname) { + if (deviceId === 0) { +#if OPENAL_DEBUG + console.error(funcname+'() on a NULL device is an error'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return null; + } + var c = AL.captures[deviceId]; + if (!c) { +#if OPENAL_DEBUG + console.error(funcname+'() on an invalid device'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return null; + } + var err = c.mediaStreamError; + if (err) { +#if OPENAL_DEBUG + switch(err.name) { + case 'PermissionDeniedError': + console.error(funcname+'() but the user denied access to the device'); + break; + case 'NotFoundError': + console.error(funcname+'() but no capture device was found'); + break; + default: + console.error(funcname+'() but a MediaStreamError was encountered: ' + err); + break; + } +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return null; + } + return c; + } + + }, + + // *************************************************************************** + // ** ALC API + // *************************************************************************** + + // ------------------------------------------------------- + // -- ALC Capture + // ------------------------------------------------------- + + // bufferSize is actually 'number of sample frames', so was renamed + // bufferFrameCapacity here for clarity. + alcCaptureOpenDevice: function(pDeviceName, requestedSampleRate, format, bufferFrameCapacity) { + + var resolvedDeviceName = AL.CAPTURE_DEVICE_NAME; + + // NULL is a valid device name here (resolves to default); + if (pDeviceName !== 0) { + resolvedDeviceName = Pointer_stringify(pDeviceName); + if (resolvedDeviceName !== AL.CAPTURE_DEVICE_NAME) { +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() with invalid device name \''+resolvedDeviceName+'\''); +#endif + // ALC_OUT_OF_MEMORY + // From the programmer's guide, ALC_OUT_OF_MEMORY's meaning is + // overloaded here, to mean: + // 'The specified device is invalid, or can not capture audio.' + // This may be misleading to API users, but well... + AL.alcErr = 0xA005 /* ALC_OUT_OF_MEMORY */; + return 0; + } + } + + // Otherwise it's probably okay (though useless) for bufferFrameCapacity to be zero. + if (bufferFrameCapacity < 0) { // ALCsizei is signed int +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() with negative bufferSize'); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; + } + + navigator.getUserMedia = navigator.getUserMedia + || navigator.webkitGetUserMedia + || navigator.mozGetUserMedia + || navigator.msGetUserMedia; + var has_getUserMedia = navigator.getUserMedia + || (navigator.mediaDevices + && navigator.mediaDevices.getUserMedia); + + if (!has_getUserMedia) { +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() cannot capture audio, because your browser lacks a `getUserMedia()` implementation'); +#endif + // See previously mentioned rationale for ALC_OUT_OF_MEMORY + AL.alcErr = 0xA005 /* ALC_OUT_OF_MEMORY */; + return 0; + } + + var AudioContext = window.AudioContext || window.webkitAudioContext; + + if (!AL.sharedCaptureAudioCtx) { + try { + AL.sharedCaptureAudioCtx = new AudioContext(); + } catch(e) { +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() could not create the shared capture AudioContext: ' + e); +#endif + // See previously mentioned rationale for ALC_OUT_OF_MEMORY + AL.alcErr = 0xA005 /* ALC_OUT_OF_MEMORY */; + return 0; + } + } + + var outputChannelCount; + + switch (format) { + case 0x10010: /* AL_FORMAT_MONO_FLOAT32 */ + case 0x1101: /* AL_FORMAT_MONO16 */ + case 0x1100: /* AL_FORMAT_MONO8 */ + outputChannelCount = 1; + break; + case 0x10011: /* AL_FORMAT_STEREO_FLOAT32 */ + case 0x1103: /* AL_FORMAT_STEREO16 */ + case 0x1102: /* AL_FORMAT_STEREO8 */ + outputChannelCount = 2; + break; + default: +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() with unsupported format ' + format); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; + } + + function newF32Array(cap) { return new Float32Array(cap);} + function newI16Array(cap) { return new Int16Array(cap); } + function newU8Array(cap) { return new Uint8Array(cap); } + + var requestedSampleType; + var newSampleArray; + + switch (format) { + case 0x10010: /* AL_FORMAT_MONO_FLOAT32 */ + case 0x10011: /* AL_FORMAT_STEREO_FLOAT32 */ + requestedSampleType = 'f32'; + newSampleArray = newF32Array; + break; + case 0x1101: /* AL_FORMAT_MONO16 */ + case 0x1103: /* AL_FORMAT_STEREO16 */ + requestedSampleType = 'i16'; + newSampleArray = newI16Array; + break; + case 0x1100: /* AL_FORMAT_MONO8 */ + case 0x1102: /* AL_FORMAT_STEREO8 */ + requestedSampleType = 'u8'; + newSampleArray = newU8Array; + break; + } + + var buffers = []; + try { + for (var chan=0; chan < outputChannelCount; ++chan) { + buffers[chan] = newSampleArray(bufferFrameCapacity); + } + } catch(e) { +#if OPENAL_DEBUG + console.error('alcCaptureOpenDevice() failed to allocate internal buffers (is bufferSize low enough?): ' + e); +#endif + AL.alcErr = 0xA005 /* ALC_OUT_OF_MEMORY */; + return 0; + } + + + // What we'll place into the `AL.captures` array in the end, + // declared here for closures to access it + var newCapture = { + audioCtx: AL.sharedCaptureAudioCtx, + deviceName: resolvedDeviceName, + requestedSampleRate: requestedSampleRate, + requestedSampleType: requestedSampleType, + outputChannelCount: outputChannelCount, + inputChannelCount: null, // Not known until the getUserMedia() promise resolves + mediaStreamError: null, // Used by other functions to return early and report an error. + mediaStreamSourceNode: null, + // Either one, or none of the below two, is active. + mergerNode: null, + splitterNode: null, + scriptProcessorNode: null, + isCapturing: false, + buffers: buffers, + get bufferFrameCapacity() { + return buffers[0].length; + }, + capturePlayhead: 0, // current write position, in sample frames + capturedFrameCount: 0 + }; + + // Preparing for getUserMedia() + + var onError = function(mediaStreamError) { + newCapture.mediaStreamError = mediaStreamError; +#if OPENAL_DEBUG + console.error('navigator.getUserMedia() errored with: ' + mediaStreamError); +#endif + }; + var onSuccess = function(mediaStream) { + newCapture.mediaStreamSourceNode = newCapture.audioCtx.createMediaStreamSource(mediaStream); + + var inputChannelCount = 1; + switch(newCapture.mediaStreamSourceNode.channelCountMode) { + case 'max': + inputChannelCount = outputChannelCount; + break; + case 'clamped-max': + inputChannelCount = Math.min(outputChannelCount, newCapture.mediaStreamSourceNode.channelCount); + break; + case 'explicit': + inputChannelCount = newCapture.mediaStreamSourceNode.channelCount; + break; + } + + newCapture.inputChannelCount = inputChannelCount; + +#if OPENAL_DEBUG + if (inputChannelCount > 2 || outputChannelCount > 2) { + console.warn('The number of input or output channels is too high, capture might not work as expected!'); + } +#endif + + // Have to pick a size from 256, 512, 1024, 2048, 4096, 8192, 16384. + // One can also set it to zero, which leaves the decision up to the impl. + // An extension could allow specifying this value. + var processorFrameCount = 512; + + newCapture.scriptProcessorNode = newCapture.audioCtx.createScriptProcessor( + processorFrameCount, inputChannelCount, outputChannelCount + ); + + if (inputChannelCount > outputChannelCount) { + newCapture.mergerNode = newCapture.audioCtx.createChannelMerger(inputChannelCount); + newCapture.mediaStreamSourceNode.connect(newCapture.mergerNode); + newCapture.mergerNode.connect(newCapture.scriptProcessorNode); + } else if (inputChannelCount < outputChannelCount) { + newCapture.splitterNode = newCapture.audioCtx.createChannelSplitter(outputChannelCount); + newCapture.mediaStreamSourceNode.connect(newCapture.splitterNode); + newCapture.splitterNode.connect(newCapture.scriptProcessorNode); + } else { + newCapture.mediaStreamSourceNode.connect(newCapture.scriptProcessorNode); + } + + newCapture.scriptProcessorNode.connect(newCapture.audioCtx.destination); + + newCapture.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) { + + if (!newCapture.isCapturing) { + return; + } + + var c = newCapture; + var srcBuf = audioProcessingEvent.inputBuffer; + + // Actually just copy srcBuf's channel data into + // c.buffers, optimizing for each case. + switch (format) { + case 0x10010: /* AL_FORMAT_MONO_FLOAT32 */ + var channel0 = srcBuf.getChannelData(0); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = channel0[i]; + } + break; + case 0x10011: /* AL_FORMAT_STEREO_FLOAT32 */ + var channel0 = srcBuf.getChannelData(0); + var channel1 = srcBuf.getChannelData(1); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = channel0[i]; + c.buffers[1][wi] = channel1[i]; + } + break; + case 0x1101: /* AL_FORMAT_MONO16 */ + var channel0 = srcBuf.getChannelData(0); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = channel0[i] * 32767; + } + break; + case 0x1103: /* AL_FORMAT_STEREO16 */ + var channel0 = srcBuf.getChannelData(0); + var channel1 = srcBuf.getChannelData(1); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = channel0[i] * 32767; + c.buffers[1][wi] = channel1[i] * 32767; + } + break; + case 0x1100: /* AL_FORMAT_MONO8 */ + var channel0 = srcBuf.getChannelData(0); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = (channel0[i] + 1.0) * 127; + } + break; + case 0x1102: /* AL_FORMAT_STEREO8 */ + var channel0 = srcBuf.getChannelData(0); + var channel1 = srcBuf.getChannelData(1); + for (var i = 0 ; i < srcBuf.length; ++i) { + var wi = (c.capturePlayhead + i) % c.bufferFrameCapacity; + c.buffers[0][wi] = (channel0[i] + 1.0) * 127; + c.buffers[1][wi] = (channel1[i] + 1.0) * 127; + } + break; + } + + c.capturePlayhead += srcBuf.length; + c.capturePlayhead %= c.bufferFrameCapacity; + c.capturedFrameCount += srcBuf.length; + c.capturedFrameCount = Math.min(c.capturedFrameCount, c.bufferFrameCapacity); + }; + }; + + // The latest way to call getUserMedia() + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices + .getUserMedia({audio: true}) + .then(onSuccess) + .catch(onError); + } else { // The usual (now deprecated) way + navigator.getUserMedia({audio: true}, onSuccess, onError); + } + + var id = AL.newId(); + AL.captures[id] = newCapture; + return id; + }, + + alcCaptureCloseDevice: function(deviceId) { + var c = AL.requireValidCaptureDevice(deviceId, 'alcCaptureCloseDevice'); + if (!c) return false; + + delete AL.captures[deviceId]; + AL.freeIds.push(deviceId); + + // This clean-up might be unnecessary (paranoid) ? + + // May happen if user hasn't decided to grant or deny input + if (c.mediaStreamSourceNode) c.mediaStreamSourceNode.disconnect(); + if (c.mergerNode) c.mergerNode.disconnect(); + if (c.splitterNode) c.splitterNode.disconnect(); + // May happen if user hasn't decided to grant or deny input + if (c.scriptProcessorNode) c.scriptProcessorNode.disconnect(); + + delete c.buffers; + + c.capturedFrameCount = 0; + c.isCapturing = false; + + return true; + }, + + alcCaptureStart: function(deviceId) { + var c = AL.requireValidCaptureDevice(deviceId, 'alcCaptureStart'); + if (!c) return; + + if (c.isCapturing) { +#if OPENAL_DEBUG + console.warn('Redundant call to alcCaptureStart()'); +#endif + // NOTE: Spec says (emphasis mine): + // The amount of audio samples available after **restarting** a + // stopped capture device is reset to zero. + // So redundant calls to alcCaptureStart() must have no effect. + return; + } + c.isCapturing = true; + c.capturedFrameCount = 0; + c.capturePlayhead = 0; + }, + + alcCaptureStop: function(deviceId) { + var c = AL.requireValidCaptureDevice(deviceId, 'alcCaptureStop'); + if (!c) return; + +#if OPENAL_DEBUG + if (!c.isCapturing) { + console.warn('Redundant call to alcCaptureStop()'); + } +#endif + c.isCapturing = false; + }, + + // The OpenAL spec hints that implementations are allowed to + // 'defer resampling and other conversions' up until this point. + // + // The last parameter is actually 'number of sample frames', so was + // renamed accordingly here + alcCaptureSamples: function(deviceId, pFrames, requestedFrameCount) { + var c = AL.requireValidCaptureDevice(deviceId, 'alcCaptureSamples'); + if (!c) return; + + // ALCsizei is actually 32-bit signed int, so could be negative + // Also, spec says : + // Requesting more sample frames than are currently available is + // an error. + if (requestedFrameCount < 0 + || requestedFrameCount > c.capturedFrameCount) + { + // if OPENAL_DEBUG + console.error('alcCaptureSamples() with invalid bufferSize'); + // endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return; + } + + function setF32Sample(i, sample) { + {{{ makeSetValue('pFrames', '4*i', 'sample', 'float') }}}; + } + function setI16Sample(i, sample) { + {{{ makeSetValue('pFrames', '2*i', 'sample', 'i16') }}}; + } + function setU8Sample(i, sample) { + {{{ makeSetValue('pFrames', 'i', 'sample', 'i8') }}}; + } + + var setSample; + + switch(c.requestedSampleType) { + case 'f32': setSample = setF32Sample; break; + case 'i16': setSample = setI16Sample; break; + case 'u8' : setSample = setU8Sample ; break; + default: +#if OPENAL_DEBUG + console.error('Internal error: Unknown sample type \''+c.requestedSampleType+'\''); +#endif + return; + } + + var dstfreq = c.requestedSampleRate; + var srcfreq = c.audioCtx.sampleRate; + + if (srcfreq == dstfreq) { + for (var i = 0, frame_i = 0; frame_i < requestedFrameCount; ++frame_i) { + for (var chan = 0; chan < c.buffers.length; ++chan, ++i) { + var src_i = (frame_i + c.capturePlayhead) % c.capturedFrameCount; + setSample(i, c.buffers[chan][src_i]); + } + } + } else { + // Perform linear resampling. + + // There is room for improvement - right now we're fine with linear resampling. + // We don't use OfflineAudioContexts for this: See the discussion at + // https://github.com/jpernst/emscripten/issues/2#issuecomment-312729735 + // if you're curious about why. + + function lerp(from, to, progress) { + return (1 - progress) * from + progress * to; + } + + for (var i = 0, frame_i = 0; frame_i < requestedFrameCount; ++frame_i) { + + var t = frame_i / dstfreq; // Most exact time for the current output sample + var src_i = (Math.floor(t*srcfreq) + c.capturePlayhead) % c.capturedFrameCount; + var src_next_i = (src_i+1) % c.capturedFrameCount; + var between = t*srcfreq - src_i; //(t - src_i/srcfreq) / ((src_i+1)/srcfreq - src_i/srcfreq); + + for (var chan = 0; chan < c.buffers.length; ++chan, ++i) { + var cb = c.buffers[chan]; + var sample = lerp(cb[src_i], cb[src_next_i], between); + setSample(i, sample); + } + } + } + + // Spec doesn't say if alcCaptureSamples() must zero the number + // of available captured sample-frames, but not only would it + // be insane not to do, OpenAL-Soft happens to do that as well. + c.capturedFrameCount = 0; + }, + + + // ------------------------------------------------------- + // -- ALC Resources + // ------------------------------------------------------- + + alcOpenDevice: function(pDeviceName) { + if (pDeviceName) { + var name = Pointer_stringify(pDeviceName); + if (name !== AL.DEVICE_NAME) { + return 0; + } + } + + if (typeof(AudioContext) !== 'undefined' || typeof(webkitAudioContext) !== 'undefined') { + var deviceId = AL.newId(); + AL.deviceRefCounts[deviceId] = 0; + return deviceId; + } else { + return 0; + } + }, + + alcCloseDevice: function(deviceId) { + if (!deviceId in AL.deviceRefCounts || AL.deviceRefCounts[deviceId] > 0) { + return 0 /* ALC_FALSE */; + } + + delete AL.deviceRefCounts[deviceId]; + AL.freeIds.push(deviceId); + return 1 /* ALC_TRUE */; + }, + + alcCreateContext: function(deviceId, pAttrList) { + if (!deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.log('alcCreateContext() called with an invalid device'); +#endif + AL.alcErr = 0xA001; /* ALC_INVALID_DEVICE */ + return 0; + } + + var options = null; + var attrs = []; + var hrtf = null; + pAttrList >>= 2; + if (pAttrList) { + var attr = 0; + var val = 0; + while (true) { + attr = HEAP32[pAttrList++]; + attrs.push(attr); + if (attr === 0) { + break; + } + val = HEAP32[pAttrList++]; + attrs.push(val); + + switch (attr) { + case 0x1007 /* ALC_FREQUENCY */: + if (!options) { + options = {}; + } + + options.sampleRate = val; + break; + case 0x1010 /* ALC_MONO_SOURCES */: // fallthrough + case 0x1011 /* ALC_STEREO_SOURCES */: + // Do nothing; these hints are satisfied by default + break + case 0x1992 /* ALC_HRTF_SOFT */: + switch (val) { + case 0 /* ALC_FALSE */: + hrtf = false; + break; + case 1 /* ALC_TRUE */: + hrtf = true; + break; + case 2 /* ALC_DONT_CARE_SOFT */: + break; + default: +#if OPENAL_DEBUG + console.log('Unsupported ALC_HRTF_SOFT mode ' + val); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; + } + break; + case 0x1996 /* ALC_HRTF_ID_SOFT */: + if (val !== 0) { +#if OPENAL_DEBUG + console.log('Invalid ALC_HRTF_ID_SOFT index ' + val); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; + } + break; + default: +#if OPENAL_DEBUG + console.log('Unsupported context attribute 0x' + attr.toString(16)); +#endif + AL.alcErr = 0xA004; /* ALC_INVALID_VALUE */ + return 0; + } + } + } + + var AudioContext = window.AudioContext || window.webkitAudioContext; + var ac = null; + try { + // Only try to pass options if there are any, for compat with browsers that don't support this + if (options) { + ac = new AudioContext(options); + } else { + ac = new AudioContext(); + } + } catch (e) { + if (e.name === 'NotSupportedError') { +#if OPENAL_DEBUG + console.log('Invalid or unsupported options'); +#endif + AL.alcErr = 0xA004; /* ALC_INVALID_VALUE */ + } else { + AL.alcErr = 0xA001; /* ALC_INVALID_DEVICE */ + } + + return 0; + } + + // Old Web Audio API (e.g. Safari 6.0.5) had an inconsistently named createGainNode function. + if (typeof(ac.createGain) === 'undefined') { + ac.createGain = ac.createGainNode; + } + + var gain = ac.createGain(); + gain.connect(ac.destination); + // Extend the Web Audio API AudioListener object with a few tracking values of our own. + ac.listener._position = [0.0, 0.0, 0.0]; + ac.listener._velocity = [0.0, 0.0, 0.0]; + ac.listener._direction = [0.0, 0.0, 0.0]; + ac.listener._up = [0.0, 0.0, 0.0]; + var ctx = { + deviceId: deviceId, + id: AL.newId(), + attrs: attrs, + audioCtx: ac, + sources: [], + interval: setInterval(function() { AL.scheduleContextAudio(ctx); }, AL.QUEUE_INTERVAL), + gain: gain, + distanceModel: 0xd002 /* AL_INVERSE_DISTANCE_CLAMPED */, + speedOfSound: 343.3, + dopplerFactor: 1.0, + sourceDistanceModel: false, + hrtf: hrtf || false, + + _err: 0, + get err() { + return this._err; + }, + set err(val) { + // Errors should not be overwritten by later errors until they are cleared by a query. + if (this._err === 0 /* AL_NO_ERROR */ || val === 0 /* AL_NO_ERROR */) { + this._err = val; + } + } + }; + AL.deviceRefCounts[deviceId]++; + AL.contexts[ctx.id] = ctx; + + if (hrtf !== null) { + // Apply hrtf attrib to all contexts for this device + for (var ctxId in AL.contexts) { + var c = AL.contexts[ctxId]; + if (c.deviceId === deviceId) { + c.hrtf = hrtf; + AL.updateContextGlobal(c); + } + } + } + + return ctx.id; + }, + + alcDestroyContext: function(contextId) { + var ctx = AL.contexts[contextId]; + if (AL.currentCtx === ctx) { +#if OPENAL_DEBUG + console.log('alcDestroyContext() called with an invalid context'); +#endif + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + // Stop playback, etc + if (AL.contexts[contextId].interval) { + clearInterval(AL.contexts[contextId].interval); + } + AL.deviceRefCounts[ctx.deviceId]--; + delete AL.contexts[contextId]; + AL.freeIds.push(contextId); + }, + + // ------------------------------------------------------- + // -- ALC State + // ------------------------------------------------------- + + alcGetError: function(deviceId) { + var err = AL.alcErr; + AL.alcErr = 0 /* ALC_NO_ERROR */; + return err; + }, + + alcGetCurrentContext: function() { + if (AL.currentCtx !== null) { + return AL.currentCtx.id; + } else { + return 0; + } + }, + + alcMakeContextCurrent: function(contextId) { + if (contextId === 0) { + AL.currentCtx = null; + return 0; + } else { + AL.currentCtx = AL.contexts[contextId]; + return 1; + } + }, + + alcGetContextsDevice: function(contextId) { + if (contextId in AL.contexts) { + return AL.contexts[contextId].deviceId; + } else { + return 0; + } + }, + + // The spec is vague about what these are actually supposed to do, and NOP is a reasonable implementation + alcProcessContext: function(contextId) {}, + alcSuspendContext: function(contextId) {}, + + alcIsExtensionPresent: function(deviceId, pExtName) { + name = Pointer_stringify(pExtName); + + return AL.ALC_EXTENSIONS[name] ? 1 : 0; + }, + + alcGetProcAddress__deps: ['emscripten_GetAlcProcAddress'], + alcGetProcAddress: function(deviceId, pProcName) { + if (!pProcName) { +#if OPENAL_DEBUG + console.error('alcGetProcAddress() called with null name pointer'); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; /* ALC_NONE */ + } + return _emscripten_GetAlcProcAddress(pProcName); + }, + + alcGetEnumValue: function(deviceId, pEnumName) { + // Spec says : + // Using a NULL handle is legal, but only the + // tokens defined by the AL core are guaranteed. + if (deviceId !== 0 && !deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.error('alcGetEnumValue() called with an invalid device'); +#endif + // ALC_INVALID_DEVICE is not listed as a possible error state for + // this function, sadly. + return 0 /* ALC_NONE */; + } else if (!pEnumName) { + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; /* ALC_NONE */ + } + name = Pointer_stringify(pEnumName); + // See alGetEnumValue(), but basically behave the same as OpenAL-Soft + switch(name) { + case 'ALC_NO_ERROR': return 0; + case 'ALC_INVALID_DEVICE': return 0xA001; + case 'ALC_INVALID_CONTEXT': return 0xA002; + case 'ALC_INVALID_ENUM': return 0xA003; + case 'ALC_INVALID_VALUE': return 0xA004; + case 'ALC_OUT_OF_MEMORY': return 0xA005; + case 'ALC_MAJOR_VERSION': return 0x1000; + case 'ALC_MINOR_VERSION': return 0x1001; + case 'ALC_ATTRIBUTES_SIZE': return 0x1002; + case 'ALC_ALL_ATTRIBUTES': return 0x1003; + case 'ALC_DEFAULT_DEVICE_SPECIFIER': return 0x1004; + case 'ALC_DEVICE_SPECIFIER': return 0x1005; + case 'ALC_EXTENSIONS': return 0x1006; + case 'ALC_FREQUENCY': return 0x1007; + case 'ALC_REFRESH': return 0x1008; + case 'ALC_SYNC': return 0x1009; + case 'ALC_MONO_SOURCES': return 0x1010; + case 'ALC_STEREO_SOURCES': return 0x1011; + case 'ALC_CAPTURE_DEVICE_SPECIFIER': return 0x310; + case 'ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER': return 0x311; + case 'ALC_CAPTURE_SAMPLES': return 0x312; + + /* Extensions */ + case 'ALC_HRTF_SOFT': return 0x1992; + case 'ALC_HRTF_ID_SOFT': return 0x1996; + case 'ALC_DONT_CARE_SOFT': return 0x0002; + case 'ALC_HRTF_STATUS_SOFT': return 0x1993; + case 'ALC_NUM_HRTF_SPECIFIERS_SOFT': return 0x1994; + case 'ALC_HRTF_SPECIFIER_SOFT': return 0x1995; + case 'ALC_HRTF_DISABLED_SOFT': return 0x0000; + case 'ALC_HRTF_ENABLED_SOFT': return 0x0001; + case 'ALC_HRTF_DENIED_SOFT': return 0x0002; + case 'ALC_HRTF_REQUIRED_SOFT': return 0x0003; + case 'ALC_HRTF_HEADPHONES_DETECTED_SOFT': return 0x0004; + case 'ALC_HRTF_UNSUPPORTED_FORMAT_SOFT': return 0x0005; + + default: +#if OPENAL_DEBUG + console.error('No value for `' + pEnumName + '` is known by alcGetEnumValue()'); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0 /* AL_NONE */; + } + }, + + alcGetString: function(deviceId, param) { + if (AL.alcStringCache[param]) { + return AL.alcStringCache[param]; + } + + var ret; + switch (param) { + case 0 /* ALC_NO_ERROR */: + ret = 'No Error'; + break; + case 0xA001 /* ALC_INVALID_DEVICE */: + ret = 'Invalid Device'; + break; + case 0xA002 /* ALC_INVALID_CONTEXT */: + ret = 'Invalid Context'; + break; + case 0xA003 /* ALC_INVALID_ENUM */: + ret = 'Invalid Enum'; + break; + case 0xA004 /* ALC_INVALID_VALUE */: + ret = 'Invalid Value'; + break; + case 0xA005 /* ALC_OUT_OF_MEMORY */: + ret = 'Out of Memory'; + break; + case 0x1004 /* ALC_DEFAULT_DEVICE_SPECIFIER */: + if (typeof(AudioContext) !== 'undefined' || + typeof(webkitAudioContext) !== 'undefined') { + ret = AL.DEVICE_NAME; + } else { + return 0; + } + break; + case 0x1005 /* ALC_DEVICE_SPECIFIER */: + if (typeof(AudioContext) !== 'undefined' || + typeof(webkitAudioContext) !== 'undefined') { + ret = AL.DEVICE_NAME.concat('\0'); + } else { + ret = '\0'; + } + break; + case 0x311 /* ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER */: + ret = AL.CAPTURE_DEVICE_NAME; + break; + case 0x310 /* ALC_CAPTURE_DEVICE_SPECIFIER */: + if (deviceId === 0) + ret = AL.CAPTURE_DEVICE_NAME.concat('\0'); + else { + var c = AL.requireValidCaptureDevice(deviceId, 'alcGetString'); + if (!c) { + return 0; + } + ret = c.deviceName; + } + break; + case 0x1006 /* ALC_EXTENSIONS */: + if (!deviceId) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return 0; + } + + ret = ''; + for (ext in AL.ALC_EXTENSIONS) { + ret = ret.concat(ext); + ret = ret.concat(' '); + } + ret = ret.trim(); + break; + default: + AL.alcErr = 0xA003 /* ALC_INVALID_ENUM */; + return 0; + } + + ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL); + AL.alcStringCache[param] = ret; + return ret; + }, + + alcGetIntegerv: function(deviceId, param, size, pValues) { + if (size === 0 || !pValues) { + // Ignore the query, per the spec + return; + } + + switch(param) { + case 0x1000 /* ALC_MAJOR_VERSION */: + {{{ makeSetValue('pValues', '0', '1', 'i32') }}}; + break; + case 0x1001 /* ALC_MINOR_VERSION */: + {{{ makeSetValue('pValues', '0', '1', 'i32') }}}; + break; + case 0x1002 /* ALC_ATTRIBUTES_SIZE */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + if (!AL.currentCtx) { + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + {{{ makeSetValue('pValues', '0', 'AL.currentCtx.attrs.length', 'i32') }}}; + break; + case 0x1003 /* ALC_ALL_ATTRIBUTES */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + if (!AL.currentCtx) { + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + for (var i = 0; i < AL.currentCtx.attrs.length; i++) { + {{{ makeSetValue('pValues', 'i*4', 'AL.currentCtx.attrs[i]', 'i32') }}}; + } + break; + case 0x1007 /* ALC_FREQUENCY */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + if (!AL.currentCtx) { + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + {{{ makeSetValue('pValues', '0', 'AL.currentCtx.audioCtx.sampleRate', 'i32') }}}; + break; + case 0x1010 /* ALC_MONO_SOURCES */: + case 0x1011 /* ALC_STEREO_SOURCES */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + if (!AL.currentCtx) { + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + {{{ makeSetValue('pValues', '0', '0x7FFFFFFF', 'i32') }}}; + break; + case 0x1992 /* ALC_HRTF_SOFT */: + case 0x1993 /* ALC_HRTF_STATUS_SOFT */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + + var hrtfStatus = 0 /* ALC_HRTF_DISABLED_SOFT */; + for (var ctxId in AL.contexts) { + var ctx = AL.contexts[ctxId]; + if (ctx.deviceId === deviceId) { + hrtfStatus = ctx.hrtf ? 1 /* ALC_HRTF_ENABLED_SOFT */ : 0 /* ALC_HRTF_DISABLED_SOFT */; + } + } + {{{ makeSetValue('pValues', '0', 'hrtfStatus', 'i32') }}}; + break; + case 0x1994 /* ALC_NUM_HRTF_SPECIFIERS_SOFT */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + {{{ makeSetValue('pValues', '0', '1', 'i32') }}}; + break; + case 0x20003 /* ALC_MAX_AUXILIARY_SENDS */: + if (!deviceId in AL.deviceRefCounts) { + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + if (!AL.currentCtx) { + AL.alcErr = 0xA002 /* ALC_INVALID_CONTEXT */; + return; + } + + {{{ makeSetValue('pValues', '0', '1', 'i32') }}}; + case 0x312 /* ALC_CAPTURE_SAMPLES */: + var c = AL.requireValidCaptureDevice(deviceId, 'alcGetIntegerv'); + if (!c) { + return; + } + var n = c.capturedFrameCount; + var dstfreq = c.requestedSampleRate; + var srcfreq = c.audioCtx.sampleRate; + var nsamples = Math.floor(n * (dstfreq/srcfreq)); + {{{ makeSetValue('pValues', '0', 'nsamples', 'i32') }}}; + break; + default: +#if OPENAL_DEBUG + console.log('alcGetIntegerv() with param 0x' + param.toString(16) + ' not implemented yet'); +#endif + AL.alcErr = 0xA003 /* ALC_INVALID_ENUM */; + return; + } + }, + + emscripten_alcDevicePauseSOFT: function(deviceId) { + if (!deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.log('alcDevicePauseSOFT() called with an invalid device'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + + if (AL.paused) { + return; + } + AL.paused = true; + + for (ctxId in AL.contexts) { + var ctx = AL.contexts[ctxId]; + if (ctx.deviceId !== deviceId) { + continue; + } + + ctx.audioCtx.suspend(); + clearInterval(ctx.interval); + ctx.interval = null; + } + }, + + emscripten_alcDeviceResumeSOFT: function(deviceId) { + if (!deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.log('alcDeviceResumeSOFT() called with an invalid device'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return; + } + + if (!AL.paused) { + return; + } + AL.paused = false; + + for (ctxId in AL.contexts) { + var ctx = AL.contexts[ctxId]; + if (ctx.deviceId !== deviceId) { + continue; + } + + ctx.interval = setInterval(function() { AL.scheduleContextAudio(ctx); }, AL.QUEUE_INTERVAL); + ctx.audioCtx.resume(); + } + }, + + emscripten_alcGetStringiSOFT: function(deviceId, param, index) { + if (!deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.log('alcGetStringiSOFT() called with an invalid device'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return 0; + } + + if (AL.alcStringCache[param]) { + return AL.alcStringCache[param]; + } + + var ret; + switch (param) { + case 0x1995 /* ALC_HRTF_SPECIFIER_SOFT */: + if (index === 0) { + ret = 'Web Audio HRTF'; + } else { +#if OPENAL_DEBUG + console.log('alcGetStringiSOFT() with param ALC_HRTF_SPECIFIER_SOFT index ' + index + ' is out of range'); +#endif + AL.alcErr = 0xA004 /* ALC_INVALID_VALUE */; + return 0; + } + default: + if (index === 0) { + return alcGetString(deviceId, param); + } else { +#if OPENAL_DEBUG + console.log('alcGetStringiSOFT() with param 0x' + param.toString(16) + ' not implemented yet'); +#endif + AL.alcErr = 0xA003 /* ALC_INVALID_ENUM */; + return 0; + } + } + + ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL); + AL.alcStringCache[param] = ret; + return ret; + }, + + emscripten_alcResetDeviceSOFT: function(deviceId, pAttrList) { + if (!deviceId in AL.deviceRefCounts) { +#if OPENAL_DEBUG + console.log('alcResetDeviceSOFT() called with an invalid device'); +#endif + AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; + return 0 /* ALC_FALSE */; + } + + var hrtf = null; + pAttrList >>= 2; + if (pAttrList) { + var attr = 0; + var val = 0; + while (true) { + attr = HEAP32[pAttrList++]; + if (attr === 0) { + break; + } + val = HEAP32[pAttrList++]; + + switch (attr) { + case 0x1992 /* ALC_HRTF_SOFT */: + if (val === 1 /* ALC_TRUE */) { + hrtf = true; + } else if (val === 0 /* ALC_FALSE */) { + hrtf = false; + } + break; + } + } + } + + if (hrtf !== null) { + // Apply hrtf attrib to all contexts for this device + for (var ctxId in AL.contexts) { + var ctx = AL.contexts[ctxId]; + if (ctx.deviceId === deviceId) { + ctx.hrtf = hrtf; + AL.updateContextGlobal(ctx); + } + } + } + + return 1 /* ALC_TRUE */; + }, + + // *************************************************************************** + // ** AL API + // *************************************************************************** + + // ------------------------------------------------------- + // -- AL Resources + // ------------------------------------------------------- + + alGenBuffers: function(count, pBufferIds) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alGenBuffers() called without a valid context'); +#endif + return; + } + + for (var i = 0; i < count; ++i) { + var buf = { + deviceId: AL.currentCtx.deviceId, + id: AL.newId(), + refCount: 0, + audioBuf: null, + frequency: 0, + bytesPerSample: 2, + channels: 1, + length: 0, + }; + AL.deviceRefCounts[buf.deviceId]++; + AL.buffers[buf.id] = buf; + {{{ makeSetValue('pBufferIds', 'i*4', 'buf.id', 'i32') }}}; + } + }, + + alDeleteBuffers: function(count, pBufferIds) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alDeleteBuffers() called without a valid context'); +#endif + return; + } + + for (var i = 0; i < count; ++i) { + var bufId = {{{ makeGetValue('pBufferIds', 'i*4', 'i32') }}}; + /// Deleting the zero buffer is a legal NOP, so ignore it + if (bufId === 0) { + continue; + } + + // Make sure the buffer index is valid. + if (!AL.buffers[bufId]) { +#if OPENAL_DEBUG + console.error('alDeleteBuffers() called with an invalid buffer'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + + // Make sure the buffer is no longer in use. + if (AL.buffers[bufId].refCount) { +#if OPENAL_DEBUG + console.error('alDeleteBuffers() called with a used buffer'); +#endif + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; + return; + } + } + + for (var i = 0; i < count; ++i) { + var bufId = {{{ makeGetValue('pBufferIds', 'i*4', 'i32') }}}; + if (bufId === 0) { + continue; + } + + AL.deviceRefCounts[AL.buffers[bufId].deviceId]--; + delete AL.buffers[bufId]; + AL.freeIds.push(bufId); + } + }, + + alGenSources: function(count, pSourceIds) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alGenSources() called without a valid context'); +#endif + return; + } + for (var i = 0; i < count; ++i) { + var gain = AL.currentCtx.audioCtx.createGain(); + gain.connect(AL.currentCtx.gain); + var src = { + context: AL.currentCtx, + id: AL.newId(), + type: 0x1030 /* AL_UNDETERMINED */, + state: 0x1011 /* AL_INITIAL */, + bufQueue: [AL.buffers[0]], + audioQueue: [], + looping: false, + pitch: 1.0, + dopplerShift: 1.0, + gain: gain, + minGain: 0.0, + maxGain: 1.0, + panner: null, + bufsProcessed: 0, + bufStartTime: Number.NEGATIVE_INFINITY, + bufOffset: 0.0, + relative: false, + refDistance: 1.0, + maxDistance: 3.40282e38 /* FLT_MAX */, + rolloffFactor: 1.0, + position: [0.0, 0.0, 0.0], + velocity: [0.0, 0.0, 0.0], + direction: [0.0, 0.0, 0.0], + coneOuterGain: 0.0, + coneInnerAngle: 360.0, + coneOuterAngle: 360.0, + distanceModel: 0xd002 /* AL_INVERSE_DISTANCE_CLAMPED */, + spatialize: 2 /* AL_AUTO_SOFT */, + + get playbackRate() { + return this.pitch * this.dopplerShift; + } + }; + AL.currentCtx.sources[src.id] = src; + {{{ makeSetValue('pSourceIds', 'i*4', 'src.id', 'i32') }}}; + } + }, + + alDeleteSources__deps: ['alSourcei'], + alDeleteSources: function(count, pSourceIds) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alDeleteSources() called without a valid context'); +#endif + return; + } + + for (var i = 0; i < count; ++i) { + var srcId = {{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}; + if (!AL.currentCtx.sources[srcId]) { +#if OPENAL_DEBUG + console.error('alDeleteSources() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + } + + for (var i = 0; i < count; ++i) { + var srcId = {{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}; + AL.setSourceState(AL.currentCtx.sources[srcId], 0x1014 /* AL_STOPPED */); + _alSourcei(srcId, 0x1009 /* AL_BUFFER */, 0); + delete AL.currentCtx.sources[srcId]; + AL.freeIds.push(srcId); + } + }, + + // ------------------------------------------------------- + // --- AL Context State + // ------------------------------------------------------- + + alGetError: function() { + if (!AL.currentCtx) { + return 0xA004 /* AL_INVALID_OPERATION */; + } else { + // Reset error on get. + var err = AL.currentCtx.err; + AL.currentCtx.err = 0 /* AL_NO_ERROR */; + return err; + } + }, + + alIsExtensionPresent: function(pExtName) { + name = Pointer_stringify(pExtName); + + return AL.AL_EXTENSIONS[name] ? 1 : 0; + }, + + alGetProcAddress__deps: ['emscripten_GetAlProcAddress'], + alGetProcAddress: function(pProcName) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alGetProcAddress() called without a valid context'); +#endif + return; + } + if (!pProcName) { +#if OPENAL_DEBUG + console.error('alcGetProcAddress() called with null name pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return 0; /* ALC_NONE */ + } + return _emscripten_GetAlProcAddress(pProcName); + }, + + alGetEnumValue: function(pEnumName) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alGetEnumValue() called without a valid context'); +#endif + return 0; + } + + if (!pEnumName) { +#if OPENAL_DEBUG + console.error('alGetEnumValue() called with null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return 0 /* AL_NONE */; + } + name = Pointer_stringify(pEnumName); + + switch(name) { + // Spec doesn't clearly state that alGetEnumValue() is required to + // support _only_ extension tokens. + // We should probably follow OpenAL-Soft's example and support all + // of the names we know. + // See http://repo.or.cz/openal-soft.git/blob/HEAD:/Alc/ALc.c + case 'AL_BITS': return 0x2002; + case 'AL_BUFFER': return 0x1009; + case 'AL_BUFFERS_PROCESSED': return 0x1016; + case 'AL_BUFFERS_QUEUED': return 0x1015; + case 'AL_BYTE_OFFSET': return 0x1026; + case 'AL_CHANNELS': return 0x2003; + case 'AL_CONE_INNER_ANGLE': return 0x1001; + case 'AL_CONE_OUTER_ANGLE': return 0x1002; + case 'AL_CONE_OUTER_GAIN': return 0x1022; + case 'AL_DIRECTION': return 0x1005; + case 'AL_DISTANCE_MODEL': return 0xD000; + case 'AL_DOPPLER_FACTOR': return 0xC000; + case 'AL_DOPPLER_VELOCITY': return 0xC001; + case 'AL_EXPONENT_DISTANCE': return 0xD005; + case 'AL_EXPONENT_DISTANCE_CLAMPED': return 0xD006; + case 'AL_EXTENSIONS': return 0xB004; + case 'AL_FORMAT_MONO16': return 0x1101; + case 'AL_FORMAT_MONO8': return 0x1100; + case 'AL_FORMAT_STEREO16': return 0x1103; + case 'AL_FORMAT_STEREO8': return 0x1102; + case 'AL_FREQUENCY': return 0x2001; + case 'AL_GAIN': return 0x100A; + case 'AL_INITIAL': return 0x1011; + case 'AL_INVALID': return -1; + case 'AL_ILLEGAL_ENUM': // fallthrough + case 'AL_INVALID_ENUM': return 0xA002; + case 'AL_INVALID_NAME': return 0xA001; + case 'AL_ILLEGAL_COMMAND': // fallthrough + case 'AL_INVALID_OPERATION': return 0xA004; + case 'AL_INVALID_VALUE': return 0xA003; + case 'AL_INVERSE_DISTANCE': return 0xD001; + case 'AL_INVERSE_DISTANCE_CLAMPED': return 0xD002; + case 'AL_LINEAR_DISTANCE': return 0xD003; + case 'AL_LINEAR_DISTANCE_CLAMPED': return 0xD004; + case 'AL_LOOPING': return 0x1007; + case 'AL_MAX_DISTANCE': return 0x1023; + case 'AL_MAX_GAIN': return 0x100E; + case 'AL_MIN_GAIN': return 0x100D; + case 'AL_NONE': return 0; + case 'AL_NO_ERROR': return 0; + case 'AL_ORIENTATION': return 0x100F; + case 'AL_OUT_OF_MEMORY': return 0xA005; + case 'AL_PAUSED': return 0x1013; + case 'AL_PENDING': return 0x2011; + case 'AL_PITCH': return 0x1003; + case 'AL_PLAYING': return 0x1012; + case 'AL_POSITION': return 0x1004; + case 'AL_PROCESSED': return 0x2012; + case 'AL_REFERENCE_DISTANCE': return 0x1020; + case 'AL_RENDERER': return 0xB003; + case 'AL_ROLLOFF_FACTOR': return 0x1021; + case 'AL_SAMPLE_OFFSET': return 0x1025; + case 'AL_SEC_OFFSET': return 0x1024; + case 'AL_SIZE': return 0x2004; + case 'AL_SOURCE_RELATIVE': return 0x202; + case 'AL_SOURCE_STATE': return 0x1010; + case 'AL_SOURCE_TYPE': return 0x1027; + case 'AL_SPEED_OF_SOUND': return 0xC003; + case 'AL_STATIC': return 0x1028; + case 'AL_STOPPED': return 0x1014; + case 'AL_STREAMING': return 0x1029; + case 'AL_UNDETERMINED': return 0x1030; + case 'AL_UNUSED': return 0x2010; + case 'AL_VELOCITY': return 0x1006; + case 'AL_VENDOR': return 0xB001; + case 'AL_VERSION': return 0xB002; + + /* Extensions */ + case 'AL_AUTO_SOFT': return 0x0002; + case 'AL_SOURCE_DISTANCE_MODEL': return 0x200; + case 'AL_SOURCE_SPATIALIZE_SOFT': return 0x1214; + case 'AL_LOOP_POINTS_SOFT': return 0x2015; + case 'AL_BYTE_LENGTH_SOFT': return 0x2009; + case 'AL_SAMPLE_LENGTH_SOFT': return 0x200A; + case 'AL_SEC_LENGTH_SOFT': return 0x200B; + case 'AL_FORMAT_MONO_FLOAT32': return 0x10010; + case 'AL_FORMAT_STEREO_FLOAT32': return 0x10011; + + default: +#if OPENAL_DEBUG + console.error('No value for `' + name + '` is known by alGetEnumValue()'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return 0; + } + }, + + alGetString: function(param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alGetString() called without a valid context'); +#endif + return 0; + } + + if (AL.stringCache[param]) { + return AL.stringCache[param]; + } + + var ret; + switch (param) { + case 0 /* AL_NO_ERROR */: + ret = 'No Error'; + break; + case 0xA001 /* AL_INVALID_NAME */: + ret = 'Invalid Name'; + break; + case 0xA002 /* AL_INVALID_ENUM */: + ret = 'Invalid Enum'; + break; + case 0xA003 /* AL_INVALID_VALUE */: + ret = 'Invalid Value'; + break; + case 0xA004 /* AL_INVALID_OPERATION */: + ret = 'Invalid Operation'; + break; + case 0xA005 /* AL_OUT_OF_MEMORY */: + ret = 'Out of Memory'; + break; + case 0xB001 /* AL_VENDOR */: + ret = 'Emscripten'; break; - case 0x1007 /* AL_LOOPING */: - src.loop = (value === 1 /* AL_TRUE */); + case 0xB002 /* AL_VERSION */: + ret = '1.1'; + break; + case 0xB003 /* AL_RENDERER */: + ret = 'WebAudio'; + break; + case 0xB004 /* AL_EXTENSIONS */: + ret = ''; + for (ext in AL.AL_EXTENSIONS) { + ret = ret.concat(ext); + ret = ret.concat(' '); + } + ret = ret.trim(); + break; + default: + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return 0; + } + + ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL); + AL.stringCache[param] = ret; + return ret; + }, + + alEnable: function(param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alEnable() called without a valid context'); +#endif + return; + } + switch (param) { + case 'AL_SOURCE_DISTANCE_MODEL': + AL.currentCtx.sourceDistanceModel = true; + AL.updateContextGlobal(AL.currentCtx); + break; + default: +#if OPENAL_DEBUG + console.error('alEnable() with param 0x' + param.toString(16) + ' not implemented yet'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alDisable: function(param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alDisable() called without a valid context'); +#endif + return; + } + switch (pname) { + case 'AL_SOURCE_DISTANCE_MODEL': + AL.currentCtx.sourceDistanceModel = false; + AL.updateContextGlobal(AL.currentCtx); + break; + default: +#if OPENAL_DEBUG + console.error('alDisable() with param 0x' + param.toString(16) + ' not implemented yet'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alIsEnabled: function(param) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alIsEnabled() called without a valid context'); +#endif + return 0; + } + switch (pname) { + case 'AL_SOURCE_DISTANCE_MODEL': + return AL.currentCtx.sourceDistanceModel ? 0 /* AL_FALSE */ : 1 /* AL_TRUE */; + default: +#if OPENAL_DEBUG + console.error('alIsEnabled() with param 0x' + param.toString(16) + ' not implemented yet'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return 0; + } + }, + + alGetDouble: function(param) { + var val = AL.getGlobalParam('alGetDouble', param); + if (val === null) { + return 0.0; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + return val; + default: +#if OPENAL_DEBUG + console.error('alGetDouble(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return 0.0; + } + }, + + alGetDoublev: function(param, pValues) { + var val = AL.getGlobalParam('alGetDoublev', param); + // Silently ignore null destinations, as per the spec for global state functions + if (val === null || !pValues) { + return; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValues', '0', 'val', 'double') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetDoublev(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetFloat: function(param) { + var val = AL.getGlobalParam('alGetFloat', param); + if (val === null) { + return 0.0; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + return val; + default: +#if OPENAL_DEBUG + console.error('alGetFloat(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + return 0.0; + } + }, + + alGetFloatv: function(param, pValues) { + var val = AL.getGlobalParam('alGetFloatv', param); + // Silently ignore null destinations, as per the spec for global state functions + if (val === null || !pValues) { + return; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValues', '0', 'val', 'float') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetFloatv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetInteger: function(param) { + var val = AL.getGlobalParam('alGetInteger', param); + if (val === null) { + return 0; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + return val; + default: +#if OPENAL_DEBUG + console.error('alGetInteger(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return 0; + } + }, + + alGetIntegerv: function(param, pValues) { + var val = AL.getGlobalParam('alGetIntegerv', param); + // Silently ignore null destinations, as per the spec for global state functions + if (val === null || !pValues) { + return; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValues', '0', 'val', 'i32') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetIntegerv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetBoolean: function(param) { + var val = AL.getGlobalParam('alGetBoolean', param); + if (val === null) { + return 0 /* AL_FALSE */; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + return val !== 0 ? 1 /* AL_TRUE */ : 0 /* AL_FALSE */; + default: +#if OPENAL_DEBUG + console.error('alGetBoolean(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return 0 /* AL_FALSE */; + } + }, + + alGetBooleanv: function(param, pValues) { + var val = AL.getGlobalParam('alGetBooleanv', param); + // Silently ignore null destinations, as per the spec for global state functions + if (val === null || !pValues) { + return; + } + + switch (param) { + case 0xC000 /* AL_DOPPLER_FACTOR */: + case 0xC003 /* AL_SPEED_OF_SOUND */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValues', '0', 'val', 'i8') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetBooleanv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alDistanceModel: function(model) { + AL.setGlobalParam('alDistanceModel', 0xD000 /* AL_DISTANCE_MODEL */, model); + }, + + alSpeedOfSound: function(value) { + AL.setGlobalParam('alSpeedOfSound', 0xC003 /* AL_SPEED_OF_SOUND */, model); + }, + + alDopplerFactor: function(value) { + AL.setGlobalParam('alDopplerFactor', 0xC000 /* AL_DOPPLER_FACTOR */, model); + }, + + // http://openal.996291.n3.nabble.com/alSpeedOfSound-or-alDopperVelocity-tp1960.html + // alDopplerVelocity() sets a multiplier for the speed of sound. + // It's deprecated since it's equivalent to directly calling + // alSpeedOfSound() with an appropriately premultiplied value. + alDopplerVelocity: function(value) { + Runtime.warnOnce('alDopplerVelocity() is deprecated, and only kept for compatibility with OpenAL 1.0. Use alSpeedOfSound() instead.'); + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alDopplerVelocity() called without a valid context'); +#endif + return; + } + if (value <= 0) { // Negative or zero values are disallowed + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + }, + + // ------------------------------------------------------- + // -- AL Listener State + // ------------------------------------------------------- + + alGetListenerf: function(param, pValue) { + var val = AL.getListenerParam('alGetListenerf', param); + if (val === null) { + return; + } + if (!pValue) { +#if OPENAL_DEBUG + console.error('alGetListenerf() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x100A /* AL_GAIN */: + {{{ makeSetValue('pValue', '0', 'val', 'float') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetListenerf(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetListener3f: function(param, pValue0, pValue1, pValue2) { + var val = AL.getListenerParam('alGetListener3f', param); + if (val === null) { + return; + } + if (!pValue0 || !pValue1 || !pValue2) { +#if OPENAL_DEBUG + console.error('alGetListener3f() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValue0', '0', 'val[0]', 'float') }}}; + {{{ makeSetValue('pValue1', '0', 'val[1]', 'float') }}}; + {{{ makeSetValue('pValue2', '0', 'val[2]', 'float') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetListener3f(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetListenerfv: function(param, pValues) { + var val = AL.getListenerParam('alGetListenerfv', param); + if (val === null) { + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alGetListenerfv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'float') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'float') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'float') }}}; + break; + case 0x100F /* AL_ORIENTATION */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'float') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'float') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'float') }}}; + {{{ makeSetValue('pValues', '12', 'val[3]', 'float') }}}; + {{{ makeSetValue('pValues', '16', 'val[4]', 'float') }}}; + {{{ makeSetValue('pValues', '20', 'val[5]', 'float') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetListenerfv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetListeneri: function(param, pValue) { + var val = AL.getListenerParam('alGetListeneri', param); + if (val === null) { + return; + } + if (!pValue) { +#if OPENAL_DEBUG + console.error('alGetListeneri() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + +#if OPENAL_DEBUG + console.error('alGetListeneri(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + }, + + alGetListener3i: function(param, pValue0, pValue1, pValue2) { + var val = AL.getListenerParam('alGetListener3i', param); + if (val === null) { + return; + } + if (!pValue0 || !pValue1 || !pValue2) { +#if OPENAL_DEBUG + console.error('alGetListener3i() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValue0', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValue1', '0', 'val[1]', 'i32') }}}; + {{{ makeSetValue('pValue2', '0', 'val[2]', 'i32') }}}; + break; + default: +#if OPENAL_DEBUG + console.error('alGetListener3i(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetListeneriv: function(param, pValues) { + var val = AL.getListenerParam('alGetListeneriv', param); + if (val === null) { + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alGetListeneriv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'i32') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'i32') }}}; break; - case 0x1009 /* AL_BUFFER */: - var buffer = AL.currentContext.buf[value - 1]; - if (value == 0) { - src.queue = []; - } else { - src.queue = [{ buffer: buffer }]; - } - AL.updateSource(src); + case 0x100F /* AL_ORIENTATION */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'i32') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'i32') }}}; + {{{ makeSetValue('pValues', '12', 'val[3]', 'i32') }}}; + {{{ makeSetValue('pValues', '16', 'val[4]', 'i32') }}}; + {{{ makeSetValue('pValues', '20', 'val[5]', 'i32') }}}; break; - case 0x202 /* AL_SOURCE_RELATIVE */: - if (value === 1 /* AL_TRUE */) { - if (src.panner) { - src.panner = null; + default: +#if OPENAL_DEBUG + console.error('alGetListeneriv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, - // Disconnect from the panner. - src.gain.disconnect(); + alListenerf: function(param, value) { + switch (param) { + case 0x100A /* AL_GAIN */: + AL.setListenerParam('alListenerf', param, value); + break; + default: + AL.setListenerParam('alListenerf', param, null); + break; + } + }, - src.gain.connect(AL.currentContext.gain); - } - } else if (value === 0 /* AL_FALSE */) { - if (!src.panner) { - var panner = src.panner = AL.currentContext.ctx.createPanner(); - panner.panningModel = "equalpower"; - panner.distanceModel = "linear"; - panner.refDistance = src.refDistance; - panner.maxDistance = src.maxDistance; - panner.rolloffFactor = src.rolloffFactor; - panner.setPosition(src.position[0], src.position[1], src.position[2]); - // TODO: If support for doppler effect is reintroduced, compute the doppler - // speed pitch factor and apply it here. - panner.connect(AL.currentContext.gain); - - // Disconnect from the default source. - src.gain.disconnect(); - - src.gain.connect(panner); - } - } else { - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - } + alListener3f: function(param, value0, value1, value2) { + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = value0; + AL.paramArray[1] = value1; + AL.paramArray[2] = value2; + AL.setListenerParam('alListener3f', param, AL.paramArray); break; default: -#if OPENAL_DEBUG - console.log("alSourcei with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; + AL.setListenerParam('alListener3f', param, null); break; } }, - alSourcef: function(source, param, value) { - if (!AL.currentContext) { + alListenerfv: function(param, pValues) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourcef called without a valid context"); + console.error('alListenerfv() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pValues) { #if OPENAL_DEBUG - console.error("alSourcef called with an invalid source"); + console.error('alListenerfv() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } - switch (param) { - case 0x1003 /* AL_PITCH */: - if (value <= 0) { - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - return; - } - src.playbackRate = value; - if (src.state === 0x1012 /* AL_PLAYING */) { - // update currently playing entry - var entry = src.queue[src.buffersPlayed]; - if (!entry || !entry.src) return; // It is possible that AL.updateSources() has not yet fed the next buffer, if so, skip. - var currentTime = AL.currentContext.ctx.currentTime; - var oldrate = entry.src.playbackRate.value; - var offset = currentTime - src.bufferPosition; - // entry.src.duration is expressed after factoring in playbackRate, so when changing playback rate, need - // to recompute/rescale the rate to the new playback speed. - entry.src.duration = (entry.src.duration - offset) * oldrate / src.playbackRate; - if (entry.src.playbackRate.value != src.playbackRate) entry.src.playbackRate.value = src.playbackRate; - src.bufferPosition = currentTime; - - // stop other buffers - for (var k = src.buffersPlayed + 1; k < src.queue.length; k++) { - var entry = src.queue[k]; - if (entry.src) { - entry.src.stop(); - entry.src = null; - } - } - // update the source to reschedule buffers with the new playbackRate - AL.updateSource(src); - } - break; - case 0x100A /* AL_GAIN */: - if (src.gain.gain.value != value) src.gain.gain.value = value; + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'float') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'float') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'float') }}}; + AL.setListenerParam('alListenerfv', param, AL.paramArray); break; - // case 0x100D /* AL_MIN_GAIN */: - // break; - // case 0x100E /* AL_MAX_GAIN */: - // break; - case 0x1023 /* AL_MAX_DISTANCE */: - src.maxDistance = value; + case 0x100F /* AL_ORIENTATION */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'float') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'float') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'float') }}}; + AL.paramArray[3] = {{{ makeGetValue('pValues', '12', 'float') }}}; + AL.paramArray[4] = {{{ makeGetValue('pValues', '16', 'float') }}}; + AL.paramArray[5] = {{{ makeGetValue('pValues', '20', 'float') }}}; + AL.setListenerParam('alListenerfv', param, AL.paramArray); break; - case 0x1021 /* AL_ROLLOFF_FACTOR */: - src.rolloffFactor = value; + default: + AL.setListenerParam('alListenerfv', param, null); break; - case 0x1022 /* AL_CONE_OUTER_GAIN */: - src.coneOuterGain = value; + } + }, + + alListeneri: function(param, value) { + AL.setListenerParam('alListeneri', param, null); + }, + + alListener3i: function(param, value0, value1, value2) { + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = value0; + AL.paramArray[1] = value1; + AL.paramArray[2] = value2; + AL.setListenerParam('alListener3i', param, AL.paramArray); break; - case 0x1001 /* AL_CONE_INNER_ANGLE */: - src.coneInnerAngle = value; + default: + AL.setListenerParam('alListener3i', param, null); break; - case 0x1002 /* AL_CONE_OUTER_ANGLE */: - src.coneOuterAngle = value; + } + }, + + alListeneriv: function(param, pValues) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alListeneriv() called without a valid context'); +#endif + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alListeneriv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'i32') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'i32') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'i32') }}}; + AL.setListenerParam('alListeneriv', param, AL.paramArray); break; - case 0x1020 /* AL_REFERENCE_DISTANCE */: - src.refDistance = value; + case 0x100F /* AL_ORIENTATION */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'i32') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'i32') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'i32') }}}; + AL.paramArray[3] = {{{ makeGetValue('pValues', '12', 'i32') }}}; + AL.paramArray[4] = {{{ makeGetValue('pValues', '16', 'i32') }}}; + AL.paramArray[5] = {{{ makeGetValue('pValues', '20', 'i32') }}}; + AL.setListenerParam('alListeneriv', param, AL.paramArray); break; default: + AL.setListenerParam('alListeneriv', param, null); + break; + } + }, + + // ------------------------------------------------------- + // -- AL Buffer State + // ------------------------------------------------------- + + alIsBuffer: function(bufferId) { + if (!AL.currentCtx) { + return false; + } + if (bufferId > AL.buffers.length) { + return false; + } + + if (!AL.buffers[bufferId]) { + return false; + } else { + return true; + } + }, + + alBufferData: function(bufferId, format, pData, size, freq) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.log("alSourcef with param " + param + " not implemented yet"); + console.error('alBufferData() called without a valid context'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + return; + } + var buf = AL.buffers[bufferId]; + if (!buf) { +#if OPENAL_DEBUG + console.error('alBufferData() called with an invalid buffer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + if (freq <= 0) { +#if OPENAL_DEBUG + console.error('alBufferData() called with an invalid frequency'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + var audioBuf = null; + try { + switch (format) { + case 0x1100 /* AL_FORMAT_MONO8 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(1, size, freq); + var channel0 = audioBuf.getChannelData(0); + for (var i = 0; i < size; ++i) { + channel0[i] = HEAPU8[pData++] * 0.0078125 /* 1/128 */ - 1.0; + } + } + buf.bytesPerSample = 1; + buf.channels = 1; + buf.length = size; + break; + case 0x1101 /* AL_FORMAT_MONO16 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(1, size >> 1, freq); + var channel0 = audioBuf.getChannelData(0); + pData >>= 1; + for (var i = 0; i < size >> 1; ++i) { + channel0[i] = HEAP16[pData++] * 0.000030517578125 /* 1/32768 */; + } + } + buf.bytesPerSample = 2; + buf.channels = 1; + buf.length = size >> 1; + break; + case 0x1102 /* AL_FORMAT_STEREO8 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(2, size >> 1, freq); + var channel0 = audioBuf.getChannelData(0); + var channel1 = audioBuf.getChannelData(1); + for (var i = 0; i < size >> 1; ++i) { + channel0[i] = HEAPU8[pData++] * 0.0078125 /* 1/128 */ - 1.0; + channel1[i] = HEAPU8[pData++] * 0.0078125 /* 1/128 */ - 1.0; + } + } + buf.bytesPerSample = 1; + buf.channels = 2; + buf.length = size >> 1; + break; + case 0x1103 /* AL_FORMAT_STEREO16 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(2, size >> 2, freq); + var channel0 = audioBuf.getChannelData(0); + var channel1 = audioBuf.getChannelData(1); + pData >>= 1; + for (var i = 0; i < size >> 2; ++i) { + channel0[i] = HEAP16[pData++] * 0.000030517578125 /* 1/32768 */; + channel1[i] = HEAP16[pData++] * 0.000030517578125 /* 1/32768 */; + } + } + buf.bytesPerSample = 2; + buf.channels = 2; + buf.length = size >> 2; + break; + case 0x10010 /* AL_FORMAT_MONO_FLOAT32 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(1, size >> 2, freq); + var channel0 = audioBuf.getChannelData(0); + pData >>= 2; + for (var i = 0; i < size >> 2; ++i) { + channel0[i] = HEAPF32[pData++]; + } + } + buf.bytesPerSample = 4; + buf.channels = 1; + buf.length = size >> 2; + break; + case 0x10011 /* AL_FORMAT_STEREO_FLOAT32 */: + if (size > 0) { + audioBuf = AL.currentCtx.audioCtx.createBuffer(2, size >> 3, freq); + var channel0 = audioBuf.getChannelData(0); + var channel1 = audioBuf.getChannelData(1); + pData >>= 2; + for (var i = 0; i < size >> 3; ++i) { + channel0[i] = HEAPF32[pData++]; + channel1[i] = HEAPF32[pData++]; + } + } + buf.bytesPerSample = 4; + buf.channels = 2; + buf.length = size >> 3; + break; + default: +#if OPENAL_DEBUG + console.error('alBufferData() called with invalid format ' + format); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + buf.frequency = freq; + buf.audioBuf = audioBuf; + } catch (e) { +#if OPENAL_DEBUG + console.error('alBufferData() upload failed with an exception ' + e); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + }, + + alGetBufferf: function(bufferId, param, pValue) { + var val = AL.getBufferParam('alGetBufferf', bufferId, param); + if (val === null) { + return; + } + if (!pValue) { +#if OPENAL_DEBUG + console.error('alGetBufferf() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; } + +#if OPENAL_DEBUG + console.error('alGetBufferf(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; }, - alSource3i__deps: ['alSource3f'], - alSource3i: function(source, param, v1, v2, v3) { - _alSource3f(source, param, v1, v2, v3); + alGetBuffer3f: function(bufferId, param, pValue0, pValue1, pValue2) { + var val = AL.getBufferParam('alGetBuffer3f', bufferId, param); + if (val === null) { + return; + } + if (!pValue0 || !pValue1 || !pValue2) { +#if OPENAL_DEBUG + console.error('alGetBuffer3f() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + +#if OPENAL_DEBUG + console.error('alGetBuffer3f(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; }, - alSource3f: function(source, param, v1, v2, v3) { - if (!AL.currentContext) { + alGetBufferfv: function(bufferId, param, pValues) { + var val = AL.getBufferParam('alGetBufferfv', bufferId, param); + if (val === null) { + return; + } + if (!pValues) { #if OPENAL_DEBUG - console.error("alSource3f called without a valid context"); + console.error('alGetBufferfv() called with a null pointer'); #endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } - var src = AL.currentContext.src[source]; - if (!src) { + +#if OPENAL_DEBUG + console.error('alGetBufferfv(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + }, + + alGetBufferi: function(bufferId, param, pValue) { + var val = AL.getBufferParam('alGetBufferi', bufferId, param); + if (val === null) { + return; + } + if (!pValue) { #if OPENAL_DEBUG - console.error("alSource3f called with an invalid source"); + console.error('alGetBufferi() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } + switch (param) { - case 0x1004 /* AL_POSITION */: - src.position[0] = v1; - src.position[1] = v2; - src.position[2] = v3; + case 0x2001 /* AL_FREQUENCY */: + case 0x2002 /* AL_BITS */: + case 0x2003 /* AL_CHANNELS */: + case 0x2004 /* AL_SIZE */: + {{{ makeSetValue('pValue', '0', 'val', 'i32') }}}; break; - case 0x1005 /* AL_DIRECTION */: - src.direction[0] = v1; - src.direction[1] = v2; - src.direction[2] = v3; + default: +#if OPENAL_DEBUG + console.error('alGetBufferi(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; + } + }, + + alGetBuffer3i: function(bufferId, param, pValue0, pValue1, pValue2) { + var val = AL.getBufferParam('alGetBuffer3i', bufferId, param); + if (val === null) { + return; + } + if (!pValue0 || !pValue1 || !pValue2) { +#if OPENAL_DEBUG + console.error('alGetBuffer3i() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + +#if OPENAL_DEBUG + console.error('alGetBuffer3i(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + }, + + alGetBufferiv: function(bufferId, param, pValues) { + var val = AL.getBufferParam('alGetBufferiv', bufferId, param); + if (val === null) { + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alGetBufferiv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } + + switch (param) { + case 0x2001 /* AL_FREQUENCY */: + case 0x2002 /* AL_BITS */: + case 0x2003 /* AL_CHANNELS */: + case 0x2004 /* AL_SIZE */: + {{{ makeSetValue('pValues', '0', 'val', 'i32') }}}; break; - case 0x1006 /* AL_VELOCITY */: - src.velocity[0] = v1; - src.velocity[1] = v2; - src.velocity[2] = v3; + case 0x2015 /* AL_LOOP_POINTS_SOFT */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'i32') }}}; break; default: #if OPENAL_DEBUG - console.log("alSource3f with param " + param + " not implemented yet"); + console.error('alGetBufferiv(): param 0x' + param.toString(16) + ' has wrong signature'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } }, - alSourcefv__deps: ['alSource3f'], - alSourcefv: function(source, param, value) { - _alSource3f(source, param, - {{{ makeGetValue('value', '0', 'float') }}}, - {{{ makeGetValue('value', '4', 'float') }}}, - {{{ makeGetValue('value', '8', 'float') }}}); + // All of the remaining alBuffer* setters and getters are only of interest + // to extensions which need them. Core OpenAL alone defines no valid + // property for these. + + alBufferf: function(bufferId, param, value) { + AL.setBufferParam('alBufferf', bufferId, param, null); + }, + + alBuffer3f: function(bufferId, param, value0, value1, value2) { + AL.setBufferParam('alBuffer3f', bufferId, param, null); }, - alSourceQueueBuffers: function(source, count, buffers) { - if (!AL.currentContext) { + alBufferfv: function(bufferId, param, pValues) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourceQueueBuffers called without a valid context"); + console.error('alBufferfv() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pValues) { #if OPENAL_DEBUG - console.error("alSourceQueueBuffers called with an invalid source"); + console.error('alBufferfv() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } - for (var i = 0; i < count; ++i) { - var bufferIdx = {{{ makeGetValue('buffers', 'i*4', 'i32') }}}; - if (bufferIdx > AL.currentContext.buf.length) { + + AL.setBufferParam('alBufferfv', bufferId, param, null); + }, + + alBufferi: function(bufferId, param, value) { + AL.setBufferParam('alBufferi', bufferId, param, null); + }, + + alBuffer3i: function(bufferId, param, value0, value1, value2) { + AL.setBufferParam('alBuffer3i', bufferId, param, null); + }, + + alBufferiv: function(bufferId, param, pValues) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourceQueueBuffers called with an invalid buffer"); + console.error('alBufferiv() called without a valid context'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; - return; - } + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alBufferiv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; } - for (var i = 0; i < count; ++i) { - var bufferIdx = {{{ makeGetValue('buffers', 'i*4', 'i32') }}}; - var buffer = AL.currentContext.buf[bufferIdx - 1]; - src.queue.push({ buffer: buffer, src: null }); + switch (param) { + case 0x2015 /* AL_LOOP_POINTS_SOFT */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'i32') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'i32') }}}; + AL.setBufferParam('alBufferiv', bufferId, param, AL.paramArray); + break; + default: + AL.setBufferParam('alBufferiv', bufferId, param, null); + break; + } + }, + + // ------------------------------------------------------- + // -- AL Source State + // ------------------------------------------------------- + + alIsSource: function(sourceId) { + if (!AL.currentCtx) { + return false; + } + + if (!AL.currentCtx.sources[sourceId]) { + return false; + } else { + return true; } - - AL.updateSource(src); }, - alSourceUnqueueBuffers: function(source, count, buffers) { - if (!AL.currentContext) { + alSourceQueueBuffers: function(sourceId, count, pBufferIds) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourceUnqueueBuffers called without a valid context"); + console.error('alSourceQueueBuffers() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; + var src = AL.currentCtx.sources[sourceId]; if (!src) { #if OPENAL_DEBUG - console.error("alSourceUnqueueBuffers called with an invalid source"); + console.error('alSourceQueueBuffers() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + if (src.type === 0x1028 /* AL_STATIC */) { +#if OPENAL_DEBUG + console.error('alSourceQueueBuffers() called while a static buffer is bound'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; return; } - if (count > src.buffersPlayed) { - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; + if (count === 0) { return; } - for (var i = 0; i < count; i++) { - var entry = src.queue.shift(); - // Write the buffers index out to the return list. - for (var j = 0; j < AL.currentContext.buf.length; j++) { - var b = AL.currentContext.buf[j]; - if (b && b == entry.buffer) { - {{{ makeSetValue('buffers', 'i*4', 'j+1', 'i32') }}}; - break; - } + // Find the first non-zero buffer in the queue to determine the proper format + var templateBuf = AL.buffers[0]; + for (var i = 0; i < src.bufQueue.length; i++) { + if (src.bufQueue[i].id !== 0) { + templateBuf = src.bufQueue[i]; + break; } - src.buffersPlayed--; } - AL.updateSource(src); - }, - - alDeleteBuffers: function(count, buffers) - { - if (!AL.currentContext) { + for (var i = 0; i < count; ++i) { + var bufId = {{{ makeGetValue('pBufferIds', 'i*4', 'i32') }}}; + var buf = AL.buffers[bufId]; + if (!buf) { #if OPENAL_DEBUG - console.error("alDeleteBuffers called without a valid context"); + console.error('alSourceQueueBuffers() called with an invalid buffer'); #endif - return; - } - if (count > AL.currentContext.buf.length) { - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - return; - } - - for (var i = 0; i < count; ++i) { - var bufferIdx = {{{ makeGetValue('buffers', 'i*4', 'i32') }}} - 1; - - // Make sure the buffer index is valid. - if (bufferIdx >= AL.currentContext.buf.length || !AL.currentContext.buf[bufferIdx]) { - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; return; } - // Make sure the buffer is no longer in use. - var buffer = AL.currentContext.buf[bufferIdx]; - for (var srcId in AL.currentContext.src) { - var src = AL.currentContext.src[srcId]; - if (!src) { - continue; - } - for (var k = 0; k < src.queue.length; k++) { - if (buffer === src.queue[k].buffer) { - AL.currentContext.err = 0xA004 /* AL_INVALID_OPERATION */; - return; - } - } + // Check that the added buffer has the correct format. If the template is the zero buffer, any format is valid. + if (templateBuf.id !== 0 && ( + buf.frequency !== templateBuf.frequency + || buf.bytesPerSample !== templateBuf.bytesPerSample + || buf.channels !== templateBuf.channels) + ) { +#if OPENAL_DEBUG + console.error('alSourceQueueBuffers() called with a buffer of different format'); +#endif + AL.currentCtx.err = 0xA004 /* AL_INVALID_OPERATION */; } } + // If the only buffer in the queue is the zero buffer, clear the queue before we add anything. + if (src.bufQueue.length === 1 && src.bufQueue[0].id === 0) { + src.bufQueue.length = 0; + } + + src.type = 0x1029 /* AL_STREAMING */; for (var i = 0; i < count; ++i) { - var bufferIdx = {{{ makeGetValue('buffers', 'i*4', 'i32') }}} - 1; - delete AL.currentContext.buf[bufferIdx]; + var bufId = {{{ makeGetValue('pBufferIds', 'i*4', 'i32') }}}; + var buf = AL.buffers[bufId]; + buf.refCount++; + src.bufQueue.push(buf); + } + + // if the source is looping, cancel the schedule so we can reschedule the loop order + if (src.looping) { + AL.cancelPendingSourceAudio(src); } + + AL.initSourcePanner(src); + AL.scheduleSourceAudio(src); }, - alGenBuffers: function(count, buffers) { - if (!AL.currentContext) { + alSourceUnqueueBuffers: function(sourceId, count, pBufferIds) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alGenBuffers called without a valid context"); + console.error('alSourceUnqueueBuffers() called without a valid context'); #endif return; } - for (var i = 0; i < count; ++i) { - AL.currentContext.buf.push(null); - {{{ makeSetValue('buffers', 'i*4', 'AL.currentContext.buf.length', 'i32') }}}; + var src = AL.currentCtx.sources[sourceId]; + if (!src) { +#if OPENAL_DEBUG + console.error('alSourceUnqueueBuffers() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + if (count > (src.bufQueue.length === 1 && src.bufQueue[0].id === 0 ? 0 : src.bufsProcessed)) { + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; } - }, - alIsBuffer: function(bufferId) { - if (!AL.currentContext) { - return false; + if (count === 0) { + return; } - if (bufferId > AL.currentContext.buf.length) { - return false; + + for (var i = 0; i < count; i++) { + var buf = src.bufQueue.shift(); + buf.refCount--; + // Write the buffers index out to the return list. + {{{ makeSetValue('pBufferIds', 'i*4', 'buf.id', 'i32') }}}; + src.bufsProcessed--; } - if (!AL.currentContext.buf[bufferId - 1]) { - return false; - } else { - return true; + /// If the queue is empty, put the zero buffer back in + if (src.bufQueue.length === 0) { + src.bufQueue.push(AL.buffers[0]); } + + AL.initSourcePanner(src); + AL.scheduleSourceAudio(src); }, - alBufferData: function(buffer, format, data, size, freq) { - if (!AL.currentContext) { + alSourcePlay: function(sourceId) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alBufferData called without a valid context"); + console.error('alSourcePlay() called without a valid context'); #endif return; } - if (buffer > AL.currentContext.buf.length) { + var src = AL.currentCtx.sources[sourceId]; + if (!src) { #if OPENAL_DEBUG - console.error("alBufferData called with an invalid buffer"); + console.error('alSourcePlay() called with an invalid source'); #endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; return; } + AL.setSourceState(src, 0x1012 /* AL_PLAYING */); + }, - try { - switch (format) { - case 0x1100 /* AL_FORMAT_MONO8 */: - var buf = AL.currentContext.ctx.createBuffer(1, size, freq); - buf.bytesPerSample = 1; - var channel0 = buf.getChannelData(0); - for (var i = 0; i < size; ++i) channel0[i] = HEAPU8[data++] * 0.0078125 /* 1/128 */ - 1.0; - break; - case 0x1101 /* AL_FORMAT_MONO16 */: - var buf = AL.currentContext.ctx.createBuffer(1, size>>1, freq); - buf.bytesPerSample = 2; - var channel0 = buf.getChannelData(0); - data >>= 1; - for (var i = 0; i < size>>1; ++i) channel0[i] = HEAP16[data++] * 0.000030517578125 /* 1/32768 */; - break; - case 0x1102 /* AL_FORMAT_STEREO8 */: - var buf = AL.currentContext.ctx.createBuffer(2, size>>1, freq); - buf.bytesPerSample = 1; - var channel0 = buf.getChannelData(0); - var channel1 = buf.getChannelData(1); - for (var i = 0; i < size>>1; ++i) { - channel0[i] = HEAPU8[data++] * 0.0078125 /* 1/128 */ - 1.0; - channel1[i] = HEAPU8[data++] * 0.0078125 /* 1/128 */ - 1.0; - } - break; - case 0x1103 /* AL_FORMAT_STEREO16 */: - var buf = AL.currentContext.ctx.createBuffer(2, size>>2, freq); - buf.bytesPerSample = 2; - var channel0 = buf.getChannelData(0); - var channel1 = buf.getChannelData(1); - data >>= 1; - for (var i = 0; i < size>>2; ++i) { - channel0[i] = HEAP16[data++] * 0.000030517578125 /* 1/32768 */; - channel1[i] = HEAP16[data++] * 0.000030517578125 /* 1/32768 */; - } - break; - case 0x10010 /* AL_FORMAT_MONO_FLOAT32 */: - var buf = AL.currentContext.ctx.createBuffer(1, size>>2, freq); - buf.bytesPerSample = 4; - var channel0 = buf.getChannelData(0); - data >>= 2; - for (var i = 0; i < size>>2; ++i) channel0[i] = HEAPF32[data++]; - break; - case 0x10011 /* AL_FORMAT_STEREO_FLOAT32 */: - var buf = AL.currentContext.ctx.createBuffer(2, size>>3, freq); - buf.bytesPerSample = 4; - var channel0 = buf.getChannelData(0); - var channel1 = buf.getChannelData(1); - data >>= 2; - for (var i = 0; i < size>>2; ++i) { - channel0[i] = HEAPF32[data++]; - channel1[i] = HEAPF32[data++]; - } - break; - default: -#if OPENAL_DEBUG - console.error("alBufferData called with invalid format " + format); -#endif - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - break; - } - AL.currentContext.buf[buffer - 1] = buf; - } catch (e) { + alSourcePlayv: function(count, pSourceIds) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alBufferData upload failed with an exception " + e); + console.error('alSourcePlayv() called without a valid context'); #endif - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; + return; } - }, - - alGetBufferi: function(buffer, param, value) - { - if (!AL.currentContext) { + if (!pSourceIds) { #if OPENAL_DEBUG - console.error("alGetBufferi called without a valid context"); + console.error('alSourcePlayv() called with null pointer'); #endif - return; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; } - var buf = AL.currentContext.buf[buffer - 1]; - if (!buf) { + for (var i = 0; i < count; ++i) { + if (!AL.currentCtx.sources[{{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}]) { #if OPENAL_DEBUG - console.error("alGetBufferi called with an invalid buffer"); + console.error('alSourcePlayv() called with an invalid source'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; - return; + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } } - switch (param) { - case 0x2001 /* AL_FREQUENCY */: - {{{ makeSetValue('value', '0', 'buf.sampleRate', 'i32') }}}; - break; - case 0x2002 /* AL_BITS */: - {{{ makeSetValue('value', '0', 'buf.bytesPerSample * 8', 'i32') }}}; - break; - case 0x2003 /* AL_CHANNELS */: - {{{ makeSetValue('value', '0', 'buf.numberOfChannels', 'i32') }}}; - break; - case 0x2004 /* AL_SIZE */: - {{{ makeSetValue('value', '0', 'buf.length * buf.bytesPerSample * buf.numberOfChannels', 'i32') }}}; - break; - default: - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + + for (var i = 0; i < count; ++i) { + AL.setSourceState({{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}, 0x1012 /* AL_PLAYING */); } }, - alSourcePlay: function(source) { - if (!AL.currentContext) { + alSourceStop: function(sourceId) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourcePlay called without a valid context"); + console.error('alSourceStop() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; + var src = AL.currentCtx.sources[sourceId]; if (!src) { #if OPENAL_DEBUG - console.error("alSourcePlay called with an invalid source"); + console.error('alSourceStop() called with an invalid source'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; return; } - AL.setSourceState(src, 0x1012 /* AL_PLAYING */); + AL.setSourceState(src, 0x1014 /* AL_STOPPED */); }, - alSourceStop: function(source) { - if (!AL.currentContext) { + alSourceStopv: function(count, pSourceIds) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourceStop called without a valid context"); + console.error('alSourceStopv() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pSourceIds) { #if OPENAL_DEBUG - console.error("alSourceStop called with an invalid source"); + console.error('alSourceStopv() called with null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; - return; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + } + for (var i = 0; i < count; ++i) { + if (!AL.currentCtx.sources[{{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}]) { +#if OPENAL_DEBUG + console.error('alSourceStopv() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + } + + for (var i = 0; i < count; ++i) { + AL.setSourceState({{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}, 0x1014 /* AL_STOPPED */); } - AL.setSourceState(src, 0x1014 /* AL_STOPPED */); }, - alSourceRewind: function(source) { - if (!AL.currentContext) { + alSourceRewind: function(sourceId) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourceRewind called without a valid context"); + console.error('alSourceRewind() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; + var src = AL.currentCtx.sources[sourceId]; if (!src) { #if OPENAL_DEBUG - console.error("alSourceRewind called with an invalid source"); + console.error('alSourceRewind() called with an invalid source'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; return; } // Stop the source first to clear the source queue @@ -1003,617 +4200,494 @@ var LibraryOpenAL = { AL.setSourceState(src, 0x1011 /* AL_INITIAL */); }, - alSourcePause: function(source) { - if (!AL.currentContext) { + alSourceRewindv: function(count, pSourceIds) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alSourcePause called without a valid context"); + console.error('alSourceRewindv() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pSourceIds) { #if OPENAL_DEBUG - console.error("alSourcePause called with an invalid source"); + console.error('alSourceRewindv() called with null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; - return; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + } + for (var i = 0; i < count; ++i) { + if (!AL.currentCtx.sources[{{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}]) { +#if OPENAL_DEBUG + console.error('alSourceRewindv() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; + } + } + + for (var i = 0; i < count; ++i) { + AL.setSourceState({{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}, 0x1011 /* AL_INITIAL */); } - AL.setSourceState(src, 0x1013 /* AL_PAUSED */); }, - alGetSourcei: function(source, param, value) { - if (!AL.currentContext) { + alSourcePause: function(sourceId) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alGetSourcei called without a valid context"); + console.error('alSourcePause() called without a valid context'); #endif return; } - var src = AL.currentContext.src[source]; + var src = AL.currentCtx.sources[sourceId]; if (!src) { #if OPENAL_DEBUG - console.error("alGetSourcei called with an invalid source"); + console.error('alSourcePause() called with an invalid source'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; return; } + AL.setSourceState(src, 0x1013 /* AL_PAUSED */); + }, - // Being that we have no way to receive end events from buffer nodes, - // we currently proccess and update a source's buffer queue every - // ~QUEUE_INTERVAL milliseconds. However, this interval is not precise, - // so we also forcefully update the source when alGetSourcei is queried - // to aid in the common scenario of application calling alGetSourcei(AL_BUFFERS_PROCESSED) - // to recycle buffers. - AL.updateSource(src); - - switch (param) { - case 0x202 /* AL_SOURCE_RELATIVE */: - {{{ makeSetValue('value', '0', 'src.panner ? 1 : 0', 'i32') }}}; - break; - case 0x1001 /* AL_CONE_INNER_ANGLE */: - {{{ makeSetValue('value', '0', 'src.coneInnerAngle', 'i32') }}}; - break; - case 0x1002 /* AL_CONE_OUTER_ANGLE */: - {{{ makeSetValue('value', '0', 'src.coneOuterAngle', 'i32') }}}; - break; - case 0x1007 /* AL_LOOPING */: - {{{ makeSetValue('value', '0', 'src.loop', 'i32') }}}; - break; - case 0x1009 /* AL_BUFFER */: - if (!src.queue.length) { - {{{ makeSetValue('value', '0', '0', 'i32') }}}; - } else { - // Find the first unprocessed buffer. - var buffer = src.queue[src.buffersPlayed].buffer; - // Return its index. - for (var i = 0; i < AL.currentContext.buf.length; ++i) { - if (buffer == AL.currentContext.buf[i]) { - {{{ makeSetValue('value', '0', 'i+1', 'i32') }}}; - return; - } - } - {{{ makeSetValue('value', '0', '0', 'i32') }}}; - } - break; - case 0x1010 /* AL_SOURCE_STATE */: - {{{ makeSetValue('value', '0', 'src.state', 'i32') }}}; - break; - case 0x1015 /* AL_BUFFERS_QUEUED */: - {{{ makeSetValue('value', '0', 'src.queue.length', 'i32') }}} - break; - case 0x1016 /* AL_BUFFERS_PROCESSED */: - if (src.loop) { - {{{ makeSetValue('value', '0', '0', 'i32') }}} - } else { - {{{ makeSetValue('value', '0', 'src.buffersPlayed', 'i32') }}} + alSourcePausev: function(count, pSourceIds) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alSourcePausev() called without a valid context'); +#endif + return; + } + if (!pSourceIds) { +#if OPENAL_DEBUG + console.error('alSourcePausev() called with null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + } + for (var i = 0; i < count; ++i) { + if (!AL.currentCtx.sources[{{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}]) { +#if OPENAL_DEBUG + console.error('alSourcePausev() called with an invalid source'); +#endif + AL.currentCtx.err = 0xA001 /* AL_INVALID_NAME */; + return; } - break; - default: - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + } + + for (var i = 0; i < count; ++i) { + AL.setSourceState({{{ makeGetValue('pSourceIds', 'i*4', 'i32') }}}, 0x1013 /* AL_PAUSED */); } }, - alGetSourceiv__deps: ['alGetSourcei'], - alGetSourceiv: function(source, param, values) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alGetSourceiv called without a valid context"); -#endif + alGetSourcef: function(sourceId, param, pValue) { + var val = AL.getSourceParam('alGetSourcef', sourceId, param); + if (val === null) { return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pValue) { #if OPENAL_DEBUG - console.error("alGetSourceiv called with an invalid source"); + console.error('alGetSourcef() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } + switch (param) { - case 0x202 /* AL_SOURCE_RELATIVE */: case 0x1001 /* AL_CONE_INNER_ANGLE */: case 0x1002 /* AL_CONE_OUTER_ANGLE */: - case 0x1007 /* AL_LOOPING */: - case 0x1009 /* AL_BUFFER */: - case 0x1010 /* AL_SOURCE_STATE */: - case 0x1015 /* AL_BUFFERS_QUEUED */: - case 0x1016 /* AL_BUFFERS_PROCESSED */: - _alGetSourcei(source, param, values); + case 0x1003 /* AL_PITCH */: + case 0x100A /* AL_GAIN */: + case 0x100D /* AL_MIN_GAIN */: + case 0x100E /* AL_MAX_GAIN */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1022 /* AL_CONE_OUTER_GAIN */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x200B /* AL_SEC_LENGTH_SOFT */: + {{{ makeSetValue('pValue', '0', 'val', 'float') }}}; break; default: #if OPENAL_DEBUG - console.error("alGetSourceiv with param " + param + " not implemented yet"); + console.error('alGetSourcef(): param 0x' + param.toString(16) + ' has wrong signature'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } }, - alGetSourcef: function(source, param, value) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alGetSourcef called without a valid context"); -#endif + alGetSource3f: function(source, param, pValue0, pValue1, pValue2) { + var val = AL.getSourceParam('alGetSource3f', sourceId, param); + if (val === null) { return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pValue0 || !pValue1 || !pValue2) { #if OPENAL_DEBUG - console.error("alGetSourcef called with an invalid source"); + console.error('alGetSource3f() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } + switch (param) { - case 0x1003 /* AL_PITCH */: - {{{ makeSetValue('value', '0', 'src.playbackRate', 'float') }}} - break; - case 0x100A /* AL_GAIN */: - {{{ makeSetValue('value', '0', 'src.gain.gain.value', 'float') }}} - break; - // case 0x100D /* AL_MIN_GAIN */: - // break; - // case 0x100E /* AL_MAX_GAIN */: - // break; - case 0x1023 /* AL_MAX_DISTANCE */: - {{{ makeSetValue('value', '0', 'src.maxDistance', 'float') }}} - break; - case 0x1021 /* AL_ROLLOFF_FACTOR */: - {{{ makeSetValue('value', '0', 'src.rolloffFactor', 'float') }}} - break; - case 0x1022 /* AL_CONE_OUTER_GAIN */: - {{{ makeSetValue('value', '0', 'src.coneOuterGain', 'float') }}} - break; - case 0x1001 /* AL_CONE_INNER_ANGLE */: - {{{ makeSetValue('value', '0', 'src.coneInnerAngle', 'float') }}} - break; - case 0x1002 /* AL_CONE_OUTER_ANGLE */: - {{{ makeSetValue('value', '0', 'src.coneOuterAngle', 'float') }}} + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValue0', '0', 'val[0]', 'float') }}}; + {{{ makeSetValue('pValue1', '0', 'val[1]', 'float') }}}; + {{{ makeSetValue('pValue2', '0', 'val[2]', 'float') }}}; break; - case 0x1020 /* AL_REFERENCE_DISTANCE */: - {{{ makeSetValue('value', '0', 'src.refDistance', 'float') }}} - break; - // case 0x1024 /* AL_SEC_OFFSET */: - // break; - // case 0x1025 /* AL_SAMPLE_OFFSET */: - // break; - // case 0x1026 /* AL_BYTE_OFFSET */: - // break; default: - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; +#if OPENAL_DEBUG + console.error('alGetSource3f(): param 0x' + param.toString(16) + ' has wrong signature'); +#endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } }, - alGetSourcefv__deps: ['alGetSourcef'], - alGetSourcefv: function(source, param, values) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alGetSourcefv called without a valid context"); -#endif + alGetSourcefv: function(sourceId, param, pValues) { + var val = AL.getSourceParam('alGetSourcefv', sourceId, param); + if (val === null) { return; } - var src = AL.currentContext.src[source]; - if (!src) { + if (!pValues) { #if OPENAL_DEBUG - console.error("alGetSourcefv called with an invalid source"); + console.error('alGetSourcefv() called with a null pointer'); #endif - AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } + switch (param) { + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: case 0x1003 /* AL_PITCH */: case 0x100A /* AL_GAIN */: case 0x100D /* AL_MIN_GAIN */: case 0x100E /* AL_MAX_GAIN */: - case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: case 0x1021 /* AL_ROLLOFF_FACTOR */: case 0x1022 /* AL_CONE_OUTER_GAIN */: - case 0x1001 /* AL_CONE_INNER_ANGLE */: - case 0x1002 /* AL_CONE_OUTER_ANGLE */: - case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1023 /* AL_MAX_DISTANCE */: case 0x1024 /* AL_SEC_OFFSET */: case 0x1025 /* AL_SAMPLE_OFFSET */: case 0x1026 /* AL_BYTE_OFFSET */: - _alGetSourcef(source, param, values); + case 0x200B /* AL_SEC_LENGTH_SOFT */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'float') }}}; break; case 0x1004 /* AL_POSITION */: - var position = src.position; - {{{ makeSetValue('values', '0', 'position[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'position[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'position[2]', 'float') }}} - break; case 0x1005 /* AL_DIRECTION */: - var direction = src.direction; - {{{ makeSetValue('values', '0', 'direction[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'direction[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'direction[2]', 'float') }}} - break; case 0x1006 /* AL_VELOCITY */: - var velocity = src.velocity; - {{{ makeSetValue('values', '0', 'velocity[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'velocity[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'velocity[2]', 'float') }}} + {{{ makeSetValue('pValues', '0', 'val[0]', 'float') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'float') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'float') }}}; break; default: #if OPENAL_DEBUG - console.error("alGetSourcefv with param " + param + " not implemented yet"); + console.error('alGetSourcefv(): param 0x' + param.toString(16) + ' has wrong signature'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } }, - alDistanceModel: function(model) { - if (model !== 0 /* AL_NONE */) { -#if OPENAL_DEBUG - console.log("Only alDistanceModel(AL_NONE) is currently supported"); -#endif + alGetSourcei: function(sourceId, param, pValue) { + var val = AL.getSourceParam('alGetSourcei', sourceId, param); + if (val === null) { + return; } - }, - - alGetListenerf: function(pname, value) { - if (!AL.currentContext) { + if (!pValue) { #if OPENAL_DEBUG - console.error("alGetListenerf called without a valid context"); + console.error('alGetSourcei() called with a null pointer'); #endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } - switch (pname) { - case 0x100A /* AL_GAIN */: - {{{ makeSetValue('value', '0', 'AL.currentContext.gain.gain.value', 'float') }}} + + switch (param) { + case 0x202 /* AL_SOURCE_RELATIVE */: + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1007 /* AL_LOOPING */: + case 0x1009 /* AL_BUFFER */: + case 0x1010 /* AL_SOURCE_STATE */: + case 0x1015 /* AL_BUFFERS_QUEUED */: + case 0x1016 /* AL_BUFFERS_PROCESSED */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x1027 /* AL_SOURCE_TYPE */: + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValue', '0', 'val', 'i32') }}}; break; default: #if OPENAL_DEBUG - console.error("alGetListenerf with param " + pname + " not implemented yet"); + console.error('alGetSourcei(): param 0x' + param.toString(16) + ' has wrong signature'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } - }, - alGetListenerfv: function(pname, values) { - if (!AL.currentContext) { + alGetSource3i: function(source, param, pValue0, pValue1, pValue2) { + var val = AL.getSourceParam('alGetSource3i', sourceId, param); + if (val === null) { + return; + } + if (!pValue0 || !pValue1 || !pValue2) { #if OPENAL_DEBUG - console.error("alGetListenerfv called without a valid context"); + console.error('alGetSource3i() called with a null pointer'); #endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } - switch (pname) { + + switch (param) { case 0x1004 /* AL_POSITION */: - var position = AL.currentContext.ctx.listener._position; - {{{ makeSetValue('values', '0', 'position[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'position[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'position[2]', 'float') }}} - break; + case 0x1005 /* AL_DIRECTION */: case 0x1006 /* AL_VELOCITY */: - var velocity = AL.currentContext.ctx.listener._velocity; - {{{ makeSetValue('values', '0', 'velocity[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'velocity[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'velocity[2]', 'float') }}} - break; - case 0x100F /* AL_ORIENTATION */: - var orientation = AL.currentContext.ctx.listener._orientation; - {{{ makeSetValue('values', '0', 'orientation[0]', 'float') }}} - {{{ makeSetValue('values', '4', 'orientation[1]', 'float') }}} - {{{ makeSetValue('values', '8', 'orientation[2]', 'float') }}} - {{{ makeSetValue('values', '12', 'orientation[3]', 'float') }}} - {{{ makeSetValue('values', '16', 'orientation[4]', 'float') }}} - {{{ makeSetValue('values', '20', 'orientation[5]', 'float') }}} + {{{ makeSetValue('pValue0', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValue1', '0', 'val[1]', 'i32') }}}; + {{{ makeSetValue('pValue2', '0', 'val[2]', 'i32') }}}; break; default: #if OPENAL_DEBUG - console.error("alGetListenerfv with param " + pname + " not implemented yet"); + console.error('alGetSource3i(): param 0x' + param.toString(16) + ' has wrong signature'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; + return; } }, - alGetListeneri: function(pname, value) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alGetListeneri called without a valid context"); -#endif + alGetSourceiv: function(sourceId, param, pValues) { + var val = AL.getSourceParam('alGetSourceiv', sourceId, param); + if (val === null) { return; } - switch (pname) { - default: + if (!pValues) { #if OPENAL_DEBUG - console.error("alGetListeneri with param " + pname + " not implemented yet"); + console.error('alGetSourceiv() called with a null pointer'); #endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; } - }, - alListenerf: function(param, value) { - if (!AL.currentContext) { + switch (param) { + case 0x202 /* AL_SOURCE_RELATIVE */: + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1007 /* AL_LOOPING */: + case 0x1009 /* AL_BUFFER */: + case 0x1010 /* AL_SOURCE_STATE */: + case 0x1015 /* AL_BUFFERS_QUEUED */: + case 0x1016 /* AL_BUFFERS_PROCESSED */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x1027 /* AL_SOURCE_TYPE */: + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + case 0xD000 /* AL_DISTANCE_MODEL */: + {{{ makeSetValue('pValues', '0', 'val', 'i32') }}}; + break; + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: + case 0x1006 /* AL_VELOCITY */: + {{{ makeSetValue('pValues', '0', 'val[0]', 'i32') }}}; + {{{ makeSetValue('pValues', '4', 'val[1]', 'i32') }}}; + {{{ makeSetValue('pValues', '8', 'val[2]', 'i32') }}}; + break; + default: #if OPENAL_DEBUG - console.error("alListenerf called without a valid context"); + console.error('alGetSourceiv(): param 0x' + param.toString(16) + ' has wrong signature'); #endif + AL.currentCtx.err = 0xA002 /* AL_INVALID_ENUM */; return; } + }, + + alSourcef: function(sourceId, param, value) { switch (param) { + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1003 /* AL_PITCH */: case 0x100A /* AL_GAIN */: - if (AL.currentContext.gain.gain.value != value) AL.currentContext.gain.gain.value = value; + case 0x100D /* AL_MIN_GAIN */: + case 0x100E /* AL_MAX_GAIN */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1022 /* AL_CONE_OUTER_GAIN */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x200B /* AL_SEC_LENGTH_SOFT */: + AL.setSourceParam('alSourcef', sourceId, param, value); break; default: -#if OPENAL_DEBUG - console.error("alListenerf with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; + AL.setSourceParam('alSourcef', sourceId, param, null); break; } }, - alEnable: function(param) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alEnable called without a valid context"); -#endif - return; - } + alSource3f: function(sourceId, param, value0, value1, value2) { switch (param) { + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = value0; + AL.paramArray[1] = value1; + AL.paramArray[2] = value2; + AL.setSourceParam('alSource3f', sourceId, param, AL.paramArray); + break; default: -#if OPENAL_DEBUG - console.error("alEnable with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; + AL.setSourceParam('alSource3f', sourceId, param, null); break; } }, - alDisable: function(param) { - if (!AL.currentContext) { + alSourcefv: function(sourceId, param, pValues) { + if (!AL.currentCtx) { #if OPENAL_DEBUG - console.error("alDisable called without a valid context"); + console.error('alSourcefv() called without a valid context'); #endif return; } - switch (pname) { - default: -#if OPENAL_DEBUG - console.error("alDisable with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - break; - } - }, - - alListener3f: function(param, v1, v2, v3) { - if (!AL.currentContext) { + if (!pValues) { #if OPENAL_DEBUG - console.error("alListener3f called without a valid context"); + console.error('alSourcefv() called with a null pointer'); #endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; return; } + switch (param) { - case 0x1004 /* AL_POSITION */: - AL.currentContext.ctx.listener._position[0] = v1; - AL.currentContext.ctx.listener._position[1] = v2; - AL.currentContext.ctx.listener._position[2] = v3; - AL.currentContext.ctx.listener.setPosition(v1, v2, v3); + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1003 /* AL_PITCH */: + case 0x100A /* AL_GAIN */: + case 0x100D /* AL_MIN_GAIN */: + case 0x100E /* AL_MAX_GAIN */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1022 /* AL_CONE_OUTER_GAIN */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x200B /* AL_SEC_LENGTH_SOFT */: + var val = {{{ makeGetValue('pValues', '0', 'float') }}}; + AL.setSourceParam('alSourcefv', sourceId, param, val); break; + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: case 0x1006 /* AL_VELOCITY */: - AL.currentContext.ctx.listener._velocity[0] = v1; - AL.currentContext.ctx.listener._velocity[1] = v2; - AL.currentContext.ctx.listener._velocity[2] = v3; - // TODO: The velocity values are not currently used to implement a doppler effect. - // If support for doppler effect is reintroduced, compute the doppler - // speed pitch factor and apply it here. + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'float') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'float') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'float') }}}; + AL.setSourceParam('alSourcefv', sourceId, param, AL.paramArray); break; default: -#if OPENAL_DEBUG - console.error("alListener3f with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; + AL.setSourceParam('alSourcefv', sourceId, param, null); break; } }, - alListenerfv: function(param, values) { - if (!AL.currentContext) { -#if OPENAL_DEBUG - console.error("alListenerfv called without a valid context"); -#endif - return; - } + alSourcei: function(sourceId, param, value) { switch (param) { - case 0x1004 /* AL_POSITION */: - var x = {{{ makeGetValue('values', '0', 'float') }}}; - var y = {{{ makeGetValue('values', '4', 'float') }}}; - var z = {{{ makeGetValue('values', '8', 'float') }}}; - AL.currentContext.ctx.listener._position[0] = x; - AL.currentContext.ctx.listener._position[1] = y; - AL.currentContext.ctx.listener._position[2] = z; - AL.currentContext.ctx.listener.setPosition(x, y, z); - break; - case 0x1006 /* AL_VELOCITY */: - var x = {{{ makeGetValue('values', '0', 'float') }}}; - var y = {{{ makeGetValue('values', '4', 'float') }}}; - var z = {{{ makeGetValue('values', '8', 'float') }}}; - AL.currentContext.ctx.listener._velocity[0] = x; - AL.currentContext.ctx.listener._velocity[1] = y; - AL.currentContext.ctx.listener._velocity[2] = z; - // TODO: The velocity values are not currently used to implement a doppler effect. - // If support for doppler effect is reintroduced, compute the doppler - // speed pitch factor and apply it here. - break; - case 0x100F /* AL_ORIENTATION */: - var x = {{{ makeGetValue('values', '0', 'float') }}}; - var y = {{{ makeGetValue('values', '4', 'float') }}}; - var z = {{{ makeGetValue('values', '8', 'float') }}}; - var x2 = {{{ makeGetValue('values', '12', 'float') }}}; - var y2 = {{{ makeGetValue('values', '16', 'float') }}}; - var z2 = {{{ makeGetValue('values', '20', 'float') }}}; - AL.currentContext.ctx.listener._orientation[0] = x; - AL.currentContext.ctx.listener._orientation[1] = y; - AL.currentContext.ctx.listener._orientation[2] = z; - AL.currentContext.ctx.listener._orientation[3] = x2; - AL.currentContext.ctx.listener._orientation[4] = y2; - AL.currentContext.ctx.listener._orientation[5] = z2; - AL.currentContext.ctx.listener.setOrientation(x, y, z, x2, y2, z2); + case 0x202 /* AL_SOURCE_RELATIVE */: + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1007 /* AL_LOOPING */: + case 0x1009 /* AL_BUFFER */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + case 0xD000 /* AL_DISTANCE_MODEL */: + AL.setSourceParam('alSourcei', sourceId, param, value); break; default: -#if OPENAL_DEBUG - console.error("alListenerfv with param " + param + " not implemented yet"); -#endif - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; + AL.setSourceParam('alSourcei', sourceId, param, null); break; } }, - alIsExtensionPresent: function(extName) { - extName = Pointer_stringify(extName); - - if (extName == "AL_EXT_float32") return 1; - - return 0; - }, - - alcIsExtensionPresent: function(device, extName) { - return 0; - }, - - alGetString: function(param) { - if (AL.stringCache[param]) return AL.stringCache[param]; - var ret; + alSource3i: function(sourceId, param, value0, value1, value2) { switch (param) { - case 0 /* AL_NO_ERROR */: - ret = 'No Error'; - break; - case 0xA001 /* AL_INVALID_NAME */: - ret = 'Invalid Name'; - break; - case 0xA002 /* AL_INVALID_ENUM */: - ret = 'Invalid Enum'; - break; - case 0xA003 /* AL_INVALID_VALUE */: - ret = 'Invalid Value'; - break; - case 0xA004 /* AL_INVALID_OPERATION */: - ret = 'Invalid Operation'; - break; - case 0xA005 /* AL_OUT_OF_MEMORY */: - ret = 'Out of Memory'; - break; - case 0xB001 /* AL_VENDOR */: - ret = 'Emscripten'; - break; - case 0xB002 /* AL_VERSION */: - ret = '1.1'; - break; - case 0xB003 /* AL_RENDERER */: - ret = 'WebAudio'; - break; - case 0xB004 /* AL_EXTENSIONS */: - ret = 'AL_EXT_float32'; + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = value0; + AL.paramArray[1] = value1; + AL.paramArray[2] = value2; + AL.setSourceParam('alSource3i', sourceId, param, AL.paramArray); break; default: - AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */; - return 0; + AL.setSourceParam('alSource3i', sourceId, param, null); + break; } - - ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL); - - AL.stringCache[param] = ret; - - return ret; }, - alGetProcAddress: function(fname) { - return 0; - }, + alSourceiv: function(source, param, pValues) { + if (!AL.currentCtx) { +#if OPENAL_DEBUG + console.error('alSourceiv() called without a valid context'); +#endif + return; + } + if (!pValues) { +#if OPENAL_DEBUG + console.error('alSourceiv() called with a null pointer'); +#endif + AL.currentCtx.err = 0xA003 /* AL_INVALID_VALUE */; + return; + } - alcGetString: function(device, param) { - if (AL.alcStringCache[param]) return AL.alcStringCache[param]; - var ret; switch (param) { - case 0 /* ALC_NO_ERROR */: - ret = 'No Error'; - break; - case 0xA001 /* ALC_INVALID_DEVICE */: - ret = 'Invalid Device'; - break; - case 0xA002 /* ALC_INVALID_CONTEXT */: - ret = 'Invalid Context'; - break; - case 0xA003 /* ALC_INVALID_ENUM */: - ret = 'Invalid Enum'; - break; - case 0xA004 /* ALC_INVALID_VALUE */: - ret = 'Invalid Value'; - break; - case 0xA005 /* ALC_OUT_OF_MEMORY */: - ret = 'Out of Memory'; - break; - case 0x1004 /* ALC_DEFAULT_DEVICE_SPECIFIER */: - if (typeof(AudioContext) !== "undefined" || - typeof(webkitAudioContext) !== "undefined") { - ret = 'Device'; - } else { - return 0; - } - break; - case 0x1005 /* ALC_DEVICE_SPECIFIER */: - if (typeof(AudioContext) !== "undefined" || - typeof(webkitAudioContext) !== "undefined") { - ret = 'Device\0'; - } else { - ret = '\0'; - } - break; - case 0x311 /* ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER */: - return 0; - break; - case 0x310 /* ALC_CAPTURE_DEVICE_SPECIFIER */: - ret = '\0' + case 0x202 /* AL_SOURCE_RELATIVE */: + case 0x1001 /* AL_CONE_INNER_ANGLE */: + case 0x1002 /* AL_CONE_OUTER_ANGLE */: + case 0x1007 /* AL_LOOPING */: + case 0x1009 /* AL_BUFFER */: + case 0x1020 /* AL_REFERENCE_DISTANCE */: + case 0x1021 /* AL_ROLLOFF_FACTOR */: + case 0x1023 /* AL_MAX_DISTANCE */: + case 0x1024 /* AL_SEC_OFFSET */: + case 0x1025 /* AL_SAMPLE_OFFSET */: + case 0x1026 /* AL_BYTE_OFFSET */: + case 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */: + case 0x2009 /* AL_BYTE_LENGTH_SOFT */: + case 0x200A /* AL_SAMPLE_LENGTH_SOFT */: + case 0xD000 /* AL_DISTANCE_MODEL */: + var val = {{{ makeGetValue('pValues', '0', 'i32') }}}; + AL.setSourceParam('alSourceiv', sourceId, param, val); break; - case 0x1006 /* ALC_EXTENSIONS */: - if (!device) { - AL.alcErr = 0xA001 /* ALC_INVALID_DEVICE */; - return 0; - } - ret = ''; + case 0x1004 /* AL_POSITION */: + case 0x1005 /* AL_DIRECTION */: + case 0x1006 /* AL_VELOCITY */: + AL.paramArray[0] = {{{ makeGetValue('pValues', '0', 'i32') }}}; + AL.paramArray[1] = {{{ makeGetValue('pValues', '4', 'i32') }}}; + AL.paramArray[2] = {{{ makeGetValue('pValues', '8', 'i32') }}}; + AL.setSourceParam('alSourceiv', sourceId, param, AL.paramArray); break; default: - AL.alcErr = 0xA003 /* ALC_INVALID_ENUM */; - return 0; + AL.setSourceParam('alSourceiv', sourceId, param, null); + break; } - - ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL); - - AL.alcStringCache[param] = ret; - - return ret; - }, - - alcGetProcAddress: function(device, fname) { - return 0; - }, - - alGetEnumValue: function(name) { - name = Pointer_stringify(name); - - if (name == "AL_FORMAT_MONO_FLOAT32") return 0x10010; - if (name == "AL_FORMAT_STEREO_FLOAT32") return 0x10011; - - AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */; - return 0; - }, - - alSpeedOfSound: function(value) { - Runtime.warnOnce('alSpeedOfSound() is not yet implemented! Ignoring all calls to it.'); - }, - - alDopplerFactor: function(value) { - Runtime.warnOnce('alDopplerFactor() is not yet implemented! Ignoring all calls to it.'); - }, - - alDopplerVelocity: function(value) { - Runtime.warnOnce('alDopplerVelocity() is not yet implemented! Ignoring all calls to it.'); } }; diff --git a/system/lib/al.c b/system/lib/al.c new file mode 100644 index 0000000000000..aafaeedae1e11 --- /dev/null +++ b/system/lib/al.c @@ -0,0 +1,138 @@ +// AL proc address retrieval + +#include +#include +#include + +#include +#include + + +// ALC extensions +void emscripten_alcDevicePauseSOFT(ALCdevice *device); +void emscripten_alcDeviceResumeSOFT(ALCdevice *device); +const ALCchar *emscripten_alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index); +ALCboolean emscripten_alcResetDeviceSOFT(ALCdevice *device, const ALCint *attrList); + + +void* emscripten_GetAlcProcAddress(ALCchar *name) { + // Base API + if (!strcmp(name, "alcCreateContext")) { return alcCreateContext; } + else if (!strcmp(name, "alcMakeContextCurrent")) { return alcMakeContextCurrent; } + else if (!strcmp(name, "alcProcessContext")) { return alcProcessContext; } + else if (!strcmp(name, "alcSuspendContext")) { return alcSuspendContext; } + else if (!strcmp(name, "alcDestroyContext")) { return alcDestroyContext; } + else if (!strcmp(name, "alcGetCurrentContext")) { return alcGetCurrentContext; } + else if (!strcmp(name, "alcGetContextsDevice")) { return alcGetContextsDevice; } + else if (!strcmp(name, "alcOpenDevice")) { return alcOpenDevice; } + else if (!strcmp(name, "alcCloseDevice")) { return alcCloseDevice; } + else if (!strcmp(name, "alcGetError")) { return alcGetError; } + else if (!strcmp(name, "alcIsExtensionPresent")) { return alcIsExtensionPresent; } + else if (!strcmp(name, "alcGetProcAddress")) { return alcGetProcAddress; } + else if (!strcmp(name, "alcGetEnumValue")) { return alcGetEnumValue; } + else if (!strcmp(name, "alcGetString")) { return alcGetString; } + else if (!strcmp(name, "alcGetIntegerv")) { return alcGetIntegerv; } + else if (!strcmp(name, "alcCaptureOpenDevice")) { return alcCaptureOpenDevice; } + else if (!strcmp(name, "alcCaptureCloseDevice")) { return alcCaptureCloseDevice; } + else if (!strcmp(name, "alcCaptureStart")) { return alcCaptureStart; } + else if (!strcmp(name, "alcCaptureStop")) { return alcCaptureStop; } + else if (!strcmp(name, "alcCaptureSamples")) { return alcCaptureSamples; } + + // Extensions + else if (!strcmp(name, "alcDevicePauseSOFT")) { return emscripten_alcDevicePauseSOFT; } + else if (!strcmp(name, "alcDeviceResumeSOFT")) { return emscripten_alcDeviceResumeSOFT; } + else if (!strcmp(name, "alcGetStringiSOFT")) { return emscripten_alcGetStringiSOFT; } + else if (!strcmp(name, "alcResetDeviceSOFT")) { return emscripten_alcResetDeviceSOFT; } + + EM_ASM_({ + Module.printErr("bad name in alcGetProcAddress: " + Pointer_stringify($0)); + }, name); + return 0; +} + + +void* emscripten_GetAlProcAddress(ALchar *name) { + // Base API + if (!strcmp(name, "alDopplerFactor")) { return alDopplerFactor; } + else if (!strcmp(name, "alDopplerVelocity")) { return alDopplerVelocity; } + else if (!strcmp(name, "alSpeedOfSound")) { return alSpeedOfSound; } + else if (!strcmp(name, "alDistanceModel")) { return alDistanceModel; } + else if (!strcmp(name, "alEnable")) { return alEnable; } + else if (!strcmp(name, "alDisable")) { return alDisable; } + else if (!strcmp(name, "alIsEnabled")) { return alIsEnabled; } + else if (!strcmp(name, "alGetString")) { return alGetString; } + else if (!strcmp(name, "alGetBooleanv")) { return alGetBooleanv; } + else if (!strcmp(name, "alGetIntegerv")) { return alGetIntegerv; } + else if (!strcmp(name, "alGetFloatv")) { return alGetFloatv; } + else if (!strcmp(name, "alGetDoublev")) { return alGetDoublev; } + else if (!strcmp(name, "alGetBoolean")) { return alGetBoolean; } + else if (!strcmp(name, "alGetInteger")) { return alGetInteger; } + else if (!strcmp(name, "alGetFloat")) { return alGetFloat; } + else if (!strcmp(name, "alGetDouble")) { return alGetDouble; } + else if (!strcmp(name, "alGetError")) { return alGetError; } + else if (!strcmp(name, "alIsExtensionPresent")) { return alIsExtensionPresent; } + else if (!strcmp(name, "alGetProcAddress")) { return alGetProcAddress; } + else if (!strcmp(name, "alGetEnumValue")) { return alGetEnumValue; } + else if (!strcmp(name, "alListenerf")) { return alListenerf; } + else if (!strcmp(name, "alListener3f")) { return alListener3f; } + else if (!strcmp(name, "alListenerfv")) { return alListenerfv; } + else if (!strcmp(name, "alListeneri")) { return alListeneri; } + else if (!strcmp(name, "alListener3i")) { return alListener3i; } + else if (!strcmp(name, "alListeneriv")) { return alListeneriv; } + else if (!strcmp(name, "alGetListenerf")) { return alGetListenerf; } + else if (!strcmp(name, "alGetListener3f")) { return alGetListener3f; } + else if (!strcmp(name, "alGetListenerfv")) { return alGetListenerfv; } + else if (!strcmp(name, "alGetListeneri")) { return alGetListeneri; } + else if (!strcmp(name, "alGetListener3i")) { return alGetListener3i; } + else if (!strcmp(name, "alGetListeneriv")) { return alGetListeneriv; } + else if (!strcmp(name, "alGenSources")) { return alGenSources; } + else if (!strcmp(name, "alDeleteSources")) { return alDeleteSources; } + else if (!strcmp(name, "alIsSource")) { return alIsSource; } + else if (!strcmp(name, "alIsSource")) { return alIsSource; } + else if (!strcmp(name, "alSourcef")) { return alSourcef; } + else if (!strcmp(name, "alSource3f")) { return alSource3f; } + else if (!strcmp(name, "alSourcefv")) { return alSourcefv; } + else if (!strcmp(name, "alSourcei")) { return alSourcei; } + else if (!strcmp(name, "alSource3i")) { return alSource3i; } + else if (!strcmp(name, "alSourceiv")) { return alSourceiv; } + else if (!strcmp(name, "alGetSourcef")) { return alGetSourcef; } + else if (!strcmp(name, "alGetSource3f")) { return alGetSource3f; } + else if (!strcmp(name, "alGetSourcefv")) { return alGetSourcefv; } + else if (!strcmp(name, "alGetSourcei")) { return alGetSourcei; } + else if (!strcmp(name, "alGetSource3i")) { return alGetSource3i; } + else if (!strcmp(name, "alGetSourceiv")) { return alGetSourceiv; } + else if (!strcmp(name, "alSourcePlayv")) { return alSourcePlayv; } + else if (!strcmp(name, "alSourceStopv")) { return alSourceStopv; } + else if (!strcmp(name, "alSourceRewindv")) { return alSourceRewindv; } + else if (!strcmp(name, "alSourcePausev")) { return alSourcePausev; } + else if (!strcmp(name, "alSourcePlay")) { return alSourcePlay; } + else if (!strcmp(name, "alSourceStop")) { return alSourceStop; } + else if (!strcmp(name, "alSourceRewind")) { return alSourceRewind; } + else if (!strcmp(name, "alSourcePause")) { return alSourcePause; } + else if (!strcmp(name, "alSourceQueueBuffers")) { return alSourceQueueBuffers; } + else if (!strcmp(name, "alSourceUnqueueBuffers")) { return alSourceUnqueueBuffers; } + else if (!strcmp(name, "alGenBuffers")) { return alGenBuffers; } + else if (!strcmp(name, "alDeleteBuffers")) { return alDeleteBuffers; } + else if (!strcmp(name, "alIsBuffer")) { return alIsBuffer; } + else if (!strcmp(name, "alBufferData")) { return alBufferData; } + else if (!strcmp(name, "alBufferData")) { return alBufferData; } + else if (!strcmp(name, "alBufferf")) { return alBufferf; } + else if (!strcmp(name, "alBuffer3f")) { return alBuffer3f; } + else if (!strcmp(name, "alBufferfv")) { return alBufferfv; } + else if (!strcmp(name, "alBufferi")) { return alBufferi; } + else if (!strcmp(name, "alBuffer3i")) { return alBuffer3i; } + else if (!strcmp(name, "alBufferiv")) { return alBufferiv; } + else if (!strcmp(name, "alGetBufferf")) { return alGetBufferf; } + else if (!strcmp(name, "alGetBuffer3f")) { return alGetBuffer3f; } + else if (!strcmp(name, "alGetBufferfv")) { return alGetBufferfv; } + else if (!strcmp(name, "alGetBufferi")) { return alGetBufferi; } + else if (!strcmp(name, "alGetBuffer3i")) { return alGetBuffer3i; } + else if (!strcmp(name, "alGetBufferiv")) { return alGetBufferiv; } + + // Extensions + + EM_ASM_({ + Module.printErr("bad name in alGetProcAddress: " + Pointer_stringify($0)); + }, name); + return 0; +} diff --git a/system/lib/al.symbols b/system/lib/al.symbols new file mode 100644 index 0000000000000..0d8b0a8e1a77b --- /dev/null +++ b/system/lib/al.symbols @@ -0,0 +1,2 @@ + T emscripten_GetAlcProcAddress + T emscripten_GetAlProcAddress diff --git a/tests/openal_buffers.c b/tests/openal_buffers.c index ab2eae292c6f0..dec1e14a6d29f 100644 --- a/tests/openal_buffers.c +++ b/tests/openal_buffers.c @@ -182,6 +182,10 @@ int main(int argc, char* argv[]) { numBuffers++; } + ALint srcLen = 0; + alGetSourcei(source, 0x2009 /* AL_BYTE_LENGTH_SOFT */, &srcLen); + assert(srcLen == NUM_BUFFERS * BUFFER_SIZE); + #ifdef TEST_ANIMATED_PITCH printf("You should hear a clip of the 1902 piano song \"The Entertainer\" played back at a high pitch rate, and animated to slow down to half playback speed.\n"); #else diff --git a/tests/openal_capture.c b/tests/openal_capture.c new file mode 100644 index 0000000000000..8f33f059189b0 --- /dev/null +++ b/tests/openal_capture.c @@ -0,0 +1,298 @@ +// This tests captures a fixed amount of audio data, +// then plays it back. +// +// Wishlist: +// - Try multiple devices simultaneously; +// - Have several recording passes over the same fixed buffer_size. + +#include +#include +#include +#include +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#define ASSUME_AL_FLOAT32 +#endif +#include +#include +#ifdef ASSUME_AL_FLOAT32 +#define AL_FORMAT_MONO_FLOAT32 0x10010 +#define AL_FORMAT_STEREO_FLOAT32 0x10011 +#endif + +static const char* alformat_string(ALenum format) { + switch(format) { + #define CASE(X) case X: return #X; + CASE(AL_FORMAT_MONO8) + CASE(AL_FORMAT_MONO16) + CASE(AL_FORMAT_STEREO8) + CASE(AL_FORMAT_STEREO16) +#ifdef ASSUME_AL_FLOAT32 + CASE(AL_FORMAT_MONO_FLOAT32) + CASE(AL_FORMAT_STEREO_FLOAT32) +#endif + #undef CASE + } + return ""; +} + +#ifdef __EMSCRIPTEN__ +EMSCRIPTEN_KEEPALIVE +#endif +void end_test(int result) { +#ifdef __EMSCRIPTEN__ + REPORT_RESULT(result); +#else + exit(result); +#endif +} + +#ifndef TEST_SAMPLERATE +#define TEST_SAMPLERATE 44100 +#endif +#ifndef TEST_FORMAT +#define TEST_FORMAT AL_FORMAT_MONO16 +#endif +#ifndef TEST_BUFFERSIZE +#define TEST_BUFFERSIZE TEST_SAMPLERATE*8 // 8 seconds of data +#endif + +// The "arg" pointer passed to iter(). +// It's also a state machine with only two states (see is_playing_back): +// either capturing audio or playing back the captured samples. +typedef struct { + bool is_playing_back; + + // When capturing + const char *capture_device_name; + ALCuint sample_rate; + ALenum format; + ALCsizei buffer_size; + ALCdevice *capture_device; + size_t sample_size; + unsigned nchannels; + + // Playback + ALuint source, buffer; + ALCcontext *context; + ALCdevice *playback_device; +} App; + + +static void iter(void *papp) { + App* const app = papp; + + if(app->is_playing_back) { + ALint state; + alGetSourcei(app->source, AL_SOURCE_STATE, &state); + +#ifdef __EMSCRIPTEN__ + return; +#else + if(state != AL_STOPPED) + return; +#endif + + alDeleteSources(1, &app->source); + alDeleteBuffers(1, &app->buffer); + alcMakeContextCurrent(NULL); + alcDestroyContext(app->context); + alcCloseDevice(app->playback_device); + end_test(EXIT_SUCCESS); + } + + ALCint ncaptured = 0; + alcGetIntegerv(app->capture_device, ALC_CAPTURE_SAMPLES, 1, &ncaptured); + + const ALCint WANTED_NCAPTURED = 7 * app->sample_rate; + + if(ncaptured < WANTED_NCAPTURED) + return; + + size_t datasize = WANTED_NCAPTURED * app->nchannels * app->sample_size; + + ALCubyte *data = malloc(datasize); + if(!data) { + fprintf(stderr, "Out of memory!\n"); + end_test(EXIT_FAILURE); + } + + alcCaptureSamples(app->capture_device, data, WANTED_NCAPTURED); + ALCenum err = alcGetError(app->capture_device); + if(err != ALC_NO_ERROR) { + fprintf(stderr, "alcCaptureSamples() yielded an error, but wasn't supposed to! (%x, %s)\n", err, alcGetString(NULL, err)); + end_test(EXIT_FAILURE); + } + + // This was here to see if alcCaptureSamples() would reset the number of + // available captured samples as a side-effect. + // Turns out, it does (on Linux with OpenAL-Soft). + // That's important to know because this behaviour, while reasonably + // expected, isn't documented anywhere. + /* + { + ALCint ncaptured_now = 0; + alcGetIntegerv(app->capture_device, ALC_CAPTURE_SAMPLES, 1, &ncaptured_now); + + printf( + "For information, number of captured sample frames :\n" + "- Before alcCaptureSamples(): %u;\n" + "- After alcCaptureSamples(): %u.\n" + , (unsigned)ncaptured, (unsigned)ncaptured_now + ); + } + */ + + alcCaptureStop(app->capture_device); + +#ifdef __EMSCRIPTEN__ + // Restarting capture must zero the reported number of captured samples. + // Works in our case because no processing takes place until the current + // iteration yields to the javascript main loop. + alcCaptureStart(app->capture_device); + alcCaptureStop(app->capture_device); + ALCint zeroed_ncaptured = 0xdead; + alcGetIntegerv(app->capture_device, ALC_CAPTURE_SAMPLES, 1, &zeroed_ncaptured); + if(zeroed_ncaptured) { + fprintf(stderr, "Restarting capture didn't zero the reported number of available sample frames!\n"); + } +#endif + + ALCboolean could_close = alcCaptureCloseDevice(app->capture_device); + if(!could_close) { + fprintf(stderr, "Could not close device \"%s\"!\n", app->capture_device_name); + end_test(EXIT_FAILURE); + } + + // We're not as careful with playback - this is already tested + // elsewhere. + app->playback_device = alcOpenDevice(NULL); + assert(app->playback_device); + app->context = alcCreateContext(app->playback_device, NULL); + assert(app->context); + alcMakeContextCurrent(app->context); + alGenBuffers(1, &app->buffer); + alGenSources(1, &app->source); + alBufferData(app->buffer, app->format, data, datasize, app->sample_rate); + alSourcei(app->source, AL_BUFFER, app->buffer); + + free(data); + +#ifdef __EMSCRIPTEN__ + EM_ASM( + var succeed_btn = document.createElement('input'); + var fail_btn = document.createElement('input'); + succeed_btn.type = fail_btn.type = 'button'; + succeed_btn.name = succeed_btn.value = 'Succeed'; + fail_btn.name = fail_btn.value = 'Fail'; + succeed_btn.onclick = function() { + //Module.ccall('end_test', null, ['number'], [0]); + _end_test(0); + }; + fail_btn.onclick = function() { + //Module.ccall('end_test', null, ['number'], [1]); + _end_test(1); + }; + document.body.appendChild(succeed_btn); + document.body.appendChild(fail_btn); + ); +#endif + + app->is_playing_back = true; + alSourcePlay(app->source); + printf( + "You should now hear the captured audio data.\n" +#ifdef __EMSCRIPTEN__ + "Press the [Succeed] button to end the test successfully, or the [Fail] button otherwise.\n" +#endif + ); +} + + +static App app = { + .is_playing_back = false, + .sample_rate = TEST_SAMPLERATE, + .format = TEST_FORMAT, + .buffer_size = TEST_BUFFERSIZE +}; + +#ifdef __EMSCRIPTEN__ +EMSCRIPTEN_KEEPALIVE +#endif +static void ignite() { + + app.capture_device_name = alcGetString(NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER); + + app.capture_device = alcCaptureOpenDevice( + app.capture_device_name, app.sample_rate, app.format, app.buffer_size + ); + if(!app.capture_device) { + ALCenum err = alcGetError(app.capture_device); + fprintf(stderr, + "alcCaptureOpenDevice(\"%s\", sample_rate=%u, format=%s, " + "buffer_size=%u) failed with ALC error %x (%s)\n", + app.capture_device_name, + (unsigned) app.sample_rate, alformat_string(app.format), + (unsigned) app.buffer_size, + (unsigned) err, alcGetString(NULL, err) + ); + end_test(EXIT_FAILURE); + } + + switch(app.format) { + case AL_FORMAT_MONO8: app.sample_size=1; app.nchannels=1; break; + case AL_FORMAT_MONO16: app.sample_size=2; app.nchannels=1; break; + case AL_FORMAT_STEREO8: app.sample_size=1; app.nchannels=2; break; + case AL_FORMAT_STEREO16: app.sample_size=2; app.nchannels=2; break; +#ifdef ASSUME_AL_FLOAT32 + case AL_FORMAT_MONO_FLOAT32: app.sample_size=4; app.nchannels=1; break; + case AL_FORMAT_STEREO_FLOAT32: app.sample_size=4; app.nchannels=2; break; +#endif + } + + alcCaptureStart(app.capture_device); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop_arg(iter, &app, 0, 0); +#else + for(;;) { + iter(&app); + usleep(16666); + } +#endif +} + +int main() { + + printf( + "This test will attempt to capture %f seconds " + "worth of audio data from your default audio " + "input device, and then play it back.\n" + , TEST_BUFFERSIZE / (float) TEST_SAMPLERATE + ); +#ifdef __EMSCRIPTEN__ + printf( + "Press the [Start Recording] button below when you're ready, then " + "allow audio capture when asked by the browser.\n" + "No sample should be captured until that moment.\n" + ); + EM_ASM( + var btn = document.createElement('input'); + btn.type = 'button'; + btn.name = btn.value = 'Start recording'; + btn.onclick = function() { + _ignite(); + document.body.removeChild(btn); + }; + document.body.appendChild(btn); + ); +#else + printf("Press [Enter] when you're ready.\n"); + getchar(); + ignite(); +#endif + + return EXIT_SUCCESS; +} diff --git a/tests/openal_capture_sanity.c b/tests/openal_capture_sanity.c new file mode 100644 index 0000000000000..b6305f1070160 --- /dev/null +++ b/tests/openal_capture_sanity.c @@ -0,0 +1,151 @@ +// This test attempts to open all possible capture devices, each one +// several times with a combination of "reasonable" parameters, and +// checks some basic conformance to expectations w.r.t the spec. +// +// Wishlist: +// - Any operation a closed device should fail; +// - Trying to open multiple devices with the same name at the same time +// and different settings should be fine; + +#include +#include +#include +#include +#include +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#define ASSUME_AL_FLOAT32 +#endif +#include +#include + +#ifdef ASSUME_AL_FLOAT32 +// We, in particular, are expected to support this, since +// this is the data type used by Web Audio. +#define AL_FORMAT_MONO_FLOAT32 0x10010 +#define AL_FORMAT_STEREO_FLOAT32 0x10011 +#endif + +#define countof(x) (sizeof(x)/sizeof((x)[0])) + +static int result = EXIT_SUCCESS; + +static void end_test() { +#ifdef __EMSCRIPTEN__ + REPORT_RESULT(result); +#endif + exit(result); +} + +// OfflineAudioContexts are required to support sample rates ranging +// from 22050 to 96000. +// Should we test for purposefully-ugly sample rates within that range, too ? +static const ALCuint SAMPLE_RATES[] = { + 22050, 32000, 37800, 44100, 48000, 88200, 96000 +}; + +static const ALenum FORMATS[] = { + AL_FORMAT_MONO8, + AL_FORMAT_MONO16, + AL_FORMAT_STEREO8, + AL_FORMAT_STEREO16, +#ifdef ASSUME_AL_FLOAT32 + AL_FORMAT_MONO_FLOAT32, + AL_FORMAT_STEREO_FLOAT32, +#endif +}; + +static const char* alformat_string(ALenum format) { + switch(format) { + #define CASE(X) case X: return #X; + CASE(AL_FORMAT_MONO8) + CASE(AL_FORMAT_MONO16) + CASE(AL_FORMAT_STEREO8) + CASE(AL_FORMAT_STEREO16) +#ifdef ASSUME_AL_FLOAT32 + CASE(AL_FORMAT_MONO_FLOAT32) + CASE(AL_FORMAT_STEREO_FLOAT32) +#endif + #undef CASE + } + return ""; +} + +static void check_device_sanity_with_params(const char *name, ALCuint sample_rate, ALenum format, ALCsizei buffer_size) { + + printf( + "Testing \"%s\" @%uHz with %u sample-frames (format: %s)...\n", + name, (unsigned)sample_rate, (unsigned) buffer_size, + alformat_string(format) + ); + + ALCdevice* dev = alcCaptureOpenDevice(name, sample_rate, format, buffer_size); + + if(dev) { + const char *claimed = alcGetString(dev, ALC_CAPTURE_DEVICE_SPECIFIER); + if(strcmp(name, claimed)) { + fprintf(stderr, "The device \"%s\" claims to be actually named \"%s\", which is not correct behavior.\n", name, claimed); + result = EXIT_FAILURE; + } + ALCboolean could_close = alcCaptureCloseDevice(dev); + if(!could_close) { + fprintf(stderr, "alcCaptureCloseDevice() with \"%s\" failed!\n", name); + result = EXIT_FAILURE; + } + return; + } + + ALCenum err = alcGetError(dev); + fprintf(stderr, + "alcCaptureOpenDevice(\"%s\", sample_rate=%u, format=%s, " + "buffer_size=%u) failed with ALC error %x (%s)\n", + name, (unsigned)sample_rate, alformat_string(format), + (unsigned) buffer_size, + (unsigned) err, alcGetString(NULL, err) + ); + result = EXIT_FAILURE; +} + +static void check_device_sanity(const char *name) { + for(int si=0 ; si +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#else +#include "../system/include/AL/al.h" +#include "../system/include/AL/alc.h" +#endif + +ALCdevice* device = NULL; +ALCcontext* context = NULL; + +int main(int argc, char* argv[]) { + // + // Setup the AL context. + // + device = alcOpenDevice(NULL); + + // Request an invalid enum to generate an ALC error + int value = 0; + alcGetIntegerv(device, 0, 1, &value); + assert(alcGetError(device) == ALC_INVALID_ENUM); + // Check that the error is reset after reading it. + assert(alcGetError(device) == ALC_NO_ERROR); + + context = alcCreateContext(device, NULL); + alcMakeContextCurrent(context); + + // Request an invalid enum to generate an AL error + alGetFloat(0); + assert(alGetError() == AL_INVALID_ENUM); + // Check that the error is reset after reading it. + assert(alGetError() == AL_NO_ERROR); + + alcMakeContextCurrent(NULL); + alcDestroyContext(context); + alcCloseDevice(device); + +#ifdef __EMSCRIPTEN__ + int result = 1; + REPORT_RESULT(); +#endif +} + diff --git a/tests/openal_playback.cpp b/tests/openal_playback.cpp index 6414bb3d9ab38..2237316d46b19 100644 --- a/tests/openal_playback.cpp +++ b/tests/openal_playback.cpp @@ -21,7 +21,16 @@ void EMSCRIPTEN_KEEPALIVE test_finished() #ifdef REPORT_RESULT REPORT_RESULT(1); #endif + } + +#if defined(TEST_ALC_SOFT_PAUSE_DEVICE) + typedef void (*ALC_DEVICE_PAUSE_SOFT)(ALCdevice *); + typedef void (*ALC_DEVICE_RESUME_SOFT)(ALCdevice *); + + ALC_DEVICE_PAUSE_SOFT alcDevicePauseSOFT; + ALC_DEVICE_RESUME_SOFT alcDeviceResumeSOFT; +#endif } void playSource(void* arg) @@ -48,16 +57,43 @@ void playSource(void* arg) void main_tick(void *arg) { ALuint source = static_cast(reinterpret_cast(arg)); - double t = emscripten_get_now() * 0.001; + +#if defined(TEST_LOOPED_SEEK_PLAYBACK) + int offset = 0; + alGetSourcei(source, AL_SAMPLE_OFFSET, &offset); + if (offset < 44100 * 3 / 2) { + alSourcei(source, AL_SAMPLE_OFFSET, 44100 * 3 / 2); + } +#elif defined(TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK) double pitch = sin(t) * 0.5 + 1.0; alSourcef(source, AL_PITCH, pitch); +#elif defined(TEST_ANIMATED_LOOPED_DISTANCE_PLAYBACK) + double pos = (sin(t) - 1.0) * 100.0; + ALfloat listenerPos[] = {0.0, 0.0, pos}; + alListenerfv(AL_POSITION, listenerPos); +#elif defined(TEST_ANIMATED_LOOPED_DOPPLER_PLAYBACK) + double vel = sin(t) * (343.3 / 2.0); + ALfloat listenerVel[] = {0.0, 0.0, vel}; + alListenerfv(AL_VELOCITY, listenerVel); +#elif defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK) || defined(TEST_ANIMATED_LOOPED_RELATIVE_PLAYBACK) || defined(TEST_AL_SOFT_SOURCE_SPATIALIZE) + ALfloat listenerPos[] = {cos(t), 0.0, sin(t)}; + alListenerfv(AL_POSITION, listenerPos); +#elif defined(TEST_ALC_SOFT_PAUSE_DEVICE) + ALCcontext *ctx = alcGetCurrentContext(); + ALCdevice *dev = alcGetContextsDevice(ctx); + if (fmod(t, 2.0) < 1.0) { + alcDeviceResumeSOFT(dev); + } else { + alcDevicePauseSOFT(dev); + } +#endif } int main() { int major, minor; alcGetIntegerv(NULL, ALC_MAJOR_VERSION, 1, &major); - alcGetIntegerv(NULL, ALC_MAJOR_VERSION, 1, &minor); + alcGetIntegerv(NULL, ALC_MINOR_VERSION, 1, &minor); assert(major == 1); @@ -65,7 +101,12 @@ int main() { printf("Default device: %s\n", alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER)); ALCdevice* device = alcOpenDevice(NULL); +#if defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK) + ALCint attrs[] = {0x1992 /* ALC_HRTF_SOFT */, ALC_TRUE, 0x1996 /* ALC_HRTF_ID_SOFT */, 0, 0}; + ALCcontext* context = alcCreateContext(device, attrs); +#else ALCcontext* context = alcCreateContext(device, NULL); +#endif alcMakeContextCurrent(context); assert(alGetString(AL_VERSION)); @@ -74,7 +115,7 @@ int main() { printf("OpenAL vendor: %s\n", alGetString(AL_VENDOR)); printf("OpenAL renderer: %s\n", alGetString(AL_RENDERER)); - ALfloat listenerPos[] = {0.0, 0.0, 0.0}; + ALfloat listenerPos[] = {0.0, 0.0, 1.0}; ALfloat listenerVel[] = {0.0, 0.0, 0.0}; ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0}; @@ -153,6 +194,12 @@ int main() { alBufferData(buffers[0], format, &buffer[offset], size - offset, frequency); +#if defined(TEST_AL_SOFT_LOOP_POINTS) + ALint loopPoints[] = {44100, 44100 * 2}; + ALint alLoopPointsSoft = alGetEnumValue("AL_LOOP_POINTS_SOFT"); + alBufferiv(buffers[0], alLoopPointsSoft, loopPoints); +#endif + ALint val; alGetBufferi(buffers[0], AL_FREQUENCY, &val); assert(val == frequency); @@ -181,10 +228,32 @@ int main() { #ifdef TEST_LOOPED_PLAYBACK alSourcei(sources[0], AL_LOOPING, AL_TRUE); - alSourcef(sources[0], AL_PITCH, 1.5f); -#ifdef TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK +#if defined(TEST_LOOPED_SEEK_PLAYBACK) + printf("You should hear a continuously looping ~1.5 second half of a clip of the 1902 piano song \"The Entertainer\". If you hear a full 3 second clip, the test has failed. Press OK when confirmed.\n"); +#elif defined(TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK) printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a dynamic playback rate that smoothly varies its pitch according to a sine wave. Press OK when confirmed.\n"); +#elif defined(TEST_ANIMATED_LOOPED_DISTANCE_PLAYBACK) + alSourcef(sources[0], AL_REFERENCE_DISTANCE, 25.0); + printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" fade in and out. Press OK when confirmed.\n"); +#elif defined(TEST_ANIMATED_LOOPED_DOPPLER_PLAYBACK) + printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a dynamic playback rate that smoothly varies its pitch according to a sine wave doppler shift. Press OK when confirmed.\n"); +#elif defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK) + printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" smoothly panning around the listener. Press OK when confirmed.\n"); +#elif defined(TEST_ANIMATED_LOOPED_RELATIVE_PLAYBACK) + alSourcei(sources[0], AL_SOURCE_RELATIVE, AL_TRUE); + printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" centered at the listener. If it is panning, then the test failed. Press OK when confirmed.\n"); +#elif defined(TEST_ALC_SOFT_PAUSE_DEVICE) + alcDevicePauseSOFT = reinterpret_cast(alcGetProcAddress(device, "alcDevicePauseSOFT")); + alcDeviceResumeSOFT = reinterpret_cast(alcGetProcAddress(device, "alcDeviceResumeSOFT")); + assert(alcDevicePauseSOFT && alcDeviceResumeSOFT); + printf("You should hear a looping clip of the 1902 piano song \"The Entertainer\" That pauses for 1 second every second. Press OK when confirmed.\n"); +#elif defined(TEST_AL_SOFT_LOOP_POINTS) + printf("You should hear a clip of the 1902 piano song \"The Entertainer\" start normally, then begin looping the same 3 notes repeatedly. If you hear the entire clip, then the test failed. Press OK when confirmed.\n"); +#elif defined(TEST_AL_SOFT_SOURCE_SPATIALIZE) + alSourcei(sources[0], 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */, AL_FALSE); + printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" centered at the listener. If it is panning, then the test failed. Press OK when confirmed.\n"); #else + alSourcef(sources[0], AL_PITCH, 1.5f); printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a high playback rate (high pitch). Press OK when confirmed.\n"); #endif EM_ASM( @@ -202,8 +271,8 @@ int main() { #ifdef __EMSCRIPTEN__ -#ifdef TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK - emscripten_set_main_loop_arg(main_tick, (void*)buffers[0], 0, 0); +#if defined(TEST_LOOPED_PLAYBACK) + emscripten_set_main_loop_arg(main_tick, (void*)sources[0], 0, 0); #else emscripten_async_call(playSource, reinterpret_cast(sources[0]), 700); #endif diff --git a/tests/test_browser.py b/tests/test_browser.py index c5f5443ac2826..30c1776d915e4 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -1967,6 +1967,12 @@ def test_perspective(self): def test_glerror(self): self.btest('gl_error.c', expected='1', args=['-s', 'LEGACY_GL_EMULATION=1', '-lGL']) + def test_openal_error(self): + self.btest('openal_error.c', expected='1') + + def test_openal_capture_sanity(self): + self.btest('openal_capture_sanity.c', expected='0') + def test_runtimelink(self): main, supp = self.setup_runtimelink_test() open('supp.cpp', 'w').write(supp) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index f73f70a1b8262..788fdeb606046 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -115,9 +115,36 @@ def test_openal_buffers_animated_pitch(self): def test_openal_looped_pitched_playback(self): self.btest('openal_playback.cpp', '1', args=['-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + def test_openal_looped_seek_playback(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_LOOPED_SEEK_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + def test_openal_animated_looped_pitched_playback(self): self.btest('openal_playback.cpp', '1', args=['-DTEST_ANIMATED_LOOPED_PITCHED_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + def test_openal_animated_looped_distance_playback(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_ANIMATED_LOOPED_DISTANCE_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_animated_looped_doppler_playback(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_ANIMATED_LOOPED_DOPPLER_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_animated_looped_panned_playback(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_ANIMATED_LOOPED_PANNED_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_animated_looped_relative_playback(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_ANIMATED_LOOPED_RELATIVE_PLAYBACK=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_al_soft_loop_points(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_AL_SOFT_LOOP_POINTS=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_alc_soft_pause_device(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_ALC_SOFT_PAUSE_DEVICE=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_al_soft_source_spatialize(self): + self.btest('openal_playback.cpp', '1', args=['-DTEST_AL_SOFT_SOURCE_SPATIALIZE=1', '-DTEST_LOOPED_PLAYBACK=1', '--preload-file', path_from_root('tests', 'sounds', 'the_entertainer.wav') + '@/audio.wav'],) + + def test_openal_capture(self): + self.btest('openal_capture.c', expected='0', args=['-s', 'NO_EXIT_RUNTIME=1']) + def get_freealut_library(self): if WINDOWS and Building.which('cmake'): return self.get_library('freealut', os.path.join('hello_world.bc'), configure=['cmake', '.'], configure_args=['-DBUILD_TESTS=ON']) diff --git a/tools/system_libs.py b/tools/system_libs.py index 06e752a19d6e1..99bbbbdbdc807 100755 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -51,6 +51,7 @@ def read_symbols(path): libcxx_symbols = read_symbols(shared.path_from_root('system', 'lib', 'libcxx', 'symbols')) libcxxabi_symbols = read_symbols(shared.path_from_root('system', 'lib', 'libcxxabi', 'symbols')) gl_symbols = read_symbols(shared.path_from_root('system', 'lib', 'gl.symbols')) + al_symbols = read_symbols(shared.path_from_root('system', 'lib', 'al.symbols')) compiler_rt_symbols = read_symbols(shared.path_from_root('system', 'lib', 'compiler-rt.symbols')) pthreads_symbols = read_symbols(shared.path_from_root('system', 'lib', 'pthreads.symbols')) wasm_libc_symbols = read_symbols(shared.path_from_root('system', 'lib', 'wasm-libc.symbols')) @@ -253,6 +254,12 @@ def create_gl(libname): # libname is ignored, this is just one .o file check_call([shared.PYTHON, shared.EMCC, shared.path_from_root('system', 'lib', 'gl.c'), '-o', o]) return o + # al + def create_al(libname): # libname is ignored, this is just one .o file + o = in_temp('al.o') + check_call([shared.PYTHON, shared.EMCC, shared.path_from_root('system', 'lib', 'al.c'), '-o', o, '-Os']) + return o + def create_html5(libname): src_dir = shared.path_from_root('system', 'lib', 'html5') files = [] @@ -423,6 +430,7 @@ class Dummy(object): system_libs = [('libcxx', 'a', create_libcxx, libcxx_symbols, ['libcxxabi'], True), ('libcxxabi', 'bc', create_libcxxabi, libcxxabi_symbols, ['libc'], False), ('gl', 'bc', create_gl, gl_symbols, ['libc'], False), + ('al', 'bc', create_al, al_symbols, ['libc'], False), ('html5', 'bc', create_html5, html5_symbols, ['html5'], False), ('compiler-rt', 'a', create_compiler_rt, compiler_rt_symbols, ['libc'], False), (dlmalloc_name(), 'bc', create_dlmalloc, [], [], False)]