package lime._internal.backend.native; import sys.thread.Thread; import sys.thread.Mutex; import haxe.Timer; import haxe.Int64; import lime.media.openal.AL; import lime.media.openal.ALBuffer; import lime.media.openal.ALSource; #if lime_vorbis import lime.media.vorbis.Vorbis; import lime.media.vorbis.VorbisFile; import lime.media.vorbis.VorbisInfo; #end import lime.math.Vector2; import lime.math.Vector4; import lime.media.AudioBuffer; import lime.media.AudioSource; import lime.system.Endian; import lime.system.System; import lime.utils.ArrayBuffer; import lime.utils.ArrayBufferView.TypedArrayType; import lime.utils.ArrayBufferView; #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end @:access(haxe.Timer) @:access(lime.media.AudioBuffer) @:access(lime.utils.ArrayBufferView) class NativeAudioSource { // Can hold up to 3 hours 44100 sampleRate audio, if you are into that, theorically. public static var STREAM_BUFFER_SAMPLES:Int = 0x2000; // how much buffers will be generating every frequency (doesnt have to be pow of 2?). public static var STREAM_MIN_BUFFERS:Int = 2; // how much buffers can a stream hold on minimum or starting. public static var STREAM_MAX_BUFFERS:Int = 8; // how much limit of a buffers can be used for streamed audios, must be higher than minimum. public static var STREAM_MAX_FLUSH_BUFFERS:Int = 3; // how much buffers can it play. public static var STREAM_PROCESS_BUFFERS:Int = 2; // how much buffers can be processed in a frequency tick. public static var POOL_MAX_BUFFERS:Int = 32; // how much buffers for the pool to hold. public static var moreFormatsSupported:Null; public static var loopPointsSupported:Null; public static var stereoAnglesExtensionSupported:Null; public static var latencyExtensionSupported:Null; private static var bufferDataPool:Array = []; private static var isBigEndian:Bool = System.endianness == Endian.BIG_ENDIAN; public static function getALFormat(bitsPerSample:Int, channels:Int):Int { if (moreFormatsSupported == null) moreFormatsSupported = AL.isExtensionPresent("AL_EXT_MCFORMATS"); // There was a code to also supports for X-Fi Renderer but, that kind of device is // rare nowadays and none of sounds should have more than 24 bitsPerSample. // https://github.com/kcat/openal-soft/issues/934 if (channels > 2 && moreFormatsSupported) { if (channels == 3) return bitsPerSample == 32 ? 0x1209 : (bitsPerSample == 16 ? 0x1208 : 0x1207); else if (channels == 4) return bitsPerSample == 32 ? 0x1206 : (bitsPerSample == 16 ? 0x1205 : 0x1204); else if (channels == 6) return bitsPerSample == 32 ? 0x120C : (bitsPerSample == 16 ? 0x120B : 0x120A); else if (channels == 7) return bitsPerSample == 32 ? 0x120F : (bitsPerSample == 16 ? 0x120E : 0x120D); else if (channels == 8) return bitsPerSample == 32 ? 0x1212 : (bitsPerSample == 16 ? 0x1211 : 0x1210); else return AL.FORMAT_MONO8; } else if (bitsPerSample == 32 && moreFormatsSupported) return channels == 2 ? 0x1203 : 0x1202; else if (channels == 2) return bitsPerSample == 16 ? AL.FORMAT_STEREO16 : AL.FORMAT_STEREO8; else return bitsPerSample == 16 ? AL.FORMAT_MONO16 : AL.FORMAT_MONO8; } private static function resetTimer(timer:Timer, time:Float, callback:Void->Void):Timer { if (timer == null) (timer = new Timer(time)).run = callback; else { timer.mTime = time; timer.mFireAt = Timer.getMS() + time; timer.mRunning = true; timer.run = callback; if (!Timer.sRunningTimers.contains(timer)) Timer.sRunningTimers.push(timer); } return timer; } inline private static function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >> 0); // Backward Compatibility Variables var handle(get, set):ALSource; inline function get_handle() return source; inline function set_handle(v) return source = v; var timer(get, set):Timer; inline function get_timer() return completeTimer; inline function set_timer(v) return completeTimer = v; var length(get, set):Null; inline function get_length() return endTime; inline function set_length(v) return endTime = v; var toLoop(get, set):Int; inline function get_toLoop() return streamLoops; inline function set_toLoop(v) return streamLoops = v; var bufferSizes(get, set):Array; inline function get_bufferSizes() return bufferLengths; inline function set_bufferSizes(v) return bufferLengths = v; var parent:AudioSource; var disposed:Bool; var streamed:Bool; var playing:Bool; var completed:Bool; var lastTime:Float; var position:Vector4; var angles:Vector2; var anglesArray:Array; var endTime:Null; var loopTime:Float; var loops:Int; var channels:Int; var sampleRate:Int; var wordSize:Int; // is bitsPerSample >> 3, ex: 8 bits is 1, 16 bits is 2, 32 bits is 4, etc. var samples:Int; var dataLength:Int; var duration:Float; var completeTimer:Timer; var source:ALSource; var buffer:ALBuffer; var standaloneBuffer:Bool; var format:Int; // AL.FORMAT_... var arrayType:TypedArrayType; var loopPoints:Array; // In Samples static var streamSources:Array = []; static var queuedStreamSources:Array = []; static var streamMutex:Mutex = new Mutex(); static var streamTimer:Timer; #if !ALLOW_MULTITHREADING static var wasEmpty:Bool = false; static var threadRunning:Bool = false; static var streamThread:Thread; #end var streamRemove:Bool; var bufferLength:Int; // Size in bytes for current streamed audio buffers. var requestBuffers:Int; var queuedBuffers:Int; var streamLoops:Int; var streamEnded:Bool; // ORDERING IS CURRENT TO NEXT, STARTS FROM THE LENGTH OF THE ARRAYS var bufferDatas:Array; var bufferTimes:Array; var bufferLengths:Array; var buffers:Array; var nextBuffer:Int = 0; public function new(parent:AudioSource) { this.parent = parent; if (loopPointsSupported == null) loopPointsSupported = AL.isExtensionPresent("AL_SOFT_loop_points"); if (stereoAnglesExtensionSupported == null) stereoAnglesExtensionSupported = AL.isExtensionPresent("AL_EXT_STEREO_ANGLES"); if (latencyExtensionSupported == null) latencyExtensionSupported = AL.isExtensionPresent("AL_SOFT_source_latency"); } public function dispose() { streamMutex.acquire(); removeStream(); stop(); disposed = true; position = null; angles = null; anglesArray = null; if (source != null) { if (streamed) AL.sourceUnqueueBuffers(source, AL.getSourcei(source, AL.BUFFERS_QUEUED)); else AL.sourcei(source, AL.BUFFER, AL.NONE); AL.deleteSource(source); source = null; } if (standaloneBuffer && buffer != null) { AL.bufferData(buffer, 0, null, 0, 0); AL.deleteBuffer(buffer); buffer = null; } loopPoints = null; if (buffers != null) { for (buffer in buffers) AL.bufferData(buffer, 0, null, 0, 0); AL.deleteBuffers(buffers); buffers = null; } if (bufferDatas != null) { for (data in bufferDatas) if (bufferDataPool.length < POOL_MAX_BUFFERS) bufferDataPool.push(data); bufferDatas = null; } completeTimer = null; bufferTimes = null; bufferLengths = null; streamMutex.release(); } public function init() { if (source != null || (disposed = parent == null || (source = AL.createSource()) == null)) return; AL.sourcef(source, AL.MAX_GAIN, 32); AL.distanceModel(AL.NONE); if (position == null) position = new Vector4(); if (angles == null) angles = new Vector2(Math.PI / 6, -Math.PI / 6); // https://github.com/kcat/openal-soft/issues/1032 if (loopPoints == null) loopPoints = [0, 0]; if (stereoAnglesExtensionSupported && anglesArray == null) anglesArray = [0, 0]; resetBuffer(); } public function resetBuffer() { if (parent.buffer == null) return; streamMutex.acquire(); removeStream(); stop(); if (streamed) AL.sourceUnqueueBuffers(source, AL.getSourcei(source, AL.BUFFERS_QUEUED)); else AL.sourcei(source, AL.BUFFER, AL.NONE); streamMutex.release(); final audioBuffer = parent.buffer; channels = audioBuffer.channels; sampleRate = audioBuffer.sampleRate; wordSize = audioBuffer.bitsPerSample >> 3; format = getALFormat(audioBuffer.bitsPerSample, channels); arrayType = wordSize == 4 ? TypedArrayType.Uint32 : (wordSize == 2 ? TypedArrayType.Uint16 : TypedArrayType.Int8); standaloneBuffer = false; loopTime = 0; endTime = null; if (buffer != null) { if (standaloneBuffer) { AL.bufferData(buffer, 0, null, 0, 0); AL.deleteBuffer(buffer); } buffer = null; } if (audioBuffer.data != null) { streamed = false; samples = Std.int((dataLength = audioBuffer.data.byteLength) / wordSize / channels); } #if lime_vorbis else if (audioBuffer.__srcVorbisFile != null) { streamed = true; dataLength = Std.int(getFloat(samples = Int64.toInt(audioBuffer.__srcVorbisFile.pcmTotal())) * channels * wordSize); } #end else return; duration = getFloat(samples) / sampleRate * 1000; loopPoints[0] = 0; loopPoints[1] = samples - 1; if (streamed) { final length = STREAM_BUFFER_SAMPLES * channels; bufferLength = length * wordSize; if (buffers == null) buffers = AL.genBuffers(STREAM_MAX_FLUSH_BUFFERS); if (bufferDatas == null) { bufferDatas = []; bufferTimes = []; bufferLengths = []; } for (i in 0...STREAM_MAX_BUFFERS) { bufferTimes[i] = 0.0; bufferLengths[i] = 0; var data = bufferDataPool.pop(); if (data == null) data = new ArrayBufferView(length, arrayType); else { data.type = arrayType; data.bytesPerElement = wordSize; data.length = length; if (data.byteLength != bufferLength) { #if cpp data.buffer.getData().resize(bufferLength); data.buffer.fill(data.byteLength, bufferLength - data.byteLength, 0); @:privateAccess data.buffer.length = bufferLength; #else data.buffer = new ArrayBuffer(bufferLength); #end } data.byteLength = bufferLength; } bufferDatas[i] = data; } } else { if (buffers != null) { for (buffer in buffers) AL.bufferData(buffer, 0, null, 0, 0); AL.deleteBuffers(buffers); buffers = null; for (data in bufferDatas) if (bufferDataPool.length < POOL_MAX_BUFFERS) bufferDataPool.push(data); bufferDatas.resize(0); } if (audioBuffer.__srcBuffer != null) { if (AL.getBufferi(audioBuffer.__srcBuffer, AL.SIZE) != dataLength) { AL.bufferData(audioBuffer.__srcBuffer, 0, null, 0, 0); AL.deleteBuffer(audioBuffer.__srcBuffer); if ((buffer = audioBuffer.__srcBuffer = AL.createBuffer()) != null) AL.bufferData(buffer, format, audioBuffer.data, dataLength, sampleRate); } else buffer = audioBuffer.__srcBuffer; } else if ((buffer = audioBuffer.__srcBuffer = AL.createBuffer()) != null) AL.bufferData(buffer, format, audioBuffer.data, dataLength, sampleRate); AL.sourcei(source, AL.BUFFER, buffer); } updateLoopPoints(); } function updateLoopPoints() { if (loops <= 0) return AL.sourcei(source, AL.LOOPING, AL.FALSE); var time = getCurrentTime() + parent.offset, length = getRealLength(); final fixed = time >= length; if (fixed) time = loopTime; if (!streamed) { var internalLoop = AL.TRUE; if (length < duration - 1 && loopTime > 1) { if (!loopPointsSupported) internalLoop = AL.FALSE; else { AL.sourceStop(source); AL.sourcei(source, AL.BUFFER, AL.NONE); if (!standaloneBuffer) { if (standaloneBuffer = (buffer = AL.createBuffer()) != null) AL.bufferData(buffer, format, parent.buffer.data, dataLength, sampleRate); else { buffer = parent.buffer.__srcBuffer; internalLoop = AL.FALSE; } } if (internalLoop == AL.TRUE) AL.bufferiv(buffer, 0x2015/*AL.LOOP_POINTS_SOFT*/, loopPoints); AL.sourcei(source, AL.BUFFER, buffer); } } AL.sourcei(source, AL.LOOPING, internalLoop); if (playing) setCurrentTime(time - parent.offset); else updateCompleteTimer(); } else if (playing && (fixed || streamLoops > 0)) { AL.sourcei(source, AL.LOOPING, AL.FALSE); AL.sourceStop(source); snapBuffersToTime(time, streamLoops > 0); AL.sourcePlay(source); } } // https://github.com/xiph/vorbis/blob/master/CHANGES#L39 bug in libvorbis <= 1.3.4 inline function streamSeek(samples:Int64) if (samples <= 1) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples); inline function streamTell():Int64 return parent.buffer.__srcVorbisFile.pcmTell(); inline function streamRead(buffer:ArrayBuffer, position:Int, length:Int, wordSize:Int):Int return parent.buffer.__srcVorbisFile.read(buffer, position, length, isBigEndian, wordSize, true); function readToBufferData(data:ArrayBufferView, currentPCM:Int64):Int { var length = (Int64.ofInt(loopPoints[1]) - currentPCM) * channels * wordSize; var n = length < bufferLength ? length.low : bufferLength, total = 0, result = 0, wasEOF = false; try while (total < bufferLength) { result = n > 0 ? streamRead(data.buffer, total, n, wordSize) : 0; if (result == Vorbis.HOLE) continue; else if (result <= Vorbis.EREAD) break; else if (result == 0) { if (streamEnded = wasEOF == (wasEOF = true) || loops <= streamLoops) break; streamSeek(Int64.ofInt(loopPoints[0])); streamLoops++; if ((length = Int64.ofInt(loopPoints[1] - loopPoints[0]) * channels * wordSize) < (n = bufferLength - total)) n = length.low; } else { total += result; n -= result; wasEOF = false; } } catch (e:haxe.Exception) { trace('NativeAudioSource readToBufferData Bug! error: ${e.details()}, streamEnded: $streamEnded, total: $total, n: $n'); return result; } if (result < 0) { trace('NativeAudioSource readToBufferData Bug! reading result is $result, streamEnded: $streamEnded, total: $total, n: $n'); return result; } return total; } function fillBuffers(n:Int) { final max = STREAM_MAX_BUFFERS - 1; var i:Int, j:Int, data:ArrayBufferView, pcm:Int64, decoded:Int; while (n-- > 0 && !streamEnded && (decoded = readToBufferData(data = bufferDatas[i = max - requestBuffers], pcm = streamTell())) > 0) { j = i; while (i < max) { bufferDatas[i] = bufferDatas[++j]; bufferTimes[i] = bufferTimes[j]; bufferLengths[i] = bufferLengths[j]; i = j; } bufferDatas[max] = data; bufferTimes[max] = getFloat(pcm) / sampleRate; bufferLengths[max] = decoded; requestBuffers++; } } inline function flushBuffers() { var i = STREAM_MAX_BUFFERS - (requestBuffers - queuedBuffers); while (queuedBuffers < STREAM_MAX_FLUSH_BUFFERS && queuedBuffers < requestBuffers) { AL.bufferData(buffers[nextBuffer], format, bufferDatas[i], bufferLengths[i], sampleRate); AL.sourceQueueBuffer(source, buffers[nextBuffer]); if (++nextBuffer == STREAM_MAX_FLUSH_BUFFERS) nextBuffer = 0; queuedBuffers++; i++; } } inline function skipBuffers(n:Int) { queuedBuffers -= (n = AL.sourceUnqueueBuffers(source, n).length); requestBuffers -= n; } function snapBuffersToTime(time:Float, force:Bool) { if (source == null || parent.buffer == null || parent.buffer.__srcVorbisFile == null) return; streamMutex.acquire(); final sec = time / 1000; if (!force) { var bufferTime:Float; for (i in (STREAM_MAX_BUFFERS - requestBuffers)...(STREAM_MAX_BUFFERS - STREAM_MIN_BUFFERS)) if (sec >= (bufferTime = bufferTimes[i]) && sec < bufferTime + (bufferLengths[i] / wordSize / channels / sampleRate)) { skipBuffers(i - STREAM_MAX_BUFFERS + requestBuffers); AL.sourcei(source, AL.SAMPLE_OFFSET, Math.floor((sec - bufferTime) * sampleRate)); return streamMutex.release(); } } AL.sourceUnqueueBuffers(source, AL.getSourcei(source, AL.BUFFERS_QUEUED)); streamEnded = false; streamSeek(Int64.fromFloat(sec * sampleRate)); requestBuffers = queuedBuffers = streamLoops = nextBuffer = 0; fillBuffers(STREAM_MIN_BUFFERS); flushBuffers(); streamMutex.release(); } static function streamBuffersUpdate() { streamMutex.acquire(); var i:Int = streamSources.length, source:NativeAudioSource, process:Int, v:Int; while (i-- > 0) { if ((source = streamSources[i]).streamRemove) continue; else if (source.parent.buffer == null) { source.stopStream(); continue; } process = source.requestBuffers < STREAM_MIN_BUFFERS ? STREAM_MIN_BUFFERS - source.requestBuffers : 0; process = STREAM_PROCESS_BUFFERS > process ? STREAM_PROCESS_BUFFERS : process; if ((process = (v = STREAM_MAX_BUFFERS - source.requestBuffers) > process ? process : v) > 0) source.fillBuffers(process); } streamMutex.release(); } #if !ALLOW_MULTITHREADING static function streamThreadRun() { while (Thread.readMessage(true)) streamBuffersUpdate(); threadRunning = false; } #end static function streamUpdate() { if (!streamMutex.tryAcquire()) return; var i = queuedStreamSources.length, source:NativeAudioSource; while (i-- > 0) streamSources.push(queuedStreamSources[i]); queuedStreamSources.resize(0); i = streamSources.length; while (i-- > 0) { if ((source = streamSources[i]).streamRemove || source.source == null) source.removeStream(); else { source.skipBuffers(AL.getSourcei(source.source, AL.BUFFERS_PROCESSED)); source.flushBuffers(); if (AL.getSourcei(source.source, AL.SOURCE_STATE) == AL.STOPPED) { AL.sourcePlay(source.source); source.updateCompleteTimer(); } if (source.streamEnded && source.requestBuffers == source.queuedBuffers) source.removeStream(); } } #if ALLOW_MULTITHREADING if (streamSources.length != 0) funkin.backend.utils.ThreadUtil.execAsync(streamBuffersUpdate); #else if (streamSources.length == 0) { if (wasEmpty) { wasEmpty = false; streamTimer.stop(); if (threadRunning) streamThread.sendMessage(1); } else { wasEmpty = true; streamTimer = resetTimer(streamTimer, 1000, streamUpdate); } } else { wasEmpty = false; if (threadRunning || (threadRunning = (streamThread = Thread.create(streamThreadRun)) != null)) streamThread.sendMessage(1); } #end streamMutex.release(); } function removeStream() { streamRemove = false; queuedStreamSources.remove(this); streamSources.remove(this); } function stopStream() { streamRemove = true; queuedStreamSources.remove(this); } function resetStream() { streamRemove = false; if (!queuedStreamSources.contains(this) && !streamSources.contains(this)) { queuedStreamSources.push(this); if (streamTimer == null || !streamTimer.mRunning) streamTimer = resetTimer(streamTimer, 0, streamUpdate); } } function timer_onRun() { final pitch = getPitch(); var timeRemaining = (getLength() - getCurrentTime()) / pitch; if (timeRemaining > 50 && AL.getSourcei(source, AL.SOURCE_STATE) == AL.PLAYING && (!streamed || !streamEnded && streamLoops <= 0)) { completeTimer = resetTimer(completeTimer, timeRemaining, timer_onRun); return; } completeTimer.stop(); if (loops == 0) return complete(); if (streamLoops > 0) { loops -= streamLoops; streamLoops = 0; completeTimer = resetTimer(completeTimer, (getLength() + parent.offset - loopTime) / pitch, timer_onRun); } else if (!loopPointsSupported || AL.getSourcei(source, AL.LOOPING) == AL.FALSE) { loops--; playing = true; setCurrentTime(loopTime - parent.offset); } if (loops <= 0) { loops = 0; AL.sourcei(source, AL.LOOPING, AL.FALSE); } parent.onLoop.dispatch(); } function updateCompleteTimer() { if (playing) { var timeRemaining = (getLength() - getCurrentTime()) / getPitch(); if (timeRemaining > 50) completeTimer = resetTimer(completeTimer, timeRemaining, timer_onRun); else { if (completeTimer != null) completeTimer.stop(); if (loops > 0) play(); else complete(); } } else if (completeTimer != null) completeTimer.stop(); } public function play() { if (playing || disposed) return; final time = completed ? 0 : getCurrentTime(); playing = true; setCurrentTime(time); } public function pause() { if (!disposed) AL.sourcePause(source); lastTime = getCurrentTime(); playing = false; stopStream(); if (completeTimer != null) completeTimer.stop(); } public function stop() { if (!disposed) AL.sourceStop(source); lastTime = 0; streamLoops = 0; playing = false; stopStream(); if (completeTimer != null) completeTimer.stop(); } public function complete() { if (!completed) parent.onComplete.dispatch(); stop(); completed = true; } public function getCurrentTime():Float { if (disposed) return 0.0; else if (completed) return getLength(); else if (!playing) return lastTime - parent.offset; var time = AL.getSourcef(source, AL.SEC_OFFSET); if (streamed) { if (playing && streamEnded && AL.getSourcei(source, AL.SOURCE_STATE) == AL.STOPPED) { complete(); return getLength(); } else if (bufferTimes != null) time += bufferTimes[STREAM_MAX_BUFFERS - requestBuffers]; } time *= 1000; var length = getRealLength(); return if (loops <= 0 || time <= length) time - parent.offset; else ((time - loopTime) % (length - loopTime)) + loopTime - parent.offset; } public function setCurrentTime(value:Float):Float { if (disposed) return 0.0; final length = getRealLength(); value = Math.isFinite(value) ? Math.max(Math.min(value + parent.offset, length), parent.offset) : parent.offset; if (streamed) AL.sourceStop(source); else AL.sourcef(source, AL.SEC_OFFSET, value / 1000); final timeRemaining = (length - value) / getPitch(); if (playing) { if (timeRemaining < 8 && value > 8) complete(); else { completed = false; if (streamed) { snapBuffersToTime(value, false); if (!streamEnded) resetStream(); } if (AL.getSourcei(source, AL.SOURCE_STATE) != AL.PLAYING) AL.sourcePlay(source); completeTimer = resetTimer(completeTimer, timeRemaining, timer_onRun); } } else { completed = timeRemaining < 8; lastTime = value; if (completeTimer != null) completeTimer.stop(); } return value; } public function getPitch():Float { return if (disposed) 1; else AL.getSourcef(source, AL.PITCH); } public function setPitch(value:Float):Float { if (disposed || (value = Math.max(value, 0)) == AL.getSourcef(source, AL.PITCH)) return value; AL.sourcef(source, AL.PITCH, value); updateCompleteTimer(); return value; } public function getGain():Float { return if (disposed) 1; else AL.getSourcef(source, AL.GAIN); } public function setGain(value:Float):Float { value = Math.max(value, 0); if (!disposed) AL.sourcef(source, AL.GAIN, value); return value; } public function getLoops():Int return loops; public function setLoops(value:Int):Int { if (loops == (loops = value < 0 ? 0 : value)) return loops; updateLoopPoints(); return loops; } public function getLoopTime():Float return loopTime - parent.offset; public function setLoopTime(value:Float):Float { if (loopTime == (loopTime = Math.max(Math.min(value + parent.offset, duration), 0))) return loopTime - parent.offset; if ((loopPoints[0] = Std.int(value / 1000 * sampleRate)) >= samples) loopPoints[0] = samples - 1; updateLoopPoints(); return loopTime - parent.offset; } public function getRealLength():Float return if (endTime == null) duration; else endTime; public function getLength():Float return if (disposed) 0; else (inline getRealLength()) - parent.offset; public function setLength(value:Null):Null { if (endTime == (endTime = (value == null ? value : Math.max(Math.min(value + parent.offset, duration), 0)))) return endTime - parent.offset; if ((loopPoints[1] = Std.int(getRealLength() / 1000 * sampleRate)) >= samples) loopPoints[1] = samples - 1; updateLoopPoints(); return endTime - parent.offset; } public function getLatency():Float { //#if (lime >= "8.4.0") //if (latencyExtensionSupported) { // final offsets = AL.getSourcedvSOFT(source, AL.SEC_OFFSET_LATENCY_SOFT, 2); // if (offsets != null) return offsets[1] * 1000; //} //#end return 0; } public function getAngles():Vector2 { if (angles == null) angles = new Vector2(Math.PI / 6, -Math.PI / 6); return angles; } public function setAngles(left:Float, right:Float):Vector2 { if (angles == null) angles = new Vector2(left, right); else angles.setTo(left, right); if (!disposed && stereoAnglesExtensionSupported) { anglesArray[0] = angles.x; anglesArray[1] = angles.y; AL.sourcei(source, 0x1214/*AL.SOURCE_SPATIALIZE_SOFT*/, AL.FALSE); AL.sourcefv(source, 0x1030/*AL.STEREO_ANGLES*/, anglesArray); AL.source3f(source, AL.POSITION, 0, 0, 0); } return angles; } public function getPosition():Vector4 { if (position == null) position = new Vector4(); return position; } public function setPosition(value:Vector4):Vector4 { position.x = value.x; position.y = value.y; position.z = value.z; position.w = value.w; // OpenAL Soft Positions doesn't seem to do anything but panning? if (!disposed) { if (stereoAnglesExtensionSupported) AL.sourcei(source, 0x1214/*AL.SOURCE_SPATIALIZE_SOFT*/, Math.abs(position.x) > 1e-04 ? AL.TRUE : AL.FALSE); AL.sourcei(source, AL.MAX_DISTANCE, 1); AL.source3f(source, AL.POSITION, position.x, position.y, position.z); } return position; } public function getPan():Float return getPosition().x; public function setPan(value:Float):Float { getPosition().setTo(value, 0, -Math.sqrt(1 - value * value)); if (!disposed) { if (parent.buffer.channels > 1) setAngles(Math.PI * (Math.min(-value * 2 + 1, 1)) / 6, -Math.PI * Math.min(value * 2 + 1, 1) / 6); else setPosition(position); } return value; } }