585 lines
11 KiB
Haxe
585 lines
11 KiB
Haxe
package lime._internal.backend.native;
|
|
|
|
import haxe.Int64;
|
|
import haxe.Timer;
|
|
import lime.math.Vector4;
|
|
import lime.media.openal.AL;
|
|
import lime.media.openal.ALBuffer;
|
|
import lime.media.openal.ALSource;
|
|
import lime.media.vorbis.VorbisFile;
|
|
import lime.media.AudioManager;
|
|
import lime.media.AudioSource;
|
|
import lime.utils.UInt8Array;
|
|
|
|
#if !lime_debug
|
|
@:fileXml('tags="haxe,release"')
|
|
@:noDebug
|
|
#end
|
|
@:access(lime.media.AudioBuffer)
|
|
class NativeAudioSource
|
|
{
|
|
public static var initBuffers:Array<NativeAudioSource> = [];
|
|
|
|
private static var STREAM_BUFFER_SIZE = 48000;
|
|
#if (native_audio_buffers && !macro)
|
|
private static var STREAM_NUM_BUFFERS = Std.parseInt(haxe.macro.Compiler.getDefine("native_audio_buffers"));
|
|
#else
|
|
private static var STREAM_NUM_BUFFERS = 3;
|
|
#end
|
|
private static var STREAM_TIMER_FREQUENCY = 100;
|
|
|
|
private var buffers:Array<ALBuffer>;
|
|
private var bufferTimeBlocks:Array<Float>;
|
|
private var completed:Bool;
|
|
private var dataLength:Int;
|
|
private var format:Int;
|
|
private var handle:ALSource;
|
|
private var length:Null<Float>;
|
|
private var loops:Int;
|
|
private var parent:AudioSource;
|
|
private var playing:Bool;
|
|
private var position:Vector4;
|
|
private var samples:Float;
|
|
private var stream:Bool;
|
|
private var streamTimer:Timer;
|
|
private var timer:Timer;
|
|
|
|
private var pcmTotal:Int64;
|
|
private var sampleRate:Int;
|
|
|
|
var disposed:Bool = false;
|
|
|
|
public function new(parent:AudioSource)
|
|
{
|
|
this.parent = parent;
|
|
|
|
position = new Vector4();
|
|
}
|
|
|
|
public function dispose():Void
|
|
{
|
|
if (handle != null)
|
|
{
|
|
stop();
|
|
AL.sourcei(handle, AL.BUFFER, null);
|
|
AL.deleteSource(handle);
|
|
if (buffers != null)
|
|
{
|
|
for (buffer in buffers)
|
|
{
|
|
AL.deleteBuffer(buffer);
|
|
}
|
|
buffers = null;
|
|
}
|
|
handle = null;
|
|
}
|
|
disposed = true;
|
|
initBuffers.remove(this);
|
|
}
|
|
|
|
public function init():Void
|
|
{
|
|
dataLength = 0;
|
|
format = 0;
|
|
|
|
if (parent.buffer.channels == 1)
|
|
{
|
|
if (parent.buffer.bitsPerSample == 8)
|
|
{
|
|
format = AL.FORMAT_MONO8;
|
|
}
|
|
else if (parent.buffer.bitsPerSample == 16)
|
|
{
|
|
format = AL.FORMAT_MONO16;
|
|
}
|
|
}
|
|
else if (parent.buffer.channels == 2)
|
|
{
|
|
if (parent.buffer.bitsPerSample == 8)
|
|
{
|
|
format = AL.FORMAT_STEREO8;
|
|
}
|
|
else if (parent.buffer.bitsPerSample == 16)
|
|
{
|
|
format = AL.FORMAT_STEREO16;
|
|
}
|
|
}
|
|
|
|
if (parent.buffer.__srcVorbisFile != null)
|
|
{
|
|
stream = true;
|
|
|
|
var vorbisFile = parent.buffer.__srcVorbisFile;
|
|
pcmTotal = vorbisFile.pcmTotal();
|
|
var info = vorbisFile.info();
|
|
sampleRate = info.rate;
|
|
var _dataLength = pcmTotal * Int64.ofInt(parent.buffer.channels) * (Int64.ofInt(parent.buffer.bitsPerSample) / Int64.ofInt(8));
|
|
dataLength = Int64.toInt(_dataLength);
|
|
|
|
buffers = new Array();
|
|
bufferTimeBlocks = new Array();
|
|
|
|
for (i in 0...STREAM_NUM_BUFFERS)
|
|
{
|
|
buffers.push(AL.createBuffer());
|
|
bufferTimeBlocks.push(0);
|
|
}
|
|
|
|
handle = AL.createSource();
|
|
|
|
samples = Int64.toInt(pcmTotal);
|
|
}
|
|
else
|
|
{
|
|
if (parent.buffer.__srcBuffer == null)
|
|
{
|
|
parent.buffer.__srcBuffer = AL.createBuffer();
|
|
|
|
if (parent.buffer.__srcBuffer != null)
|
|
{
|
|
AL.bufferData(parent.buffer.__srcBuffer, format, parent.buffer.data, parent.buffer.data.length, parent.buffer.sampleRate);
|
|
}
|
|
}
|
|
|
|
dataLength = parent.buffer.data.length;
|
|
|
|
handle = AL.createSource();
|
|
|
|
if (handle != null)
|
|
{
|
|
AL.sourcei(handle, AL.BUFFER, parent.buffer.__srcBuffer);
|
|
}
|
|
|
|
samples = Int64.toInt((Int64.make(0, dataLength) * Int64.ofInt(8)) / Int64.ofInt(parent.buffer.channels * parent.buffer.bitsPerSample));
|
|
}
|
|
|
|
initBuffers.push(this);
|
|
}
|
|
|
|
public function play():Void
|
|
{
|
|
if (disposed) {
|
|
trace("TRIED TO PLAY DISPOSED?? WTFFF");
|
|
trace(haxe.CallStack.toString(haxe.CallStack.callStack()));
|
|
}
|
|
if (playing || handle == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
playing = true;
|
|
|
|
if (stream)
|
|
{
|
|
setCurrentTime(getCurrentTime());
|
|
|
|
streamTimer = new Timer(STREAM_TIMER_FREQUENCY);
|
|
streamTimer.run = streamTimer_onRun;
|
|
}
|
|
else
|
|
{
|
|
var time = completed ? 0 : getCurrentTime();
|
|
|
|
setCurrentTime(time);
|
|
}
|
|
}
|
|
|
|
public function pause():Void
|
|
{
|
|
playing = false;
|
|
|
|
if (handle == null) return;
|
|
AL.sourcePause(handle);
|
|
|
|
if (streamTimer != null)
|
|
{
|
|
streamTimer.stop();
|
|
}
|
|
|
|
if (timer != null)
|
|
{
|
|
timer.stop();
|
|
}
|
|
}
|
|
|
|
private function readVorbisFileBuffer(vorbisFile:VorbisFile, length:Int):UInt8Array
|
|
{
|
|
#if lime_vorbis
|
|
var buffer = new UInt8Array(length);
|
|
var read = 0, total = 0, readMax;
|
|
|
|
for (i in 0...STREAM_NUM_BUFFERS-1)
|
|
{
|
|
bufferTimeBlocks[i] = bufferTimeBlocks[i + 1];
|
|
}
|
|
bufferTimeBlocks[STREAM_NUM_BUFFERS-1] = vorbisFile.timeTell();
|
|
|
|
while (total < length)
|
|
{
|
|
readMax = 4096;
|
|
|
|
if (readMax > length - total)
|
|
{
|
|
readMax = length - total;
|
|
}
|
|
|
|
read = vorbisFile.read(buffer.buffer, total, readMax);
|
|
|
|
if (read > 0)
|
|
{
|
|
total += read;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
#else
|
|
return null;
|
|
#end
|
|
}
|
|
|
|
private function refillBuffers(buffers:Array<ALBuffer> = null):Void
|
|
{
|
|
#if lime_vorbis
|
|
var vorbisFile = null;
|
|
var position = 0;
|
|
|
|
if (buffers == null)
|
|
{
|
|
var buffersProcessed:Int = AL.getSourcei(handle, AL.BUFFERS_PROCESSED);
|
|
|
|
if (buffersProcessed > 0)
|
|
{
|
|
vorbisFile = parent.buffer.__srcVorbisFile;
|
|
position = Int64.toInt(vorbisFile.pcmTell());
|
|
|
|
if (position < dataLength)
|
|
{
|
|
buffers = AL.sourceUnqueueBuffers(handle, buffersProcessed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buffers != null)
|
|
{
|
|
if (vorbisFile == null)
|
|
{
|
|
vorbisFile = parent.buffer.__srcVorbisFile;
|
|
position = Int64.toInt(vorbisFile.pcmTell());
|
|
}
|
|
|
|
var numBuffers = 0;
|
|
var data;
|
|
|
|
for (buffer in buffers)
|
|
{
|
|
if (dataLength - position >= STREAM_BUFFER_SIZE)
|
|
{
|
|
data = readVorbisFileBuffer(vorbisFile, STREAM_BUFFER_SIZE);
|
|
AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate);
|
|
position += STREAM_BUFFER_SIZE;
|
|
numBuffers++;
|
|
}
|
|
else if (position < dataLength)
|
|
{
|
|
data = readVorbisFileBuffer(vorbisFile, dataLength - position);
|
|
AL.bufferData(buffer, format, data, data.length, parent.buffer.sampleRate);
|
|
numBuffers++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
AL.sourceQueueBuffers(handle, numBuffers, buffers);
|
|
|
|
// OpenAL can unexpectedly stop playback if the buffers run out
|
|
// of data, which typically happens if an operation (such as
|
|
// resizing a window) freezes the main thread.
|
|
// If AL is supposed to be playing but isn't, restart it here.
|
|
if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED)
|
|
{
|
|
AL.sourcePlay(handle);
|
|
}
|
|
}
|
|
#end
|
|
}
|
|
|
|
public function stop():Void
|
|
{
|
|
if (playing && handle != null && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING)
|
|
{
|
|
AL.sourceStop(handle);
|
|
}
|
|
|
|
playing = false;
|
|
|
|
if (streamTimer != null)
|
|
{
|
|
streamTimer.stop();
|
|
}
|
|
|
|
if (timer != null)
|
|
{
|
|
timer.stop();
|
|
}
|
|
|
|
setCurrentTime(0);
|
|
}
|
|
|
|
// Event Handlers
|
|
private function streamTimer_onRun():Void
|
|
{
|
|
refillBuffers();
|
|
}
|
|
|
|
private function timer_onRun():Void
|
|
{
|
|
if (loops > 0)
|
|
{
|
|
playing = false;
|
|
loops--;
|
|
setCurrentTime(0);
|
|
play();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
stop();
|
|
}
|
|
|
|
completed = true;
|
|
parent.onComplete.dispatch();
|
|
}
|
|
|
|
// Get & Set Methods
|
|
public function getCurrentTime():Float
|
|
{
|
|
if (completed)
|
|
{
|
|
return getLength();
|
|
}
|
|
else if (handle != null)
|
|
{
|
|
if (stream)
|
|
{
|
|
var time = (Std.int(bufferTimeBlocks[0] * 1000) + Std.int(AL.getSourcef(handle, AL.SEC_OFFSET) * 1000)) - parent.offset;
|
|
if (time < 0) return 0;
|
|
return time;
|
|
}
|
|
else
|
|
{
|
|
var offset:Int = AL.getSourcei(handle, AL.BYTE_OFFSET);
|
|
var ratio = (offset / dataLength);
|
|
var totalSeconds = samples / parent.buffer.sampleRate;
|
|
|
|
var time = totalSeconds * ratio * 1000 - parent.offset;
|
|
|
|
// var time = Std.int (AL.getSourcef (handle, AL.SEC_OFFSET) * 1000) - parent.offset;
|
|
if (time < 0) return 0;
|
|
return time;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public function setCurrentTime(value:Float):Float
|
|
{
|
|
// `setCurrentTime()` has side effects and is never safe to skip.
|
|
/* if (value == getCurrentTime())
|
|
{
|
|
return value;
|
|
} */
|
|
|
|
if (handle != null)
|
|
{
|
|
if (stream)
|
|
{
|
|
AL.sourceStop(handle);
|
|
|
|
parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000);
|
|
AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS);
|
|
refillBuffers(buffers);
|
|
|
|
if (playing) AL.sourcePlay(handle);
|
|
}
|
|
else if (parent.buffer != null)
|
|
{
|
|
AL.sourceRewind(handle);
|
|
|
|
// AL.sourcef (handle, AL.SEC_OFFSET, (value + parent.offset) / 1000);
|
|
|
|
var secondOffset = (value + parent.offset) / 1000;
|
|
var totalSeconds = samples / parent.buffer.sampleRate;
|
|
|
|
if (secondOffset < 0) secondOffset = 0;
|
|
else if (secondOffset > totalSeconds) secondOffset = totalSeconds;
|
|
|
|
var ratio = (secondOffset / totalSeconds);
|
|
var totalOffset = Std.int(dataLength * ratio);
|
|
|
|
AL.sourcei(handle, AL.BYTE_OFFSET, totalOffset);
|
|
if (playing) AL.sourcePlay(handle);
|
|
}
|
|
}
|
|
|
|
if (playing)
|
|
{
|
|
if (timer != null)
|
|
{
|
|
timer.stop();
|
|
}
|
|
|
|
var timeRemaining = (getLength() - value) / getPitch();
|
|
|
|
if (timeRemaining > 0)
|
|
{
|
|
completed = false;
|
|
timer = new Timer(timeRemaining);
|
|
timer.run = timer_onRun;
|
|
}
|
|
else
|
|
{
|
|
playing = false;
|
|
completed = true;
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
public function getGain():Float
|
|
{
|
|
if (handle != null)
|
|
{
|
|
return AL.getSourcef(handle, AL.GAIN);
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
public function setGain(value:Float):Float
|
|
{
|
|
if (handle != null)
|
|
{
|
|
AL.sourcef(handle, AL.GAIN, value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
public function getLength():Float
|
|
{
|
|
if (length != null)
|
|
{
|
|
return length;
|
|
}
|
|
|
|
return (samples / parent.buffer.sampleRate * 1000) - parent.offset;
|
|
}
|
|
|
|
public function setLength(value:Float):Float
|
|
{
|
|
if (playing && length != value)
|
|
{
|
|
if (timer != null)
|
|
{
|
|
timer.stop();
|
|
}
|
|
|
|
var timeRemaining = (value - getCurrentTime()) / getPitch();
|
|
|
|
if (timeRemaining > 0)
|
|
{
|
|
timer = new Timer(timeRemaining);
|
|
timer.run = timer_onRun;
|
|
}
|
|
}
|
|
|
|
return length = value;
|
|
}
|
|
|
|
public function getLoops():Int
|
|
{
|
|
return loops;
|
|
}
|
|
|
|
public function setLoops(value:Int):Int
|
|
{
|
|
return loops = value;
|
|
}
|
|
|
|
public function getPitch():Float
|
|
{
|
|
if (handle != null)
|
|
{
|
|
return AL.getSourcef(handle, AL.PITCH);
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
public function setPitch(value:Float):Float
|
|
{
|
|
if (playing && value != getPitch())
|
|
{
|
|
if (timer != null)
|
|
{
|
|
timer.stop();
|
|
}
|
|
|
|
var timeRemaining = (getLength() - getCurrentTime()) / value;
|
|
|
|
if (timeRemaining > 0)
|
|
{
|
|
timer = new Timer(timeRemaining);
|
|
timer.run = timer_onRun;
|
|
}
|
|
}
|
|
|
|
if (handle != null)
|
|
{
|
|
AL.sourcef(handle, AL.PITCH, value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
public function getPosition():Vector4
|
|
{
|
|
if (handle != null)
|
|
{
|
|
#if !emscripten
|
|
var value = AL.getSource3f(handle, AL.POSITION);
|
|
position.x = value[0];
|
|
position.y = value[1];
|
|
position.z = value[2];
|
|
#end
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
public function setPosition(value:Vector4):Vector4
|
|
{
|
|
position.x = value.x;
|
|
position.y = value.y;
|
|
position.z = value.z;
|
|
position.w = value.w;
|
|
|
|
if (handle != null)
|
|
{
|
|
AL.distanceModel(AL.NONE);
|
|
AL.source3f(handle, AL.POSITION, position.x, position.y, position.z);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
}
|