Before anyone asks why vs cam, the code is public, plus i helped most of the audio backend rewrite in the mod too. :P - Static sounds can now also seamlessly loop with AL_SOFT_loop_points extension - Panning stereo sounds is now possible with AL_EXT_STEREO_ANGLES extension (although this was already implemented before) - Improved streaming sounds, should fix most of the lagspike issues - Streaming sounds should no longer regenrate if it's in the nearest timestamp (not for reversing though) ! You still can't access FlxSound.latency because of the new procedures that's not available in lime If there's any crashes with this pls report an issue asap.
519 lines
13 KiB
Haxe
519 lines
13 KiB
Haxe
package lime.media;
|
|
|
|
import haxe.io.Bytes;
|
|
import haxe.io.Path;
|
|
import lime._internal.backend.native.NativeCFFI;
|
|
import lime._internal.format.Base64;
|
|
import lime.app.Future;
|
|
import lime.app.Promise;
|
|
import lime.media.openal.AL;
|
|
import lime.media.openal.ALBuffer;
|
|
#if lime_vorbis
|
|
import lime.media.vorbis.Vorbis;
|
|
import lime.media.vorbis.VorbisFile;
|
|
#end
|
|
import lime.net.HTTPRequest;
|
|
import lime.utils.Log;
|
|
import lime.utils.UInt8Array;
|
|
#if lime_howlerjs
|
|
import lime.media.howlerjs.Howl;
|
|
#end
|
|
#if (js && html5)
|
|
import js.html.Audio;
|
|
#elseif flash
|
|
import flash.media.Sound;
|
|
import flash.net.URLRequest;
|
|
#end
|
|
|
|
@:access(lime._internal.backend.native.NativeCFFI)
|
|
@:access(lime.utils.Assets)
|
|
#if hl
|
|
@:keep
|
|
#end
|
|
#if !lime_debug
|
|
@:fileXml('tags="haxe,release"')
|
|
@:noDebug
|
|
#end
|
|
|
|
/**
|
|
The `AudioBuffer` class represents a buffer of audio data that can be played back using an `AudioSource`.
|
|
It supports a variety of audio formats and platforms, providing a consistent API for loading and managing audio data.
|
|
|
|
Depending on the platform, the audio backend may differ, but the class provides a unified interface for accessing
|
|
audio data, whether it's stored in memory, loaded from a file, or streamed.
|
|
|
|
@see lime.media.AudioSource
|
|
**/
|
|
class AudioBuffer
|
|
{
|
|
/**
|
|
The number of bits per sample in the audio data.
|
|
**/
|
|
public var bitsPerSample:Int;
|
|
|
|
/**
|
|
The number of audio channels (e.g., 1 for mono, 2 for stereo).
|
|
**/
|
|
public var channels:Int;
|
|
|
|
/**
|
|
The raw audio data stored as a `UInt8Array`.
|
|
**/
|
|
public var data:UInt8Array;
|
|
|
|
/**
|
|
The sample rate of the audio data, in Hz.
|
|
**/
|
|
public var sampleRate:Int;
|
|
|
|
/**
|
|
The source of the audio data. This can be an `Audio`, `Sound`, `Howl`, or other platform-specific object.
|
|
**/
|
|
public var src(get, set):Dynamic;
|
|
|
|
@:noCompletion private var __srcAudio:#if (js && html5) Audio #else Dynamic #end;
|
|
@:noCompletion private var __srcBuffer:#if lime_cffi ALBuffer #else Dynamic #end;
|
|
@:noCompletion private var __srcCustom:Dynamic;
|
|
@:noCompletion private var __srcHowl:#if lime_howlerjs Howl #else Dynamic #end;
|
|
@:noCompletion private var __srcSound:#if flash Sound #else Dynamic #end;
|
|
@:noCompletion private var __srcVorbisFile:#if lime_vorbis VorbisFile #else Dynamic #end;
|
|
|
|
#if commonjs
|
|
private static function __init__()
|
|
{
|
|
var p = untyped AudioBuffer.prototype;
|
|
untyped Object.defineProperties(p,
|
|
{
|
|
"src": {get: p.get_src, set: p.set_src}
|
|
});
|
|
}
|
|
#end
|
|
|
|
/**
|
|
Creates a new, empty `AudioBuffer` instance.
|
|
**/
|
|
public function new() {}
|
|
|
|
/**
|
|
Disposes of the resources used by this `AudioBuffer`, such as unloading any associated audio data.
|
|
**/
|
|
public function dispose():Void
|
|
{
|
|
#if (js && html5 && lime_howlerjs)
|
|
if (__srcHowl != null) __srcHowl.unload();
|
|
__srcHowl = null;
|
|
#end
|
|
#if lime_cffi
|
|
if (__srcBuffer != null) {
|
|
AL.bufferData(__srcBuffer, 0, null, 0, 0);
|
|
AL.deleteBuffer(__srcBuffer);
|
|
}
|
|
__srcBuffer = null;
|
|
#end
|
|
#if lime_vorbis
|
|
if (__srcVorbisFile != null) __srcVorbisFile.clear();
|
|
__srcVorbisFile = null;
|
|
#end
|
|
}
|
|
|
|
/**
|
|
Creates an `AudioBuffer` from a Base64-encoded string.
|
|
|
|
@param base64String The Base64-encoded audio data.
|
|
@return An `AudioBuffer` instance with the decoded audio data.
|
|
**/
|
|
public static function fromBase64(base64String:String):AudioBuffer
|
|
{
|
|
if (base64String == null) return null;
|
|
|
|
#if (js && html5 && lime_howlerjs)
|
|
// if base64String doesn't contain codec data, add it.
|
|
if (base64String.indexOf(",") == -1)
|
|
{
|
|
base64String = "data:" + __getCodec(Base64.decode(base64String)) + ";base64," + base64String;
|
|
}
|
|
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.src = new Howl({src: [base64String], preload: false});
|
|
return audioBuffer;
|
|
#elseif (lime_cffi && !macro)
|
|
#if !cs
|
|
// if base64String contains codec data, strip it then decode it.
|
|
var base64StringSplit = base64String.split(",");
|
|
var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1];
|
|
var bytes:Bytes = Base64.decode(base64StringNoEncoding);
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
|
|
|
|
return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer);
|
|
#else
|
|
// if base64String contains codec data, strip it then decode it.
|
|
var base64StringSplit = base64String.split(",");
|
|
var base64StringNoEncoding = base64StringSplit[base64StringSplit.length - 1];
|
|
var bytes:Bytes = Base64.decode(base64StringNoEncoding);
|
|
var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null);
|
|
|
|
if (data != null)
|
|
{
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.bitsPerSample = data.bitsPerSample;
|
|
audioBuffer.channels = data.channels;
|
|
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
|
|
audioBuffer.sampleRate = data.sampleRate;
|
|
return audioBuffer;
|
|
}
|
|
#end
|
|
#end
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
Creates an `AudioBuffer` from a `Bytes` object.
|
|
|
|
@param bytes The `Bytes` object containing the audio data.
|
|
@return An `AudioBuffer` instance with the decoded audio data.
|
|
**/
|
|
public static function fromBytes(bytes:Bytes):AudioBuffer
|
|
{
|
|
if (bytes == null) return null;
|
|
|
|
#if (js && html5 && lime_howlerjs)
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.src = new Howl({src: ["data:" + __getCodec(bytes) + ";base64," + Base64.encode(bytes)], preload: false});
|
|
|
|
return audioBuffer;
|
|
#elseif (lime_cffi && !macro)
|
|
#if lime_vorbis
|
|
var vorbisFile = VorbisFile.fromBytes(bytes);
|
|
if (vorbisFile != null) return fromVorbisFile(vorbisFile);
|
|
#end
|
|
#if !cs
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
|
|
|
|
return NativeCFFI.lime_audio_load_bytes(bytes, audioBuffer);
|
|
#else
|
|
var data:Dynamic = NativeCFFI.lime_audio_load_bytes(bytes, null);
|
|
|
|
if (data != null)
|
|
{
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.bitsPerSample = data.bitsPerSample;
|
|
audioBuffer.channels = data.channels;
|
|
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
|
|
audioBuffer.sampleRate = data.sampleRate;
|
|
return audioBuffer;
|
|
}
|
|
#end
|
|
#end
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
Creates an `AudioBuffer` from a file.
|
|
|
|
@param path The file path to the audio data.
|
|
@return An `AudioBuffer` instance with the audio data loaded from the file.
|
|
**/
|
|
public static function fromFile(path:String #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer
|
|
{
|
|
if (path == null) return null;
|
|
|
|
#if (js && html5 && lime_howlerjs)
|
|
var audioBuffer = new AudioBuffer();
|
|
|
|
#if force_html5_audio
|
|
audioBuffer.__srcHowl = new Howl({src: [path], html5: true, preload: false});
|
|
#else
|
|
audioBuffer.__srcHowl = new Howl({src: [path], html5: howlHtml5, preload: false});
|
|
#end
|
|
|
|
return audioBuffer;
|
|
#elseif flash
|
|
switch (Path.extension(path))
|
|
{
|
|
case "ogg", "wav":
|
|
return null;
|
|
default:
|
|
}
|
|
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.__srcSound = new Sound(new URLRequest(path));
|
|
return audioBuffer;
|
|
#elseif (lime_cffi && !macro)
|
|
#if !cs
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.data = new UInt8Array(Bytes.alloc(0));
|
|
|
|
//audioBuffer = NativeCFFI.lime_audio_load_file(path, audioBuffer);
|
|
//if (audioBuffer != null) audioBuffer.initBuffer();
|
|
//return audioBuffer;
|
|
return NativeCFFI.lime_audio_load_file(path, audioBuffer);
|
|
#else
|
|
var data:Dynamic = NativeCFFI.lime_audio_load_file(path, null);
|
|
|
|
if (data != null)
|
|
{
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.bitsPerSample = data.bitsPerSample;
|
|
audioBuffer.channels = data.channels;
|
|
audioBuffer.data = new UInt8Array(@:privateAccess new Bytes(data.data.length, data.data.b));
|
|
audioBuffer.sampleRate = data.sampleRate;
|
|
//audioBuffer.initBuffer();
|
|
return audioBuffer;
|
|
}
|
|
|
|
return null;
|
|
#end
|
|
#else
|
|
return null;
|
|
#end
|
|
}
|
|
|
|
/**
|
|
Creates an `AudioBuffer` from an array of file paths.
|
|
|
|
@param paths An array of file paths to search for audio data.
|
|
@return An `AudioBuffer` instance with the audio data loaded from the first valid file found.
|
|
**/
|
|
public static function fromFiles(paths:Array<String> #if (js && html5 && lime_howlerjs), ?howlHtml5 = false #end):AudioBuffer
|
|
{
|
|
#if (js && html5 && lime_howlerjs)
|
|
var audioBuffer = new AudioBuffer();
|
|
|
|
#if force_html5_audio
|
|
audioBuffer.__srcHowl = new Howl({src: paths, html5: true, preload: false});
|
|
#else
|
|
audioBuffer.__srcHowl = new Howl({src: paths, html5: howlHtml5, preload: false});
|
|
#end
|
|
|
|
return audioBuffer;
|
|
#else
|
|
var buffer = null;
|
|
|
|
for (path in paths)
|
|
{
|
|
buffer = AudioBuffer.fromFile(path);
|
|
if (buffer != null) break;
|
|
}
|
|
|
|
return buffer;
|
|
#end
|
|
}
|
|
|
|
/**
|
|
Creates an `AudioBuffer` from a `VorbisFile`.
|
|
|
|
@param vorbisFile The `VorbisFile` object containing the audio data.
|
|
@return An `AudioBuffer` instance with the decoded audio data.
|
|
**/
|
|
#if lime_vorbis
|
|
|
|
public static function fromVorbisFile(vorbisFile:VorbisFile):AudioBuffer
|
|
{
|
|
if (vorbisFile == null) return null;
|
|
|
|
var info = vorbisFile.info();
|
|
|
|
var audioBuffer = new AudioBuffer();
|
|
audioBuffer.channels = info.channels;
|
|
audioBuffer.sampleRate = info.rate;
|
|
audioBuffer.bitsPerSample = 16;
|
|
|
|
final pcmTotal = vorbisFile.pcmTotal(-1);
|
|
if (!vorbisFile.seekable() || pcmTotal < (audioBuffer.sampleRate << 2)) {
|
|
vorbisFile.rawSeek(0);
|
|
|
|
final isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN;
|
|
final bytes = Bytes.alloc(Std.int((pcmTotal.high * 4294967296. + (pcmTotal.low >> 0)) * info.channels * (audioBuffer.bitsPerSample >> 3)));
|
|
var total = 0, result = 0;
|
|
do {
|
|
result = vorbisFile.read(bytes, total, 0x1000, isBigEndian, 2, true);
|
|
total += result;
|
|
} while (result > 0 || result == Vorbis.HOLE);
|
|
|
|
audioBuffer.data = new UInt8Array(bytes);
|
|
vorbisFile.clear();
|
|
}
|
|
else
|
|
audioBuffer.__srcVorbisFile = vorbisFile;
|
|
|
|
return audioBuffer;
|
|
}
|
|
#else
|
|
public static function fromVorbisFile(vorbisFile:Dynamic):AudioBuffer
|
|
{
|
|
return null;
|
|
}
|
|
#end
|
|
|
|
/**
|
|
Asynchronously loads an `AudioBuffer` from a file.
|
|
|
|
@param path The file path to the audio data.
|
|
@return A `Future` that resolves to the loaded `AudioBuffer`.
|
|
**/
|
|
public static function loadFromFile(path:String):Future<AudioBuffer>
|
|
{
|
|
#if (flash || (js && html5))
|
|
var promise = new Promise<AudioBuffer>();
|
|
|
|
var audioBuffer = AudioBuffer.fromFile(path);
|
|
|
|
if (audioBuffer != null)
|
|
{
|
|
#if flash
|
|
audioBuffer.__srcSound.addEventListener(flash.events.Event.COMPLETE, function(event)
|
|
{
|
|
promise.complete(audioBuffer);
|
|
});
|
|
|
|
audioBuffer.__srcSound.addEventListener(flash.events.ProgressEvent.PROGRESS, function(event)
|
|
{
|
|
promise.progress(Std.int(event.bytesLoaded), Std.int(event.bytesTotal));
|
|
});
|
|
|
|
audioBuffer.__srcSound.addEventListener(flash.events.IOErrorEvent.IO_ERROR, promise.error);
|
|
#elseif (js && html5 && lime_howlerjs)
|
|
if (audioBuffer != null)
|
|
{
|
|
audioBuffer.__srcHowl.on("load", function()
|
|
{
|
|
promise.complete(audioBuffer);
|
|
});
|
|
|
|
audioBuffer.__srcHowl.on("loaderror", function(id, msg)
|
|
{
|
|
promise.error(msg);
|
|
});
|
|
|
|
audioBuffer.__srcHowl.load();
|
|
}
|
|
#else
|
|
promise.complete(audioBuffer);
|
|
#end
|
|
}
|
|
else
|
|
{
|
|
promise.error(null);
|
|
}
|
|
|
|
return promise.future;
|
|
#else
|
|
// TODO: Streaming
|
|
|
|
var request = new HTTPRequest<AudioBuffer>();
|
|
return request.load(path).then(function(buffer)
|
|
{
|
|
if (buffer != null)
|
|
{
|
|
return Future.withValue(buffer);
|
|
}
|
|
else
|
|
{
|
|
return cast Future.withError("");
|
|
}
|
|
});
|
|
#end
|
|
}
|
|
|
|
/**
|
|
Asynchronously loads an `AudioBuffer` from multiple files.
|
|
|
|
@param paths An array of file paths to search for audio data.
|
|
@return A `Future` that resolves to the loaded `AudioBuffer`.
|
|
**/
|
|
public static function loadFromFiles(paths:Array<String>):Future<AudioBuffer>
|
|
{
|
|
#if (js && html5 && lime_howlerjs)
|
|
var promise = new Promise<AudioBuffer>();
|
|
|
|
var audioBuffer = AudioBuffer.fromFiles(paths);
|
|
|
|
if (audioBuffer != null)
|
|
{
|
|
audioBuffer.__srcHowl.on("load", function()
|
|
{
|
|
promise.complete(audioBuffer);
|
|
});
|
|
|
|
audioBuffer.__srcHowl.on("loaderror", function()
|
|
{
|
|
promise.error(null);
|
|
});
|
|
|
|
audioBuffer.__srcHowl.load();
|
|
}
|
|
else
|
|
{
|
|
promise.error(null);
|
|
}
|
|
|
|
return promise.future;
|
|
#else
|
|
return new Future(fromFiles.bind(paths), true);
|
|
#end
|
|
}
|
|
|
|
private static function __getCodec(bytes:Bytes):String
|
|
{
|
|
var signature = bytes.getString(0, 4);
|
|
|
|
switch (signature)
|
|
{
|
|
case "OggS":
|
|
return "audio/ogg";
|
|
case "fLaC":
|
|
return "audio/flac";
|
|
case "RIFF" if (bytes.getString(8, 4) == "WAVE"):
|
|
return "audio/wav";
|
|
default:
|
|
switch ([bytes.get(0), bytes.get(1), bytes.get(2)])
|
|
{
|
|
case [73, 68, 51] | [255, 251, _] | [255, 250, _] | [255, 243, _]: return "audio/mp3";
|
|
default:
|
|
}
|
|
}
|
|
|
|
Log.error("Unsupported sound format");
|
|
return null;
|
|
}
|
|
|
|
// Get & Set Methods
|
|
@:noCompletion private function get_src():Dynamic
|
|
{
|
|
#if (js && html5)
|
|
#if lime_howlerjs
|
|
return __srcHowl;
|
|
#else
|
|
return __srcAudio;
|
|
#end
|
|
#elseif flash
|
|
return __srcSound;
|
|
#elseif lime_vorbis
|
|
return __srcVorbisFile;
|
|
#else
|
|
return __srcCustom;
|
|
#end
|
|
}
|
|
|
|
@:noCompletion private function set_src(value:Dynamic):Dynamic
|
|
{
|
|
#if (js && html5)
|
|
#if lime_howlerjs
|
|
return __srcHowl = value;
|
|
#else
|
|
return __srcAudio = value;
|
|
#end
|
|
#elseif flash
|
|
return __srcSound = value;
|
|
#elseif lime_vorbis
|
|
return __srcVorbisFile = value;
|
|
#else
|
|
return __srcCustom = value;
|
|
#end
|
|
}
|
|
}
|