Files
CodenameEngine/source/lime/_internal/backend/native/NativeAudioSource.hx
T
NeeEoo c4a50908d4 Support for 50 minute songs (html5 untested))
Experimental support for 3+ hour long songs comes later
2023-01-13 04:22:00 +01:00

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;
}
}