Streamed Music (Rewritten Internal Audio) PR Merge
- Reworked Internal Audio (FlxSound, NativeAudioSource, SoundChannel) to work with Streaming Vorbis Files (for now?) - FlxSound.amplitude now works. (This is implemented to avoid the lagspike on using the AudioAnalyzer on streamed audios as possible) - Implemented loopTime, and endTime into the internal instead of FlxSound for better looping (only for Streamed Audio) - Implemented Interpolation into time (only in FlxSound specifically, does not affect the actual internal audio classes) - Implemented variables such as offset, muted, pan, proximityPan, channels, stereo, loaded, streamed, onFinish (to seperate complete callback from looping callback), timeScaleBased (for pitch based in FlxG.timeScale) - Reworked AudioAnalyzer to work much more faster with all type of audios (hopefully) and work with Streamed Audio. (NOTE: Using it on Streamed Audio is pretty expensive as it constantly seeking each calls, unless if the next time using it is the expected time decoder to seek to.) - Redo AudioSwitchFix to work in time instead of when switching states. - Improved the off-sync vocals detection - Implemented LoopTime, EndTime, Offset into Ini's Music Assets
This commit is contained in:
@@ -0,0 +1,917 @@
|
||||
package flixel.sound;
|
||||
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.media.AudioSource;
|
||||
import lime.media.AudioManager;
|
||||
|
||||
import openfl.Assets;
|
||||
import openfl.events.IEventDispatcher;
|
||||
import openfl.events.Event;
|
||||
import openfl.media.Sound;
|
||||
import openfl.media.SoundChannel;
|
||||
import openfl.media.SoundTransform;
|
||||
import openfl.media.SoundMixer;
|
||||
import openfl.net.URLRequest;
|
||||
import openfl.utils.AssetType;
|
||||
#if flash11
|
||||
import openfl.utils.ByteArray;
|
||||
#end
|
||||
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.util.FlxStringUtil;
|
||||
import flixel.FlxBasic;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxG;
|
||||
|
||||
@:access(flixel.FlxGame)
|
||||
class FlxSound extends FlxBasic {
|
||||
/**
|
||||
* The x position of this sound in world coordinates.
|
||||
* Only really matters if you are doing proximity/panning stuff.
|
||||
*/
|
||||
public var x:Float;
|
||||
|
||||
/**
|
||||
* The y position of this sound in world coordinates.
|
||||
* Only really matters if you are doing proximity/panning stuff.
|
||||
*/
|
||||
public var y:Float;
|
||||
|
||||
/**
|
||||
* Whether or not this sound should be automatically destroyed when you switch states.
|
||||
*/
|
||||
public var persist:Bool;
|
||||
|
||||
/**
|
||||
* Whether or not the sound is currently playing.
|
||||
*/
|
||||
public var playing(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Set volume to a value between 0 and 1* to change how this sound is.
|
||||
*/
|
||||
public var volume(get, set):Float;
|
||||
|
||||
/**
|
||||
* Whether to make this sound muted or not.
|
||||
*/
|
||||
public var muted(default, set):Bool;
|
||||
|
||||
#if FLX_PITCH
|
||||
/**
|
||||
* Set pitch, which also alters the playback speed. Default is 1.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
/**
|
||||
* Alters the pitch of the sound depends on the current FlxG.timeScale. Default is true.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var timeScaleBased:Bool;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this.
|
||||
*/
|
||||
public var pan(get, set):Float;
|
||||
|
||||
/**
|
||||
* The position in runtime of the music playback in milliseconds.
|
||||
* If set while paused, changes only come into effect after a `resume()` call.
|
||||
*/
|
||||
public var time(get, set):Float;
|
||||
|
||||
/**
|
||||
* The offset for this FlxSound.
|
||||
* Useful for just generally offsetting this sound without affecting time.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var offset(get, set):Float;
|
||||
|
||||
/**
|
||||
* The length of the sound in milliseconds.
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public var length(get, never):Float;
|
||||
|
||||
/**
|
||||
* The latency of the sound in milliseconds.
|
||||
* @since raltyMod
|
||||
*/
|
||||
//public var latency(get, never):Float;
|
||||
|
||||
/**
|
||||
* Whether or not this sound should loop.
|
||||
*/
|
||||
public var looped(default, set):Bool;
|
||||
|
||||
/**
|
||||
* In case of looping, the point (in milliseconds) from where to restart the sound when it loops back
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public var loopTime(default, set):Float;
|
||||
|
||||
/**
|
||||
* At which point to stop playing the sound, in milliseconds.
|
||||
* If not set / `null`, the sound completes normally.
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public var endTime(default, set):Null<Float>;
|
||||
|
||||
/**
|
||||
* The sound's "target" (for proximity and panning).
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var target:Null<FlxObject>;
|
||||
|
||||
/**
|
||||
* The maximum effective radius of this sound (for proximity and panning).
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var radius:Float;
|
||||
|
||||
/**
|
||||
* Whether the proximity alters the pan or not
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var proximityPan:Bool;
|
||||
|
||||
/**
|
||||
* Stores for how much channels are in the loaded sound.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var channels(get, never):Int;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is stereo instead of mono.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var stereo(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is loaded yet.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var loaded(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is streamed, only vorbis though.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var streamed(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Stores the average wave amplitude of both stereo channels.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitude(get, never):Float;
|
||||
|
||||
/**
|
||||
* Just the amplitude of the left stereo channel
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitudeLeft(get, never):Float;
|
||||
|
||||
/**
|
||||
* Just the amplitude of the right stereo channel
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitudeRight(get, never):Float;
|
||||
|
||||
/**
|
||||
* The ID3 song name. Defaults to null. Currently only works for MP3 streamed sounds.
|
||||
*/
|
||||
public var name(default, null):String;
|
||||
|
||||
/**
|
||||
* The ID3 artist name. Defaults to null. Currently only works for MP3 streamed sounds.
|
||||
*/
|
||||
public var artist(default, null):String;
|
||||
|
||||
/**
|
||||
* Whether to call `destroy()` when the sound has finished playing.
|
||||
*/
|
||||
public var autoDestroy:Bool;
|
||||
|
||||
/**
|
||||
* Signal that is dispatched on sound complete.
|
||||
* Seperates the looping from onComplete.
|
||||
*/
|
||||
public final onFinish:FlxSignal;
|
||||
|
||||
/**
|
||||
* Tracker for sound complete callback. If assigned, will be called
|
||||
* each time when sound reaches its end.
|
||||
*/
|
||||
//@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.")
|
||||
public var onComplete:Void->Void;
|
||||
|
||||
/**
|
||||
* The sound group this sound belongs to
|
||||
*/
|
||||
public var group(default, set):FlxSoundGroup;
|
||||
|
||||
/**
|
||||
* Stores the sound lime AudioBuffer if exists.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var buffer(get, never):AudioBuffer;
|
||||
|
||||
/**
|
||||
* The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`)
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public var fadeTween:FlxTween;
|
||||
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd.load) var _sound:Sound;
|
||||
var _transform:SoundTransform;
|
||||
var _channel:SoundChannel;
|
||||
var _source:AudioSource;
|
||||
var _paused:Bool;
|
||||
var _volume:Float;
|
||||
var _volumeAdjust:Float;
|
||||
var _time:Float;
|
||||
var _offset:Float;
|
||||
var _timeInterpolation:Float;
|
||||
var _lastTime:Null<Float>; // FlxG.game.getTicks(), in MS
|
||||
var _length:Float;
|
||||
#if FLX_PITCH
|
||||
var _pitch:Float;
|
||||
var _timeScaleAdjust:Float;
|
||||
var _realPitch:Float;
|
||||
#end
|
||||
var _amplitudeLeft:Float;
|
||||
var _amplitudeRight:Float;
|
||||
var _amplitudeUpdate:Bool;
|
||||
var _alreadyPaused:Null<Bool>;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
onFinish = new FlxSignal();
|
||||
revive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this FlxSound properties.
|
||||
*
|
||||
* @param clean Whether if this FlxSound also needs to be cleaned up too.
|
||||
*/
|
||||
public function reset():Void {
|
||||
if (_source != null) stop();
|
||||
x = y = 0;
|
||||
|
||||
muted = false;
|
||||
looped = false;
|
||||
loopTime = 0;
|
||||
endTime = null;
|
||||
autoDestroy = false;
|
||||
visible = false;
|
||||
target = null;
|
||||
radius = 0;
|
||||
proximityPan = true;
|
||||
onComplete = null;
|
||||
timeScaleBased = true;
|
||||
|
||||
_alreadyPaused = null;
|
||||
_cameras = null;
|
||||
_lastTime = null;
|
||||
_paused = false;
|
||||
_offset = _length = _time = 0;
|
||||
_volume = _volumeAdjust = 1;
|
||||
_amplitudeLeft = _amplitudeRight = 0;
|
||||
_amplitudeUpdate = true;
|
||||
#if FLX_PITCH _pitch = _realPitch = _timeScaleAdjust = 1; #end
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal cleanup function for cleaning up this FlxSound.
|
||||
* @since raltyMod
|
||||
*/
|
||||
function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess {
|
||||
active = false;
|
||||
_lastTime = null;
|
||||
|
||||
if (destroySound) {
|
||||
onFinish.removeAll();
|
||||
|
||||
if (group != null) group.remove(this);
|
||||
|
||||
if (_channel != null) {
|
||||
_channel.stop();
|
||||
_channel = null;
|
||||
}
|
||||
if (_source != null) {
|
||||
_source.onComplete.remove(stopped);
|
||||
_source.onLoop.remove(source_looped);
|
||||
_source.dispose();
|
||||
}
|
||||
_source = null;
|
||||
_sound = null;
|
||||
|
||||
reset();
|
||||
}
|
||||
else if (_channel != null && _channel.__isValid) {
|
||||
if (resetPosition) {
|
||||
_source.stop();
|
||||
|
||||
_time = 0;
|
||||
_paused = false;
|
||||
}
|
||||
else if (!_paused) {
|
||||
get_time();
|
||||
_source.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fade out, fade in, panning, proximity, and amplitude operations each frame.
|
||||
*/
|
||||
override function update(elapsed:Float):Void {
|
||||
if (!playing) return;
|
||||
|
||||
var timeScaleTarget = timeScaleBased ? FlxG.timeScale : 1.0;
|
||||
if (_timeScaleAdjust != timeScaleTarget) {
|
||||
_timeScaleAdjust = timeScaleTarget;
|
||||
pitch = _pitch;
|
||||
if (_channel == null) return;
|
||||
}
|
||||
|
||||
_amplitudeUpdate = true;
|
||||
|
||||
var radialMultiplier = 1.0;
|
||||
|
||||
// Distance-based volume control (TODO for Ralty: REDO THIS)
|
||||
if (target != null) {
|
||||
var targetPosition = target.getPosition();
|
||||
radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / radius;
|
||||
targetPosition.put();
|
||||
radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1);
|
||||
|
||||
if (proximityPan && _transform != null) {
|
||||
var d:Float = (x - target.x) / radius;
|
||||
_transform.pan = FlxMath.bound(d, -1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_volumeAdjust = radialMultiplier;
|
||||
updateTransform();
|
||||
}
|
||||
|
||||
override function revive() {
|
||||
reset();
|
||||
super.revive();
|
||||
}
|
||||
|
||||
override function kill() {
|
||||
super.kill();
|
||||
reset();
|
||||
}
|
||||
|
||||
override function destroy() {
|
||||
super.destroy();
|
||||
cleanup(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from an embedded MP3.
|
||||
*
|
||||
* @param embeddedSound An embedded Class object representing an MP3 file.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @param onComplete Called when the sound finished playing
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadEmbedded(embeddedSound:FlxSoundAsset, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
|
||||
if (!exists || embeddedSound == null) return this;
|
||||
cleanup(true);
|
||||
|
||||
if ((embeddedSound is Sound)) _sound = embeddedSound;
|
||||
else if ((embeddedSound is Class)) _sound = Type.createInstance(embeddedSound, []);
|
||||
else if ((embeddedSound is String)) {
|
||||
if (Assets.exists(embeddedSound, AssetType.MUSIC) && this == FlxG.sound.music)
|
||||
_sound = Assets.getMusic(embeddedSound);
|
||||
else if (Assets.exists(embeddedSound, AssetType.SOUND))
|
||||
_sound = Assets.getSound(embeddedSound);
|
||||
else
|
||||
FlxG.log.error('Could not find a Sound asset with an ID of \'$embeddedSound\'.');
|
||||
}
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from a URL.
|
||||
*
|
||||
* @param soundURL A string representing the URL of the MP3 file you want to play.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @param onComplete Called when the sound finished playing
|
||||
* @param onLoad Called when the sound finished loading.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadStream(soundURL:String, looped = false, autoDestroy = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound {
|
||||
if (!exists) return this;
|
||||
cleanup(true);
|
||||
|
||||
_sound = new Sound();
|
||||
_sound.addEventListener(Event.ID3, gotID3);
|
||||
var loadCallback:Event->Void = null;
|
||||
loadCallback = function(e:Event)
|
||||
{
|
||||
(e.target : IEventDispatcher).removeEventListener(e.type, loadCallback);
|
||||
// Check if the sound was destroyed before calling. Weak ref doesn't guarantee GC.
|
||||
if (_sound == e.target)
|
||||
{
|
||||
_length = _sound.length;
|
||||
if (onLoad != null)
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
// Use a weak reference so this can be garbage collected if destroyed before loading.
|
||||
_sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true);
|
||||
_sound.load(new URLRequest(soundURL));
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
|
||||
#if flash11
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from a ByteArray.
|
||||
*
|
||||
* @param bytes A ByteArray object.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadByteArray(bytes:ByteArray, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
|
||||
if (!exists) return this;
|
||||
cleanup(true);
|
||||
|
||||
_sound = new Sound();
|
||||
_sound.addEventListener(Event.ID3, gotID3);
|
||||
_sound.loadCompressedDataFromByteArray(bytes, bytes.length);
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
#end
|
||||
|
||||
function init(?looped:Null<Bool>, ?autoDestroy:Null<Bool>, ?onComplete:Null<Void->Void>):FlxSound {
|
||||
if (looped != null) this.looped = looped;
|
||||
if (autoDestroy != null) this.autoDestroy = autoDestroy;
|
||||
if (onComplete != null) this.onComplete = onComplete;
|
||||
|
||||
if (_sound != null) makeChannel();
|
||||
else _length = 0;
|
||||
|
||||
endTime = null;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call inbetween init function if a sound asset does exists.
|
||||
* @since raltyMod
|
||||
*/
|
||||
function makeChannel() @:privateAccess {
|
||||
if (_sound.__buffer == null) return;
|
||||
if (_source != null) _source.dispose();
|
||||
|
||||
if (_channel == null) (_source = (_channel = new SoundChannel(null)).__source = new AudioSource(_sound.__buffer)).gain = 0;
|
||||
else {
|
||||
(_source = new AudioSource(_sound.__buffer)).gain = 0;
|
||||
_channel.__dispose();
|
||||
_channel.__source = _source;
|
||||
SoundMixer.__registerSoundChannel(_channel);
|
||||
}
|
||||
if (_transform == null) _transform = new SoundTransform();
|
||||
_transform.pan = 0;
|
||||
_length = _source.length;
|
||||
|
||||
_source.onComplete.add(stopped);
|
||||
_source.onLoop.add(source_looped);
|
||||
_channel.__soundTransform = _transform;
|
||||
_channel.__isValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after adjusting the volume to update the sound channel's settings.
|
||||
*/
|
||||
@:allow(flixel.sound.FlxSoundGroup)
|
||||
function updateTransform() {
|
||||
if (_transform == null) {
|
||||
_transform = new SoundTransform();
|
||||
pan = pan;
|
||||
}
|
||||
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || muted) ? 0 : 1) * FlxG.sound.volume * #end
|
||||
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
||||
|
||||
if (_channel != null) _channel.soundTransform = _transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function if you want this sound's volume to change
|
||||
* based on distance from a particular FlxObject.
|
||||
*
|
||||
* @param X The X position of the sound.
|
||||
* @param Y The Y position of the sound.
|
||||
* @param TargetObject The object you want to track.
|
||||
* @param Radius The maximum distance this sound can travel.
|
||||
* @param Pan Whether panning should be used in addition to the volume changes.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function proximity(x = 0.0, y = 0.0, ?targetObject:FlxObject, ?radius:Float, pan = true):FlxSound {
|
||||
setPosition(x, y);
|
||||
if (targetObject != null) this.target = targetObject;
|
||||
if (radius != null) this.radius = radius;
|
||||
proximityPan = pan;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set the coordinates of this object.
|
||||
* Sound positioning is used in conjunction with proximity/panning.
|
||||
*
|
||||
* @param x The new x position
|
||||
* @param y The new y position
|
||||
*/
|
||||
public inline function setPosition(x = 0.0, y = 0.0):Void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to play the sound - also works on paused sounds.
|
||||
*
|
||||
* @param forceRestart Whether to start the sound over or not.
|
||||
* Default value is false, meaning if the sound is already playing or was
|
||||
* paused when you call play(), it will continue playing from its current
|
||||
* position, NOT start again from the beginning.
|
||||
* @param startTime At which point to start playing the sound, in milliseconds.
|
||||
* @param endTime At which point to stop playing the sound, in milliseconds.
|
||||
* If not set / `null`, the sound completes normally.
|
||||
*/
|
||||
public function play(forceRestart = false, startTime = 0.0, ?endTime:Null<Float>):FlxSound {
|
||||
if (!exists) return this;
|
||||
|
||||
if (forceRestart) cleanup(false, true);
|
||||
else if (playing) return this;
|
||||
|
||||
if (endTime != null) this.endTime = endTime;
|
||||
if (_paused) resume();
|
||||
else startSound(startTime);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpause a sound. Only works on sounds that have been paused.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function resume():FlxSound {
|
||||
if (_paused) startSound(_time);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to pause this sound.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function pause():FlxSound {
|
||||
if (!playing) return this;
|
||||
|
||||
cleanup(false, false);
|
||||
_paused = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to stop this sound.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function stop():FlxSound {
|
||||
cleanup(autoDestroy, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that tweens this sound's volume.
|
||||
*
|
||||
* @param duration The amount of time the fade-out operation should take.
|
||||
* @param to The volume to tween to, 0 by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void):FlxSound {
|
||||
if (fadeTween != null) fadeTween.cancel();
|
||||
fadeTween = FlxTween.num(volume, to, duration, {onComplete: onComplete}, volumeTween);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that tweens this sound's volume.
|
||||
* If the sound wasn't playing at all, it'll play before the tween starts.
|
||||
*
|
||||
* @param duration The amount of time the fade-in operation should take.
|
||||
* @param from The volume to tween from, 0 by default.
|
||||
* @param to The volume to tween to, 1 by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void):FlxSound {
|
||||
if (!playing) play();
|
||||
if (fadeTween != null) fadeTween.cancel();
|
||||
fadeTween = FlxTween.num(from, to, duration, {onComplete: onComplete}, volumeTween);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function volumeTween(f:Float) volume = f;
|
||||
|
||||
/**
|
||||
* Returns the currently selected "real" volume of the sound (takes fades and proximity into account).
|
||||
*
|
||||
* @return The adjusted volume of the sound.
|
||||
*/
|
||||
public inline function getActualVolume():Float
|
||||
return _volume * _volumeAdjust;
|
||||
|
||||
#if FLX_PITCH
|
||||
/**
|
||||
* Returns the currently selected "real" pitch of the sound.
|
||||
*
|
||||
* @return The adjusted pitch of the sound.
|
||||
*/
|
||||
public inline function getActualPitch():Float
|
||||
return _realPitch;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Returns the actual time coming from the internal, can be used for detecting sync error.
|
||||
*
|
||||
* @return The actual time of the sound.
|
||||
*/
|
||||
public inline function getActualTime():Float {
|
||||
get_time();
|
||||
return _time;
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal helper function used to attempt to start playing
|
||||
* the sound and populate the _channel variable.
|
||||
*/
|
||||
function startSound(startTime:Float) @:privateAccess {
|
||||
if (_sound == null) return;
|
||||
|
||||
_paused = false;
|
||||
_time = startTime;
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
if (_channel == null || !_channel.__isValid || _source.__backend == null #if lime_cffi || _source.__backend.disposed || _source.__backend.handle == null #end)
|
||||
makeChannel();
|
||||
|
||||
if (_channel != null) {
|
||||
#if FLX_PITCH
|
||||
_timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0;
|
||||
pitch = _pitch;
|
||||
#end
|
||||
|
||||
_channel.soundTransform = _transform;
|
||||
_channel.__lastPeakTime = -10;
|
||||
_channel.__leftPeak = 0;
|
||||
_channel.__rightPeak = 0;
|
||||
|
||||
#if lime_cffi _source.__backend.playing = true; #end
|
||||
_source.offset = 0;
|
||||
_source.currentTime = startTime + _offset;
|
||||
#if !lime_cffi _source.play(); #end
|
||||
|
||||
looped = looped;
|
||||
loopTime = loopTime;
|
||||
endTime = endTime;
|
||||
|
||||
active = true;
|
||||
}
|
||||
else {
|
||||
exists = false;
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopped() {
|
||||
onFinish.dispatch();
|
||||
|
||||
if (onComplete != null) onComplete();
|
||||
|
||||
if (looped) {
|
||||
cleanup(false);
|
||||
play(false, loopTime, endTime);
|
||||
}
|
||||
else cleanup(autoDestroy);
|
||||
}
|
||||
|
||||
function source_looped() {
|
||||
if (onComplete != null) onComplete();
|
||||
|
||||
if (!looped) {
|
||||
cleanup(autoDestroy);
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
_time = loopTime;
|
||||
}
|
||||
else _channel.loops = 999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal event handler for ID3 info (i.e. fetching the song name).
|
||||
*/
|
||||
function gotID3(_) {
|
||||
name = _sound.id3.songName;
|
||||
artist = _sound.id3.artist;
|
||||
_sound.removeEventListener(Event.ID3, gotID3);
|
||||
}
|
||||
|
||||
#if FLX_SOUND_SYSTEM
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd)
|
||||
function onFocus() if (!_alreadyPaused) {
|
||||
resume();
|
||||
_alreadyPaused = null;
|
||||
}
|
||||
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd)
|
||||
function onFocusLost() if (_alreadyPaused == null && !(_alreadyPaused = _paused)) pause();
|
||||
#end
|
||||
|
||||
function set_group(value:FlxSoundGroup):FlxSoundGroup {
|
||||
if (value != null) value.add(this);
|
||||
else group.remove(this);
|
||||
return group;
|
||||
}
|
||||
|
||||
inline function get_playing():Bool @:privateAccess
|
||||
return _channel != null && _channel.__isValid && _source.playing;
|
||||
|
||||
inline function get_volume():Float
|
||||
return _volume;
|
||||
|
||||
inline function set_volume(v:Float):Float {
|
||||
_volume = FlxMath.bound(v, 0, 10);
|
||||
updateTransform();
|
||||
return _volume;
|
||||
}
|
||||
|
||||
inline function set_muted(v:Bool):Bool {
|
||||
muted = v;
|
||||
updateTransform();
|
||||
return muted;
|
||||
}
|
||||
|
||||
inline function get_loaded():Bool
|
||||
return buffer != null;
|
||||
|
||||
inline function get_streamed():Bool
|
||||
@:privateAccess return #if lime_vorbis _sound != null && _sound.__buffer.__srcVorbisFile != null #else false #end;
|
||||
|
||||
inline function get_buffer():AudioBuffer
|
||||
@:privateAccess return _sound != null ? _sound.__buffer : null;
|
||||
|
||||
function update_amplitude():Void @:privateAccess {
|
||||
if (_channel == null || !_channel.__updatePeaks(get_time()) || !_amplitudeUpdate) return;
|
||||
|
||||
_amplitudeUpdate = false;
|
||||
_amplitudeLeft = _channel.__leftPeak;
|
||||
_amplitudeRight = _channel.__rightPeak;
|
||||
}
|
||||
|
||||
inline function get_amplitudeLeft():Float {
|
||||
update_amplitude();
|
||||
return _amplitudeLeft;
|
||||
}
|
||||
|
||||
inline function get_amplitudeRight():Float {
|
||||
update_amplitude();
|
||||
return _amplitudeRight;
|
||||
}
|
||||
|
||||
inline function get_amplitude():Float {
|
||||
update_amplitude();
|
||||
return Math.max(_amplitudeLeft, _amplitudeRight);//if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft;
|
||||
}
|
||||
|
||||
inline function get_channels():Int
|
||||
@:privateAccess return (buffer != null) ? buffer.channels : 0;
|
||||
|
||||
inline function get_stereo():Bool
|
||||
return channels > 1;
|
||||
|
||||
#if FLX_PITCH
|
||||
inline function get_pitch():Float
|
||||
return _pitch;
|
||||
|
||||
function set_pitch(v:Float):Float {
|
||||
_realPitch = (_pitch = v) * _timeScaleAdjust;
|
||||
if (_channel != null) _channel.pitch = _realPitch;
|
||||
return _pitch;
|
||||
}
|
||||
#end
|
||||
|
||||
function set_looped(v:Bool):Bool {
|
||||
if (playing) {
|
||||
if (v) _channel.loops = 999;
|
||||
else _channel.loops = 0;
|
||||
}
|
||||
return looped = v;
|
||||
}
|
||||
|
||||
function set_loopTime(v:Float):Float {
|
||||
if (playing) _channel.loopTime = v;
|
||||
return loopTime = v;
|
||||
}
|
||||
|
||||
function set_endTime(v:Null<Float>):Null<Float> {
|
||||
if (playing) {
|
||||
if (v != null && v > 0 && v < _length) _channel.endTime = v;
|
||||
else _channel.endTime = null;
|
||||
}
|
||||
return endTime = v;
|
||||
}
|
||||
|
||||
inline function get_pan():Float return _transform.pan;
|
||||
inline function set_pan(pan:Float):Float return _transform.pan = pan;
|
||||
|
||||
inline function getFakeTime():Float {
|
||||
if (@:privateAccess _channel.__isValid && _source.playing && _realPitch > 0 && _lastTime != null)
|
||||
return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation;
|
||||
else
|
||||
return _time;
|
||||
}
|
||||
function get_time():Float {
|
||||
if (_channel == null || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected /*CNE IMPLEMENTATION*/) return _time;
|
||||
|
||||
final pos = _channel.position - _offset;
|
||||
if (!playing || _realPitch <= 0) {
|
||||
_lastTime = null;
|
||||
return _time = pos;
|
||||
}
|
||||
|
||||
final fakeTime = getFakeTime();
|
||||
if (pos != _time) {
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
if ((_timeInterpolation = 1 - Math.min(fakeTime - pos, 1000) * 0.001) < 1 && _timeInterpolation > .9)
|
||||
return _time = fakeTime;
|
||||
else {
|
||||
_timeInterpolation = 1;
|
||||
return _time = pos;
|
||||
}
|
||||
}
|
||||
else
|
||||
return fakeTime;
|
||||
}
|
||||
|
||||
function set_time(time:Float):Float @:privateAccess {
|
||||
time = FlxMath.bound(time, _offset, length - 1);
|
||||
if (_channel != null && _realPitch > 0) {
|
||||
if (!_channel.__isValid) {
|
||||
cleanup(false, true);
|
||||
startSound(time);
|
||||
}
|
||||
else if (playing) {
|
||||
_source.offset = 0;
|
||||
_source.currentTime = time + _offset;
|
||||
}
|
||||
}
|
||||
|
||||
_lastTime = null;
|
||||
return _time = time;
|
||||
}
|
||||
|
||||
inline function get_offset():Float return _offset;
|
||||
inline function set_offset(offset:Float):Float {
|
||||
if (_offset == (_offset = offset)) return offset;
|
||||
//time = time + _offset;
|
||||
return offset;
|
||||
}
|
||||
|
||||
inline function get_length():Float return _length - _offset;
|
||||
|
||||
/*function get_latency():Float {
|
||||
if (_channel != null) return _source.latency;
|
||||
return 0;
|
||||
}*/
|
||||
|
||||
override function toString():String {
|
||||
return FlxStringUtil.getDebugString([
|
||||
LabelValuePair.weak("playing", playing),
|
||||
LabelValuePair.weak("time", _time),
|
||||
LabelValuePair.weak("length", length),
|
||||
LabelValuePair.weak("volume", volume),
|
||||
LabelValuePair.weak("pitch", pitch)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -159,16 +159,9 @@ class Conductor
|
||||
}
|
||||
|
||||
private static function __updateSongPos(elapsed:Float) {
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing) {
|
||||
lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.time - songOffset : -songOffset;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) {
|
||||
// update conductor
|
||||
songPosition = lastSongPos;
|
||||
} else {
|
||||
songPosition += songOffset + elapsed * 1000;
|
||||
if (FlxG.sound.music != null) { // CNE FlxSound is Interpolated.
|
||||
lastSongPos = FlxG.sound.music.time - songOffset;
|
||||
if (FlxG.sound.music.playing) songPosition = FlxG.sound.music.time;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import flixel.FlxState;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.backend.utils.NativeAPI;
|
||||
import lime.media.AudioManager;
|
||||
import lime.media.AudioSource;
|
||||
import haxe.Timer;
|
||||
|
||||
/**
|
||||
* if you are stealing this keep this comment at least please lol
|
||||
@@ -12,58 +14,45 @@ import lime.media.AudioManager;
|
||||
*/
|
||||
@:dox(hide)
|
||||
class AudioSwitchFix {
|
||||
@:noCompletion
|
||||
private static function onStateSwitch(state:FlxState):Void {
|
||||
#if windows
|
||||
if (Main.audioDisconnected) {
|
||||
public static function onAudioDisconnected() @:privateAccess {
|
||||
var soundList:Array<FlxSound> = [FlxG.sound.music];
|
||||
for (sound in FlxG.sound.list) if (sound.playing) {
|
||||
sound.pause();
|
||||
soundList.push(sound);
|
||||
}
|
||||
|
||||
var playingList:Array<PlayingSound> = [];
|
||||
for(e in FlxG.sound.list) {
|
||||
if (e.playing) {
|
||||
playingList.push({
|
||||
sound: e,
|
||||
time: e.time
|
||||
});
|
||||
e.stop();
|
||||
}
|
||||
}
|
||||
if (FlxG.sound.music != null)
|
||||
FlxG.sound.music.stop();
|
||||
var i = AudioSource.activeSources.length;
|
||||
while (i-- > 0) AudioSource.activeSources[i].dispose();
|
||||
|
||||
AudioManager.shutdown();
|
||||
AudioManager.init();
|
||||
// #if !lime_doc_gen
|
||||
// if (AudioManager.context.type == OPENAL)
|
||||
// {
|
||||
// var alc = AudioManager.context.openal;
|
||||
AudioManager.shutdown();
|
||||
|
||||
// var device = alc.openDevice();
|
||||
// var ctx = alc.createContext(device);
|
||||
// alc.makeContextCurrent(ctx);
|
||||
// alc.processContext(ctx);
|
||||
// }
|
||||
// #end
|
||||
Main.changeID++;
|
||||
AudioManager.init();
|
||||
Main.changeID++;
|
||||
// #if !lime_doc_gen
|
||||
// if (AudioManager.context.type == OPENAL)
|
||||
// {
|
||||
// var alc = AudioManager.context.openal;
|
||||
|
||||
for(e in playingList) {
|
||||
e.sound.play(e.time);
|
||||
}
|
||||
// var device = alc.openDevice();
|
||||
// var ctx = alc.createContext(device);
|
||||
// alc.makeContextCurrent(ctx);
|
||||
// alc.processContext(ctx);
|
||||
// }
|
||||
// #end
|
||||
|
||||
Main.audioDisconnected = false;
|
||||
}
|
||||
#end
|
||||
for (sound in soundList) {
|
||||
sound.makeChannel();
|
||||
sound.resume();
|
||||
}
|
||||
|
||||
Main.audioDisconnected = false;
|
||||
}
|
||||
|
||||
private static var timer:Timer;
|
||||
|
||||
private static function onRun() if (Main.audioDisconnected) onAudioDisconnected();
|
||||
public static function init() {
|
||||
#if windows
|
||||
NativeAPI.registerAudio();
|
||||
FlxG.signals.preStateCreate.add(onStateSwitch);
|
||||
#end
|
||||
if (timer == null) (timer = new Timer(1000)).run = onRun;
|
||||
}
|
||||
}
|
||||
|
||||
@:dox(hide)
|
||||
typedef PlayingSound = {
|
||||
var sound:FlxSound;
|
||||
var time:Float;
|
||||
}
|
||||
@@ -3,43 +3,184 @@ package funkin.backend.utils;
|
||||
import flixel.sound.FlxSound;
|
||||
import lime.media.AudioBuffer;
|
||||
|
||||
// Thank you yosh :DDD -lunar
|
||||
// (https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx)
|
||||
#if lime_vorbis
|
||||
import lime.media.vorbis.Vorbis;
|
||||
import lime.media.vorbis.VorbisFile;
|
||||
import lime.utils.ArrayBuffer;
|
||||
#end
|
||||
|
||||
// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx
|
||||
// REWRITTEN BY RALTYRO
|
||||
class AudioAnalyzer {
|
||||
public var sound:FlxSound;
|
||||
public var buffer:AudioBuffer;
|
||||
var __peakByte:Float = 0;
|
||||
var __timeMulti:Float = 0;
|
||||
var __toBits:Float;
|
||||
var __wordSize:Int;
|
||||
var __bitUnsignedSize:Int;
|
||||
var __bitSize:Int;
|
||||
|
||||
#if lime_vorbis
|
||||
var __vorbis:VorbisFile;
|
||||
#end
|
||||
|
||||
public function new(sound:FlxSound) {
|
||||
@:privateAccess buffer = sound._sound.__buffer;
|
||||
|
||||
__peakByte = Math.pow(2, buffer.bitsPerSample-1)-1;
|
||||
|
||||
__timeMulti = 1 / (1 / buffer.sampleRate);
|
||||
__timeMulti *= buffer.bitsPerSample;
|
||||
__timeMulti -= __timeMulti % buffer.bitsPerSample;
|
||||
this.sound = sound;
|
||||
__check();
|
||||
}
|
||||
|
||||
public function analyze(startPos:Float, endPos:Float):Float {
|
||||
var bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
|
||||
var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
|
||||
inline function __check() if (sound.buffer != buffer) {
|
||||
__bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1);
|
||||
__bitUnsignedSize = 1 << buffer.bitsPerSample;
|
||||
__toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3);
|
||||
__vorbis = null;
|
||||
}
|
||||
|
||||
bytesStartPos -= bytesStartPos % buffer.bitsPerSample;
|
||||
bytesEndPos -= bytesEndPos % buffer.bitsPerSample;
|
||||
public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
|
||||
__check();
|
||||
if (buffer.data != null) return __analyze(startPos, endPos, outSeperate);
|
||||
#if lime_vorbis
|
||||
else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate);
|
||||
#end
|
||||
return 0;
|
||||
}
|
||||
|
||||
var maxByte:Int = 0;
|
||||
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) {
|
||||
#if js
|
||||
var byte:Int = buffer.data[bytesStartPos + (i * buffer.bitsPerSample)]
|
||||
| (buffer.data[bytesStartPos + (i * buffer.bitsPerSample) + 1] << 8);
|
||||
#else
|
||||
var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample))
|
||||
| (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 1) << 8);
|
||||
#end
|
||||
if (byte > 256 * 128) byte -= 256 * 256;
|
||||
if (maxByte < byte) maxByte = byte;
|
||||
inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
|
||||
var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end;
|
||||
var max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
|
||||
if (useSeperate) {
|
||||
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
|
||||
while (c++ < buffer.channels) outSeperate.push(0);
|
||||
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
|
||||
}
|
||||
|
||||
return maxByte/__peakByte;
|
||||
pos -= pos % __wordSize;
|
||||
end -= end % __wordSize;
|
||||
|
||||
while (pos < end) {
|
||||
// 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0)
|
||||
if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize;
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= #if js buf[pos] #else buf.get(pos) #end << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
|
||||
return max / __bitSize;
|
||||
}
|
||||
|
||||
#if lime_vorbis // As far i know, only native supports vorbis
|
||||
var __buffer:ArrayBuffer;
|
||||
var __bufferSize:Int;
|
||||
function __prepareVorbis():Bool @:privateAccess {
|
||||
if (buffer.__srcVorbisFile == null) return __vorbis != null;
|
||||
if (__vorbis != null) return true;
|
||||
if ((__vorbis = buffer.__srcVorbisFile.clone()) != null) { // IM HOPING IT HAVE A GC CLOSURE.
|
||||
__buffer = new ArrayBuffer(__bufferSize = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float @:privateAccess {
|
||||
var n = Math.floor((endPos - startPos) * __toBits);
|
||||
if ((n -= n % __wordSize) < __wordSize) return 0;
|
||||
|
||||
var pos = 0, max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
|
||||
if (useSeperate) {
|
||||
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
|
||||
while (c++ < buffer.channels) outSeperate.push(0);
|
||||
}
|
||||
|
||||
// IT SHOULD BE ALWAYS BACKEND.STREAMED IF THIS GETS CALLED.
|
||||
var backend = sound._source != null ? sound._source.__backend : null;
|
||||
if (backend != null && backend.streamTimer != null) {
|
||||
var i = backend.bufferSizes.length - backend.queuedBuffers;
|
||||
var time = backend.bufferTimes[i] * 1000;
|
||||
|
||||
if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) {
|
||||
var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i];
|
||||
pos = Math.floor((startPos - time) * __toBits);
|
||||
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
|
||||
pos -= pos % __wordSize;
|
||||
|
||||
while (n > 0) {
|
||||
if (__wordSize == 1) {
|
||||
b = buf.get(pos++) - __bitSize;
|
||||
n--;
|
||||
}
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= buf.get(pos) << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
n--;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
if (pos >= size) {
|
||||
if (++i >= backend.bufferDatas.length) break;
|
||||
size = backend.bufferSizes[i];
|
||||
buf = backend.bufferDatas[i].buffer;
|
||||
pos = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
|
||||
startPos = pos / __toBits + time;
|
||||
// Cannot inline a not final return
|
||||
}
|
||||
}
|
||||
if (n == 0) return max / __bitSize;
|
||||
|
||||
if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) {
|
||||
if (startPos < 1) __vorbis.rawSeek(0);
|
||||
else __vorbis.timeSeek(startPos / 1000);
|
||||
}
|
||||
|
||||
pos = c = 0;
|
||||
var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, result;
|
||||
while (n > 0) {
|
||||
n -= (result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true));
|
||||
|
||||
if (result == Vorbis.HOLE) continue;
|
||||
else if (result <= 0) break;
|
||||
|
||||
while (pos < result) {
|
||||
if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize;
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= __buffer.get(pos) << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
return max / __bitSize;
|
||||
}
|
||||
#end
|
||||
}
|
||||
@@ -386,7 +386,6 @@ class CoolUtil
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||
{
|
||||
playMusic(Paths.music('freakyMenu'), true, fadeIn ? 0 : 1, true, 102);
|
||||
FlxG.sound.music.persist = true;
|
||||
if (fadeIn)
|
||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||
}
|
||||
@@ -416,21 +415,26 @@ class CoolUtil
|
||||
*/
|
||||
@:noUsing public static function playMusic(path:String, Persist:Bool = false, Volume:Float = 1, Looped:Bool = true, DefaultBPM:Float = 102, ?Group:FlxSoundGroup) {
|
||||
Conductor.reset();
|
||||
FlxG.sound.playMusic(path, Volume, Looped, Group);
|
||||
if (FlxG.sound.music != null) {
|
||||
FlxG.sound.music.persist = Persist;
|
||||
}
|
||||
if (FlxG.sound.music == null) FlxG.sound.music = new FlxSound();
|
||||
else if (FlxG.sound.music.active) FlxG.sound.music.stop();
|
||||
FlxG.sound.music.loadEmbedded(path, Looped);
|
||||
FlxG.sound.music.volume = Volume;
|
||||
FlxG.sound.music.persist = Persist;
|
||||
FlxG.sound.defaultMusicGroup.add(FlxG.sound.music);
|
||||
|
||||
var infoPath = '${Path.withoutExtension(path)}.ini';
|
||||
if (Assets.exists(infoPath)) {
|
||||
var musicInfo = IniUtil.parseAsset(infoPath, [
|
||||
"BPM" => null,
|
||||
"TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT,
|
||||
"LoopTime" => '${Flags.DEFAULT_LOOP_TIME}'
|
||||
"LoopTime" => '${Flags.DEFAULT_LOOP_TIME}',
|
||||
"EndTime" => null,
|
||||
"Offset" => '0'
|
||||
]);
|
||||
|
||||
if (FlxG.sound.music != null)
|
||||
FlxG.sound.music.loopTime = Std.parseFloat(musicInfo["LoopTime"]) * 1000;
|
||||
FlxG.sound.music.loopTime = Std.parseFloat(musicInfo["LoopTime"]) * 1000;
|
||||
if (musicInfo["EndTime"] != null) FlxG.sound.music.endTime = Std.parseFloat(musicInfo["EndTime"]) * 1000;
|
||||
FlxG.sound.music.offset = Std.parseFloat(musicInfo["Offset"]) * 1000;
|
||||
|
||||
var timeSignParsed:Array<Null<Float>> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)];
|
||||
var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE;
|
||||
@@ -446,6 +450,8 @@ class CoolUtil
|
||||
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
|
||||
} else
|
||||
Conductor.changeBPM(DefaultBPM);
|
||||
|
||||
FlxG.sound.music.play();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,10 +818,11 @@ class CoolUtil
|
||||
* @param music Music
|
||||
*/
|
||||
public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) {
|
||||
if (frontEnd.music != null)
|
||||
@:privateAccess frontEnd.destroySound(frontEnd.music);
|
||||
if (frontEnd.music == music) return;
|
||||
|
||||
if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music);
|
||||
frontEnd.list.remove(music);
|
||||
frontEnd.music = music;
|
||||
frontEnd.defaultMusicGroup.add(frontEnd.music = music);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -616,22 +616,17 @@ class Charter extends UIState {
|
||||
updateWaveforms();
|
||||
}
|
||||
|
||||
inline function isSoundLoaded(sound:FlxSound) {
|
||||
@:privateAccess
|
||||
return sound != null && sound._sound != null && sound._sound.length > 0;
|
||||
}
|
||||
|
||||
public function updateWaveforms() {
|
||||
var wavesToGenerate:Array<{name:String, sound:FlxSound}> = [];
|
||||
|
||||
if(isSoundLoaded(FlxG.sound.music))
|
||||
if (FlxG.sound.music.loaded)
|
||||
wavesToGenerate.push({name: "Inst.ogg", sound: FlxG.sound.music});
|
||||
|
||||
if (PlayState.SONG.meta.needsVoices != false && isSoundLoaded(vocals))
|
||||
if (PlayState.SONG.meta.needsVoices != false && vocals.loaded)
|
||||
wavesToGenerate.push({name: "Voices.ogg", sound: vocals});
|
||||
|
||||
for (strumLine in strumLines)
|
||||
if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && isSoundLoaded(strumLine.vocals))
|
||||
if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && strumLine.vocals.loaded)
|
||||
wavesToGenerate.push({
|
||||
name: 'Voices${strumLine.strumLine.vocalsSuffix}.ogg',
|
||||
sound: strumLine.vocals
|
||||
@@ -1575,12 +1570,10 @@ class Charter extends UIState {
|
||||
vocals.pause();
|
||||
for (strumLine in strumLines.members) strumLine.vocals.pause();
|
||||
} else {
|
||||
FlxG.sound.music.play();
|
||||
vocals.play();
|
||||
vocals.time = FlxG.sound.music.time = Conductor.songPosition + Conductor.songOffset * 2;
|
||||
FlxG.sound.music.play(true, Conductor.songPosition + Conductor.songOffset);
|
||||
vocals.play(true, FlxG.sound.music.getActualTime());
|
||||
for (strumLine in strumLines.members) {
|
||||
strumLine.vocals.play();
|
||||
strumLine.vocals.time = vocals.time;
|
||||
strumLine.vocals.play(true, FlxG.sound.music.getActualTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ class PlayState extends MusicBeatState
|
||||
@:noCompletion @:dox(hide) private var _endSongCalled:Bool = false;
|
||||
|
||||
@:dox(hide)
|
||||
var __vocalOffsetViolation:Float = 0;
|
||||
var __vocalSyncTimer:Float = 0;
|
||||
|
||||
private function get_accuracy():Float {
|
||||
if (accuracyPressedNotes <= 0) return -1;
|
||||
@@ -999,25 +999,10 @@ class PlayState extends MusicBeatState
|
||||
|
||||
inst.onComplete = endSong;
|
||||
|
||||
if (!paused) {
|
||||
FlxG.sound.setMusic(inst);
|
||||
FlxG.sound.music.play();
|
||||
}
|
||||
vocals.play();
|
||||
|
||||
vocals.pause();
|
||||
inst.pause();
|
||||
for (strumLine in strumLines.members) {
|
||||
strumLine.vocals.play();
|
||||
strumLine.vocals.pause();
|
||||
}
|
||||
inst.time = vocals.time = (chartingMode && Charter.startHere) ? Charter.startTime : 0;
|
||||
for (strumLine in strumLines.members) {
|
||||
strumLine.vocals.time = vocals.time;
|
||||
strumLine.vocals.play();
|
||||
}
|
||||
vocals.play();
|
||||
inst.play();
|
||||
var time = (chartingMode && Charter.startHere) ? Charter.startTime : 0;
|
||||
for (strumLine in strumLines.members) strumLine.vocals.play(true, time);
|
||||
vocals.play(true, time);
|
||||
inst.play(true, time);
|
||||
|
||||
updateDiscordPresence();
|
||||
|
||||
@@ -1028,15 +1013,14 @@ class PlayState extends MusicBeatState
|
||||
scripts.call("destroy");
|
||||
for(g in __cachedGraphics)
|
||||
g.useCount--;
|
||||
@:privateAccess
|
||||
for (strumLine in strumLines.members)
|
||||
FlxG.sound.destroySound(strumLine.vocals);
|
||||
super.destroy();
|
||||
scripts = FlxDestroyUtil.destroy(scripts);
|
||||
@:privateAccess {
|
||||
FlxG.sound.destroySound(inst);
|
||||
for (strumLine in strumLines.members) FlxG.sound.destroySound(strumLine.vocals);
|
||||
if (FlxG.sound.music != inst) FlxG.sound.destroySound(inst);
|
||||
FlxG.sound.destroySound(vocals);
|
||||
}
|
||||
scripts = FlxDestroyUtil.destroy(scripts);
|
||||
|
||||
super.destroy();
|
||||
|
||||
WindowUtils.resetAffixes();
|
||||
SaveWarning.reset();
|
||||
@@ -1079,15 +1063,14 @@ class PlayState extends MusicBeatState
|
||||
curSong = songData.meta.name.toLowerCase();
|
||||
curSongID = curSong.replace(" ", "-");
|
||||
|
||||
inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty));
|
||||
FlxG.sound.setMusic(inst = FlxG.sound.load(Assets.getMusic(Paths.inst(SONG.meta.name, difficulty))));
|
||||
if (SONG.meta.needsVoices != false && Assets.exists(Paths.voices(SONG.meta.name, difficulty))) // null or true
|
||||
vocals = FlxG.sound.load(Paths.voices(SONG.meta.name, difficulty));
|
||||
vocals = FlxG.sound.load(Options.streamedVocals ? Assets.getMusic(Paths.voices(SONG.meta.name, difficulty)) : Paths.voices(SONG.meta.name, difficulty));
|
||||
else
|
||||
vocals = new FlxSound();
|
||||
inst.group = FlxG.sound.defaultMusicGroup;
|
||||
vocals.group = FlxG.sound.defaultMusicGroup;
|
||||
|
||||
inst.persist = vocals.persist = false;
|
||||
vocals.group = FlxG.sound.defaultMusicGroup;
|
||||
vocals.persist = false;
|
||||
|
||||
generatedMusic = true;
|
||||
}
|
||||
@@ -1188,17 +1171,11 @@ class PlayState extends MusicBeatState
|
||||
@:dox(hide)
|
||||
function resyncVocals():Void
|
||||
{
|
||||
vocals.pause();
|
||||
for (strumLine in strumLines.members) strumLine.vocals.pause();
|
||||
var time = Conductor.songPosition + Conductor.songOffset;
|
||||
for (strumLine in strumLines.members) strumLine.vocals.play(true, time);
|
||||
vocals.play(true, time);
|
||||
inst.play(true, time);
|
||||
|
||||
FlxG.sound.music.play();
|
||||
Conductor.songPosition = FlxG.sound.music.time;
|
||||
vocals.time = Conductor.songPosition + Conductor.songOffset;
|
||||
for (strumLine in strumLines.members) {
|
||||
strumLine.vocals.time = vocals.time;
|
||||
strumLine.vocals.play();
|
||||
}
|
||||
vocals.play();
|
||||
gameAndCharsCall("onVocalsResync");
|
||||
}
|
||||
|
||||
@@ -1333,22 +1310,19 @@ class PlayState extends MusicBeatState
|
||||
if (Conductor.songPosition >= 0)
|
||||
startSong();
|
||||
}
|
||||
} else if (FlxG.sound.music != null) {
|
||||
var instTime = FlxG.sound.music.time;
|
||||
var isOffsync:Bool = vocals.time != instTime;
|
||||
if(!isOffsync) {
|
||||
for(strumLine in strumLines.members) {
|
||||
if(strumLine.vocals.time != instTime) {
|
||||
isOffsync = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (FlxG.sound.music != null && (__vocalSyncTimer -= elapsed) < 0) {
|
||||
__vocalSyncTimer = 1;
|
||||
|
||||
var instTime = FlxG.sound.music.getActualTime();
|
||||
var isOffsync:Bool = vocals.loaded && Math.abs(instTime - vocals.getActualTime()) > 30;
|
||||
if (!isOffsync) {
|
||||
for (strumLine in strumLines.members) {
|
||||
if ((isOffsync = strumLine.vocals.loaded && Math.abs(instTime - strumLine.vocals.getActualTime()) > 30)) break;
|
||||
}
|
||||
}
|
||||
__vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (isOffsync ? elapsed : -elapsed / 2));
|
||||
if (__vocalOffsetViolation > Flags.VOCAL_OFFSET_VIOLATION_THRESHOLD) {
|
||||
resyncVocals();
|
||||
__vocalOffsetViolation = 0;
|
||||
}
|
||||
|
||||
if (isOffsync) resyncVocals();
|
||||
}
|
||||
|
||||
while(events.length > 0 && events.last().time <= Conductor.songPosition)
|
||||
|
||||
@@ -114,7 +114,9 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
this.opponentSide = opponentSide;
|
||||
this.controls = controls;
|
||||
this.notes = new NoteGroup();
|
||||
vocals = vocalPrefix != "" ? FlxG.sound.load(Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, vocalPrefix)) : new FlxSound();
|
||||
|
||||
var v = Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, vocalPrefix);
|
||||
vocals = vocalPrefix != "" ? FlxG.sound.load(Options.streamedVocals ? Assets.getMusic(v) : v) : new FlxSound();
|
||||
vocals.persist = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class PauseSubState extends MusicBeatSubstate
|
||||
|
||||
menuItems = event.options;
|
||||
|
||||
pauseMusic = FlxG.sound.load(Paths.music(event.music), 0, true);
|
||||
pauseMusic = FlxG.sound.load(Assets.getMusic(Paths.music(event.music)), 0, true);
|
||||
pauseMusic.persist = false;
|
||||
pauseMusic.group = FlxG.sound.defaultMusicGroup;
|
||||
pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
|
||||
|
||||
@@ -40,6 +40,8 @@ class Options
|
||||
public static var songOffset:Float = 0;
|
||||
public static var framerate:Int = 120;
|
||||
public static var gpuOnlyBitmaps:Bool = #if (mac || web) false #else true #end; // causes issues on mac and web
|
||||
public static var streamedMusic:Bool = true;
|
||||
public static var streamedVocals:Bool = true;
|
||||
|
||||
public static var lastLoadedMod:String = null;
|
||||
|
||||
|
||||
@@ -43,6 +43,14 @@ class AppearanceOptions extends OptionsScreen {
|
||||
"gpuOnlyBitmaps"));
|
||||
}
|
||||
#end
|
||||
add(new Checkbox(
|
||||
"Streamed Music",
|
||||
"If checked, only musics will have streamed audio, ALSO freeing a LOT of memory with the downside of higher cpu usage if more audio are being streamed at once (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on longer songs. If you aren't sure, leave this on.",
|
||||
"streamedMusic"));
|
||||
add(new Checkbox(
|
||||
"Streamed Vocals",
|
||||
"If checked, vocals will also be streamed if Streamed Music is checked, will impact the performance if the song uses alot of vocals (EXPERIMENTAL). If you aren't sure, leave this on.",
|
||||
"streamedVocals"));
|
||||
add(new Checkbox(
|
||||
"Auto Pause",
|
||||
"If checked, switching windows will pause the game.",
|
||||
|
||||
@@ -2,6 +2,7 @@ package lime._internal.backend.html5;
|
||||
|
||||
import lime.math.Vector4;
|
||||
import lime.media.AudioSource;
|
||||
import lime.media.AudioManager;
|
||||
|
||||
@:access(lime.media.AudioBuffer)
|
||||
class HTML5AudioSource
|
||||
@@ -9,7 +10,7 @@ class HTML5AudioSource
|
||||
private var completed:Bool;
|
||||
private var gain:Float;
|
||||
private var id:Int;
|
||||
private var length:Float;
|
||||
private var length:Null<Float>;
|
||||
private var loops:Int;
|
||||
private var parent:AudioSource;
|
||||
private var playing:Bool;
|
||||
@@ -24,7 +25,9 @@ class HTML5AudioSource
|
||||
position = new Vector4();
|
||||
}
|
||||
|
||||
public function dispose():Void {}
|
||||
public function dispose():Void {
|
||||
stop();
|
||||
}
|
||||
|
||||
public function init():Void {}
|
||||
|
||||
@@ -98,8 +101,9 @@ class HTML5AudioSource
|
||||
{
|
||||
loops--;
|
||||
stop();
|
||||
// currentTime = 0;
|
||||
if (loopTime != null && loopTime > 0) setCurrentTime(loopTime);
|
||||
play();
|
||||
parent.onLoop.dispatch();
|
||||
return;
|
||||
}
|
||||
else if (parent.buffer != null && parent.buffer.__srcHowl != null)
|
||||
@@ -128,7 +132,7 @@ class HTML5AudioSource
|
||||
}
|
||||
else if (parent.buffer != null && parent.buffer.__srcHowl != null)
|
||||
{
|
||||
var time = (parent.buffer.__srcHowl.seek(id) * 1000) - parent.offset;
|
||||
var time = parent.buffer.__srcHowl.seek(id) * 1000.0 - parent.offset;
|
||||
if (time < 0) return 0;
|
||||
return time;
|
||||
}
|
||||
@@ -137,6 +141,20 @@ class HTML5AudioSource
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getLatency():Float
|
||||
{
|
||||
var ctx = AudioManager.context.web;
|
||||
if (ctx != null)
|
||||
{
|
||||
var baseLatency:Float = untyped ctx.baseLatency != null ? untyped ctx.baseLatency : 0;
|
||||
var outputLatency:Float = untyped ctx.outputLatency != null ? untyped ctx.outputLatency : 0;
|
||||
|
||||
return (baseLatency + outputLatency) * 1000;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function setCurrentTime(value:Float):Float
|
||||
{
|
||||
#if lime_howlerjs
|
||||
@@ -172,7 +190,7 @@ class HTML5AudioSource
|
||||
return gain = value;
|
||||
}
|
||||
|
||||
public function getLength():Float
|
||||
public function getLength():Null<Float>
|
||||
{
|
||||
if (length != 0)
|
||||
{
|
||||
@@ -182,14 +200,14 @@ class HTML5AudioSource
|
||||
#if lime_howlerjs
|
||||
if (parent.buffer != null && parent.buffer.__srcHowl != null)
|
||||
{
|
||||
return parent.buffer.__srcHowl.duration() * 1000;
|
||||
return parent.buffer.__srcHowl.duration() * 1000.0;
|
||||
}
|
||||
#end
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function setLength(value:Float):Float
|
||||
public function setLength(value:Null<Float>):Null<Float>
|
||||
{
|
||||
return length = value;
|
||||
}
|
||||
@@ -204,6 +222,14 @@ class HTML5AudioSource
|
||||
return loops = value;
|
||||
}
|
||||
|
||||
public function getLoopTime():Float {
|
||||
return loopTime;
|
||||
}
|
||||
|
||||
public function setLoopTime(value:Float):Float {
|
||||
return loopTime = value;
|
||||
}
|
||||
|
||||
public function getPitch():Float
|
||||
{
|
||||
#if lime_howlerjs
|
||||
@@ -218,9 +244,11 @@ class HTML5AudioSource
|
||||
#if lime_howlerjs
|
||||
parent.buffer.__srcHowl.rate(value);
|
||||
#end
|
||||
|
||||
return getPitch();
|
||||
}
|
||||
|
||||
|
||||
public function getPosition():Vector4
|
||||
{
|
||||
#if lime_howlerjs
|
||||
@@ -250,4 +278,4 @@ class HTML5AudioSource
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,584 +1,492 @@
|
||||
// ALL REWRITTEN FROM SCRATCH!!!! -raltyro
|
||||
package lime._internal.backend.native;
|
||||
|
||||
import haxe.Int64;
|
||||
import haxe.Timer;
|
||||
import lime.math.Vector4;
|
||||
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.AudioManager;
|
||||
#end
|
||||
|
||||
import lime.math.Vector4;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.media.AudioSource;
|
||||
import lime.utils.UInt8Array;
|
||||
import lime.utils.ArrayBufferView;
|
||||
|
||||
#if !lime_debug
|
||||
@:fileXml('tags="haxe,release"')
|
||||
@:noDebug
|
||||
#end
|
||||
@:access(lime.media.AudioBuffer)
|
||||
class NativeAudioSource
|
||||
{
|
||||
public static var initBuffers:Array<NativeAudioSource> = [];
|
||||
@:access(lime.utils.ArrayBufferView)
|
||||
class NativeAudioSource {
|
||||
private static var STREAM_BUFFER_SAMPLES:Int = 0x1000;
|
||||
private static var STREAM_MAX_BUFFERS:Int = 32;
|
||||
private static var STREAM_TIMER_FREQUENCY:Int = 100;
|
||||
private static var STREAM_BUFFER_FREQUENCY:Int = 6;
|
||||
|
||||
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 disposed:Bool;
|
||||
private var streamed:Bool;
|
||||
private var playing:Bool;
|
||||
private var position:Vector4;
|
||||
private var samples:Float;
|
||||
private var stream:Bool;
|
||||
private var streamTimer:Timer;
|
||||
private var completed:Bool;
|
||||
|
||||
private var handle:ALSource;
|
||||
private var buffers:Array<ALBuffer>;
|
||||
private var unusedBuffers:Array<ALBuffer>;
|
||||
private var timer:Timer;
|
||||
private var format:Int;
|
||||
private var samples:Float;
|
||||
|
||||
private var pcmTotal:Int64;
|
||||
private var sampleRate:Int;
|
||||
private var streamTimer:Timer;
|
||||
private var bufferDatas:Array<ArrayBufferView>;
|
||||
private var bufferTimes:Array<Float>;
|
||||
private var bufferSizes:Array<Int>;
|
||||
private var bufferSize:Int;
|
||||
private var requestBuffers:Int;
|
||||
private var queuedBuffers:Int;
|
||||
private var toLoop:Int;
|
||||
private var streamEnded:Bool;
|
||||
private var dataLength:Float;
|
||||
|
||||
var disposed:Bool = false;
|
||||
private var position:Vector4 = new Vector4();
|
||||
private var length:Null<Float>;
|
||||
private var loopTime:Null<Float>;
|
||||
private var loops:Int;
|
||||
|
||||
public function new(parent:AudioSource)
|
||||
{
|
||||
this.parent = parent;
|
||||
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;
|
||||
}
|
||||
public function dispose() {
|
||||
disposed = true;
|
||||
initBuffers.remove(this);
|
||||
stop();
|
||||
|
||||
if (handle != null) {
|
||||
AL.sourcei(handle, AL.BUFFER, AL.NONE);
|
||||
AL.deleteSource(handle);
|
||||
}
|
||||
handle = null;
|
||||
|
||||
if (buffers != null) AL.deleteBuffers(buffers);
|
||||
buffers = null;
|
||||
bufferDatas = null;
|
||||
bufferTimes = null;
|
||||
bufferSizes = null;
|
||||
unusedBuffers = null;
|
||||
}
|
||||
|
||||
public function init():Void
|
||||
{
|
||||
dataLength = 0;
|
||||
format = 0;
|
||||
public function init() {
|
||||
if (handle != null) return;
|
||||
|
||||
if (parent.buffer.channels == 1)
|
||||
{
|
||||
if (parent.buffer.bitsPerSample == 8)
|
||||
{
|
||||
format = AL.FORMAT_MONO8;
|
||||
}
|
||||
else if (parent.buffer.bitsPerSample == 16)
|
||||
{
|
||||
format = AL.FORMAT_MONO16;
|
||||
}
|
||||
if (disposed = (handle = AL.createSource()) == null) return;
|
||||
AL.sourcef(handle, AL.MAX_GAIN, 10);
|
||||
|
||||
var buffer = parent.buffer;
|
||||
var channels = buffer.channels, bitsPerSample = buffer.bitsPerSample;
|
||||
|
||||
// Default is just AL.FORMAT_MONO8 if it doesn't match any to avoid yo ears getting blasted
|
||||
var isCreativeXFi = AL.getString(AL.RENDERER) == "X-Fi";
|
||||
if (channels > 2 && (isCreativeXFi || AL.isExtensionPresent("AL_EXT_MCFORMATS"))) { // https://github.com/openalext/openalext/blob/master/AL_EXT_MCFORMATS.txt
|
||||
if (channels == 3) format = bitsPerSample == 32 ? 0x1209 : (bitsPerSample == 16 ? 0x1208 : 0x1207);
|
||||
else if (channels == 4) format = bitsPerSample == 32 ? 0x1206 : (bitsPerSample == 16 ? 0x1205 : 0x1204);
|
||||
else if (channels == 6) format = bitsPerSample == 32 ? 0x120C : (bitsPerSample == 16 ? 0x120B : 0x120A);
|
||||
else if (channels == 7) format = bitsPerSample == 32 ? 0x120F : (bitsPerSample == 16 ? 0x120E : 0x120D);
|
||||
else if (channels == 8) format = bitsPerSample == 32 ? 0x1212 : (bitsPerSample == 16 ? 0x1211 : 0x1210);
|
||||
}
|
||||
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;
|
||||
}
|
||||
// The 32 bit for STEREO and MONO are undocumented? https://github.com/kcat/openal-soft/issues/934, though it only works in X-Fi OpenAL??
|
||||
else if (bitsPerSample == 32 && isCreativeXFi) format = channels == 2 ? 0x1203 : 0x1202;
|
||||
else if (channels == 2) format = bitsPerSample == 16 ? AL.FORMAT_STEREO16 : AL.FORMAT_STEREO8;
|
||||
else format = bitsPerSample == 16 ? AL.FORMAT_MONO16 : AL.FORMAT_MONO8;
|
||||
|
||||
if (buffer.data != null) {
|
||||
streamed = false;
|
||||
samples = (dataLength = (buffer.data.length >> 0)) / buffer.channels / (buffer.bitsPerSample >> 3);
|
||||
|
||||
if (buffer.__srcBuffer == null && (buffer.__srcBuffer = AL.createBuffer()) != null)
|
||||
AL.bufferData(buffer.__srcBuffer, format, buffer.data, buffer.data.length, buffer.sampleRate);
|
||||
|
||||
AL.sourcei(handle, AL.BUFFER, buffer.__srcBuffer);
|
||||
}
|
||||
#if lime_vorbis
|
||||
else if (buffer.__srcVorbisFile != null) {
|
||||
streamed = true;
|
||||
dataLength = (samples = getFloat(buffer.__srcVorbisFile.pcmTotal())) * buffer.channels * (buffer.bitsPerSample >> 3);
|
||||
|
||||
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);
|
||||
var constructor = buffer.bitsPerSample == 32 ? Int32 : buffer.bitsPerSample == 16 ? Int16 : Int8;
|
||||
bufferSize = STREAM_BUFFER_SAMPLES * buffer.channels * (constructor == Int8 ? buffer.bitsPerSample >> 3 : 1);
|
||||
buffers = AL.genBuffers(STREAM_MAX_BUFFERS);
|
||||
bufferDatas = [for (i in 0...STREAM_MAX_BUFFERS) new ArrayBufferView(bufferSize, constructor)];
|
||||
bufferTimes = [for (i in 0...STREAM_MAX_BUFFERS) 0];
|
||||
bufferSizes = [for (i in 0...STREAM_MAX_BUFFERS) 0];
|
||||
unusedBuffers = [];
|
||||
bufferSize *= constructor == Int8 ? 1 : buffer.bitsPerSample >> 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parent.buffer.__srcBuffer == null)
|
||||
{
|
||||
parent.buffer.__srcBuffer = AL.createBuffer();
|
||||
#end
|
||||
|
||||
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));
|
||||
if (dataLength == 0) {
|
||||
trace('NativeAudioSource Bug! dataLength is 0');
|
||||
dispose();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public function play() {
|
||||
if (playing || disposed) 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);
|
||||
}
|
||||
if (completed) setCurrentTime(0);
|
||||
else setCurrentTime(getCurrentTime());
|
||||
}
|
||||
|
||||
public function pause():Void
|
||||
{
|
||||
public function pause() {
|
||||
if (!disposed) AL.sourcePause(handle);
|
||||
|
||||
playing = false;
|
||||
|
||||
if (handle == null) return;
|
||||
AL.sourcePause(handle);
|
||||
|
||||
if (streamTimer != null)
|
||||
{
|
||||
streamTimer.stop();
|
||||
}
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
timer.stop();
|
||||
}
|
||||
stopStreamTimer();
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
public function stop() {
|
||||
if (!disposed) {
|
||||
AL.sourceStop(handle);
|
||||
toLoop = 0;
|
||||
}
|
||||
|
||||
playing = false;
|
||||
|
||||
if (streamTimer != null)
|
||||
{
|
||||
streamTimer.stop();
|
||||
}
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
setCurrentTime(0);
|
||||
stopStreamTimer();
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
private function complete() {
|
||||
completed = true;
|
||||
stop();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming, atleast for vorbis for now.
|
||||
// just incase if we support more than 1 in the future?? which i doubt
|
||||
private function streamTell():Float {
|
||||
#if lime_vorbis
|
||||
return parent.buffer.__srcVorbisFile.timeTell();
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function setCurrentTime(value:Float):Float
|
||||
{
|
||||
// `setCurrentTime()` has side effects and is never safe to skip.
|
||||
/* if (value == getCurrentTime())
|
||||
{
|
||||
return value;
|
||||
} */
|
||||
private function streamSeek(samples:Int64) {
|
||||
#if lime_vorbis
|
||||
// There's apparently a bug in libvorbis <= 1.3.4, hhttps://github.com/xiph/vorbis/blob/master/CHANGES#L39
|
||||
// But it seems to also happen in this lime libvorbis version which is 1.3.7...? -ralty
|
||||
if (samples < 4) parent.buffer.__srcVorbisFile.rawSeek(0); else parent.buffer.__srcVorbisFile.pcmSeek(samples);
|
||||
#end
|
||||
}
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
if (stream)
|
||||
{
|
||||
AL.sourceStop(handle);
|
||||
private function readToBufferData(data:ArrayBufferView):Int {
|
||||
#if lime_vorbis
|
||||
var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, wordSize = parent.buffer.bitsPerSample >> 3;
|
||||
var size = dataLength - (streamTell() * parent.buffer.sampleRate * parent.buffer.channels * wordSize);
|
||||
var n:Int = size < bufferSize ? Math.floor(size) : bufferSize;
|
||||
n -= n % (parent.buffer.channels * wordSize);
|
||||
|
||||
parent.buffer.__srcVorbisFile.timeSeek((value + parent.offset) / 1000);
|
||||
AL.sourceUnqueueBuffers(handle, STREAM_NUM_BUFFERS);
|
||||
refillBuffers(buffers);
|
||||
var total = 0, result = 0, wasEOF = false;
|
||||
while (total < bufferSize) {
|
||||
result = n > 0 ? parent.buffer.__srcVorbisFile.read(data.buffer, total, n, isBigEndian, wordSize, true) : 0;
|
||||
|
||||
if (playing) AL.sourcePlay(handle);
|
||||
if (result == Vorbis.HOLE) continue;
|
||||
else if (result <= Vorbis.EREAD) break;
|
||||
else if (result == 0) {
|
||||
if (wasEOF || (streamEnded = loops <= toLoop)) break;
|
||||
else {
|
||||
wasEOF = true;
|
||||
toLoop++;
|
||||
|
||||
var samples = getSamples(loopTime != null ? loopTime + parent.offset : parent.offset);
|
||||
streamSeek(samples);
|
||||
if ((size = dataLength - (getFloat(samples) * parent.buffer.channels * wordSize)) < (n = bufferSize - total)) n = Math.floor(size);
|
||||
n -= n % (parent.buffer.channels * wordSize);
|
||||
}
|
||||
}
|
||||
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);
|
||||
else {
|
||||
total += result;
|
||||
n -= result;
|
||||
wasEOF = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (playing)
|
||||
{
|
||||
if (timer != null)
|
||||
{
|
||||
timer.stop();
|
||||
if (total < bufferSize) data.buffer.fill(total, n, 0);
|
||||
if (result < 0) {
|
||||
trace('NativeAudioSource Streaming Bug! reading result is $result, streamEnded: $streamEnded, total: $total, n: $n');
|
||||
return result;
|
||||
}
|
||||
return total;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
private function fillBuffer(buffer:ALBuffer):Int {
|
||||
var i = STREAM_MAX_BUFFERS - requestBuffers;
|
||||
var data = bufferDatas[i], time = streamTell();
|
||||
var decoded = readToBufferData(data);
|
||||
|
||||
if (decoded > 0) {
|
||||
AL.bufferData(buffer, format, data, decoded, parent.buffer.sampleRate);
|
||||
|
||||
var n = STREAM_MAX_BUFFERS - 1;
|
||||
while (i < n) {
|
||||
bufferDatas[i] = bufferDatas[i + 1];
|
||||
bufferTimes[i] = bufferTimes[i + 1];
|
||||
bufferSizes[i] = bufferSizes[++i];
|
||||
}
|
||||
queuedBuffers = requestBuffers;
|
||||
bufferDatas[n] = data;
|
||||
bufferTimes[n] = time;
|
||||
bufferSizes[n] = decoded;
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
private function streamRun() {
|
||||
if (parent == null || parent.buffer == null || handle == null) return dispose();
|
||||
if (!playing || disposed) return stopStreamTimer();
|
||||
|
||||
try {
|
||||
var processed = AL.getSourcei(handle, AL.BUFFERS_PROCESSED), n = STREAM_BUFFER_FREQUENCY, buffer;
|
||||
while (processed-- > 0) {
|
||||
buffer = AL.sourceUnqueueBuffer(handle);
|
||||
if (!streamEnded && --n > 0 && fillBuffer(buffer) > 0) AL.sourceQueueBuffer(handle, buffer);
|
||||
else {
|
||||
queuedBuffers = --requestBuffers;
|
||||
unusedBuffers.push(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
var timeRemaining = (getLength() - value) / getPitch();
|
||||
if (!streamEnded) {
|
||||
if (unusedBuffers.length != 0) {
|
||||
requestBuffers++;
|
||||
if (fillBuffer(buffer = unusedBuffers.pop()) > 0) {
|
||||
queuedBuffers = requestBuffers;
|
||||
AL.sourceQueueBuffer(handle, buffer);
|
||||
}
|
||||
else {
|
||||
requestBuffers--;
|
||||
unusedBuffers.push(buffer);
|
||||
}
|
||||
}
|
||||
else if (queuedBuffers < STREAM_MAX_BUFFERS) {
|
||||
if (fillBuffer(buffer = buffers[requestBuffers++]) > 0) AL.sourceQueueBuffer(handle, buffer);
|
||||
else requestBuffers--;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e) trace('NativeAudioSource Bug! in streamRun $e');
|
||||
|
||||
if (timeRemaining > 0)
|
||||
{
|
||||
completed = false;
|
||||
timer = new Timer(timeRemaining);
|
||||
timer.run = timer_onRun;
|
||||
if (AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) {
|
||||
AL.sourcePlay(handle);
|
||||
resetTimer((getLength() - getCurrentTime()) / getPitch());
|
||||
}
|
||||
}
|
||||
|
||||
// Timers
|
||||
inline function stopStreamTimer() {
|
||||
if (streamTimer != null) streamTimer.stop();
|
||||
streamTimer = null;
|
||||
}
|
||||
|
||||
private function resetStreamTimer() {
|
||||
stopStreamTimer();
|
||||
if (streamed) (streamTimer = new Timer(STREAM_TIMER_FREQUENCY)).run = streamRun;
|
||||
}
|
||||
|
||||
inline function stopTimer() {
|
||||
if (timer != null) timer.stop();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
private function resetTimer(timeRemaining:Float) {
|
||||
stopTimer();
|
||||
(timer = new Timer(timeRemaining)).run = timer_onRun;
|
||||
}
|
||||
|
||||
private function timer_onRun() {
|
||||
var timeRemaining = (getLength() - getCurrentTime()) / getPitch();
|
||||
if (timeRemaining > 100 && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.PLAYING && (!streamed || !streamEnded)) {
|
||||
resetTimer(timeRemaining);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loops == 0) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
var start = loopTime != null ? loopTime : 0;
|
||||
if (toLoop <= 0) {
|
||||
setLoops(--loops);
|
||||
if (start > 0 || AL.getSourcei(handle, AL.SOURCE_STATE) != AL.PLAYING) setCurrentTime(start);
|
||||
else resetTimer((getLength() - start) / getPitch());
|
||||
}
|
||||
else {
|
||||
if ((loops -= toLoop) < 0) loops = 0;
|
||||
toLoop = 0;
|
||||
resetTimer((getLength() - start) / getPitch());
|
||||
}
|
||||
|
||||
parent.onLoop.dispatch();
|
||||
}
|
||||
|
||||
// Get & Set Methods
|
||||
public function getCurrentTime():Float {
|
||||
if (disposed) return 0;
|
||||
else if (completed) return getLength();
|
||||
|
||||
var time = AL.getSourcef(handle, AL.SAMPLE_OFFSET) / parent.buffer.sampleRate;
|
||||
if (streamed) {
|
||||
if (playing && streamEnded && AL.getSourcei(handle, AL.SOURCE_STATE) == AL.STOPPED) {
|
||||
complete();
|
||||
return getLength();
|
||||
}
|
||||
else
|
||||
{
|
||||
playing = false;
|
||||
completed = true;
|
||||
time += bufferTimes[STREAM_MAX_BUFFERS - queuedBuffers];
|
||||
}
|
||||
time *= 1000;
|
||||
|
||||
var length = if (length == null) getRealLength(); else length;
|
||||
if (loops > 0 && time > length) {
|
||||
var start = loopTime != null ? Math.max(0, loopTime) : 0;
|
||||
return ((time - start) % (length - start)) + start - parent.offset;
|
||||
}
|
||||
|
||||
return time - parent.offset;
|
||||
}
|
||||
|
||||
public function setCurrentTime(value:Float):Float {
|
||||
if (disposed) return value;
|
||||
|
||||
if (streamed) AL.sourceStop(handle);
|
||||
else AL.sourcei(handle, AL.SAMPLE_OFFSET, getSamples(value + parent.offset));
|
||||
|
||||
if (playing) {
|
||||
var timeRemaining = (getLength() - value) / getPitch();
|
||||
if (timeRemaining < 8) complete();
|
||||
else {
|
||||
completed = streamEnded = false;
|
||||
if (streamed) {
|
||||
AL.sourceUnqueueBuffers(handle, AL.getSourcei(handle, AL.BUFFERS_QUEUED));
|
||||
|
||||
unusedBuffers.resize(0);
|
||||
streamSeek(getSamples(value + parent.offset));
|
||||
|
||||
requestBuffers = queuedBuffers = STREAM_BUFFER_FREQUENCY;
|
||||
for (i in 0...queuedBuffers) {
|
||||
if (!streamEnded && fillBuffer(buffers[i]) > 0) AL.sourceQueueBuffer(handle, buffers[i]);
|
||||
else queuedBuffers = --requestBuffers;
|
||||
}
|
||||
|
||||
if (!streamEnded) resetStreamTimer();
|
||||
}
|
||||
|
||||
resetTimer(timeRemaining);
|
||||
AL.sourcePlay(handle);
|
||||
}
|
||||
}
|
||||
|
||||
if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public function getGain():Float
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
return AL.getSourcef(handle, AL.GAIN);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
inline private function getRealLength():Float return samples / parent.buffer.sampleRate * 1000;
|
||||
public function getLength():Null<Float>
|
||||
return if (disposed) 0; else if (length == null) getRealLength() - parent.offset; else length - parent.offset;
|
||||
|
||||
public function setGain(value:Float):Float
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
AL.sourcef(handle, AL.GAIN, value);
|
||||
}
|
||||
public function setLength(value:Null<Float>):Null<Float> {
|
||||
if (value == length || disposed) return length = value;
|
||||
|
||||
var buffer = parent.buffer, wordSize = buffer.bitsPerSample >> 3;
|
||||
if ((length = value) == null) dataLength = streamed ? samples * buffer.channels * wordSize : getFloat(Int64.make(0, buffer.data.length));
|
||||
else dataLength = Math.ffloor(Math.max(0, Math.min(value, getRealLength())) / 1000 * buffer.sampleRate) * buffer.channels * wordSize;
|
||||
|
||||
if (playing) {
|
||||
if (streamed && (streamEnded || toLoop > 0)) setCurrentTime(getCurrentTime());
|
||||
else {
|
||||
var timeRemaining = ((getLength() - parent.offset) - getCurrentTime()) / getPitch();
|
||||
if (timeRemaining > 0) resetTimer(timeRemaining);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public function getLength():Float
|
||||
{
|
||||
if (length != null)
|
||||
{
|
||||
return length;
|
||||
public function getPitch():Float {
|
||||
return if (disposed) 1;
|
||||
else AL.getSourcef(handle, AL.PITCH);
|
||||
}
|
||||
|
||||
public function setPitch(value:Float):Float {
|
||||
if (disposed || value == AL.getSourcef(handle, AL.PITCH)) return value;
|
||||
if (playing) {
|
||||
var timeRemaining = (getLength() - getCurrentTime()) / value;
|
||||
if (timeRemaining > 0) resetTimer(timeRemaining);
|
||||
}
|
||||
|
||||
return (samples / parent.buffer.sampleRate * 1000) - parent.offset;
|
||||
AL.sourcef(handle, AL.PITCH, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
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 getGain():Float {
|
||||
if (disposed) return 1;
|
||||
return AL.getSourcef(handle, AL.GAIN);
|
||||
}
|
||||
|
||||
public function getLoops():Int
|
||||
{
|
||||
return loops;
|
||||
public function setGain(value:Float):Float {
|
||||
if (!disposed) AL.sourcef(handle, AL.GAIN, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public function setLoops(value:Int):Int
|
||||
{
|
||||
inline public function getLoops():Int return loops;
|
||||
|
||||
inline public function setLoops(value:Int):Int {
|
||||
if (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (loopTime <= 0 && value > 0) ? AL.TRUE : AL.FALSE);
|
||||
else if (loops == 0 && value > 0) setCurrentTime(getCurrentTime());
|
||||
if (value < 0) return loops = 0;
|
||||
return loops = value;
|
||||
}
|
||||
|
||||
public function getPitch():Float
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
return AL.getSourcef(handle, AL.PITCH);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
inline public function getLoopTime():Float return loopTime;
|
||||
|
||||
inline public function setLoopTime(value:Float):Float {
|
||||
if (!streamed && !disposed) AL.sourcei(handle, AL.LOOPING, (value <= 0 && loops > 0) ? AL.TRUE : AL.FALSE);
|
||||
return loopTime = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public function getLatency():Float {
|
||||
/*#if (lime >= "8.3.0")
|
||||
if (AL.isExtensionPresent("AL_SOFT_source_latency")) {
|
||||
final offsets = AL.getSourcedvSOFT(handle, AL.SEC_OFFSET_LATENCY_SOFT, 2);
|
||||
if (offsets != null) return offsets[1] * 1000;
|
||||
}
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
AL.sourcef(handle, AL.PITCH, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
#end*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
public function getPosition():Vector4 return position;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
public function setPosition(value:Vector4):Vector4
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!disposed) {
|
||||
AL.distanceModel(AL.NONE);
|
||||
AL.source3f(handle, AL.POSITION, position.x, position.y, position.z);
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
inline private function getFloat(x:Int64):Float return x.high * 4294967296. + (x.low >> 0);
|
||||
inline private function getSamples(ms:Float):Int64 return Int64.fromFloat(Math.max(0, Math.min(ms / 1000 * parent.buffer.sampleRate, samples)));
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
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;
|
||||
import lime.media.vorbis.VorbisFile;
|
||||
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.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 !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;
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,100 @@
|
||||
package lime.media;
|
||||
|
||||
import lime.app.Event;
|
||||
import lime.media.openal.AL;
|
||||
import lime.media.openal.ALSource;
|
||||
import lime.math.Vector4;
|
||||
|
||||
#if !lime_debug
|
||||
@:fileXml('tags="haxe,release"')
|
||||
@:noDebug
|
||||
#end
|
||||
/**
|
||||
The `AudioSource` class provides a way to control audio playback in a Lime application.
|
||||
It allows for playing, pausing, and stopping audio, as well as controlling various
|
||||
audio properties such as gain, pitch, and looping.
|
||||
|
||||
Depending on the platform, the audio backend may vary, but the API remains consistent.
|
||||
|
||||
@see lime.media.AudioBuffer
|
||||
**/
|
||||
class AudioSource
|
||||
{
|
||||
private static var activeSources:Array<AudioSource> = [];
|
||||
|
||||
/**
|
||||
An event that is dispatched when the audio playback is complete.
|
||||
**/
|
||||
public var onComplete = new Event<Void->Void>();
|
||||
|
||||
/**
|
||||
An event that is dispatched when the audio playback looped.
|
||||
**/
|
||||
public var onLoop = new Event<Void->Void>();
|
||||
|
||||
/**
|
||||
The `AudioBuffer` associated with this `AudioSource`.
|
||||
**/
|
||||
public var buffer:AudioBuffer;
|
||||
|
||||
/**
|
||||
An property if this 'AudioSource' is playing.
|
||||
**/
|
||||
public var playing(get, null):Bool;
|
||||
|
||||
/**
|
||||
The current playback position of the audio, in milliseconds.
|
||||
**/
|
||||
public var currentTime(get, set):Float;
|
||||
|
||||
/**
|
||||
The gain (volume) of the audio. A value of `1.0` represents the default volume.
|
||||
**/
|
||||
public var gain(get, set):Float;
|
||||
public var length(get, set):Float;
|
||||
|
||||
/**
|
||||
The length of the audio, in milliseconds.
|
||||
**/
|
||||
public var length(get, set):Null<Float>;
|
||||
|
||||
/**
|
||||
The number of times the audio will loop. A value of `0` means the audio will not loop.
|
||||
**/
|
||||
public var loops(get, set):Int;
|
||||
|
||||
/**
|
||||
In which audio playback time the audio will loop.
|
||||
**/
|
||||
public var loopTime(get, set):Float;
|
||||
|
||||
/**
|
||||
The pitch of the audio. A value of `1.0` represents the default pitch.
|
||||
**/
|
||||
public var pitch(get, set):Float;
|
||||
public var offset:Int;
|
||||
|
||||
/**
|
||||
The offset within the audio buffer to start playback, in samples.
|
||||
**/
|
||||
public var offset:Float;
|
||||
|
||||
/**
|
||||
The 3D position of the audio source, represented as a `Vector4`.
|
||||
**/
|
||||
public var position(get, set):Vector4;
|
||||
|
||||
/**
|
||||
The latency of the audio source.
|
||||
**/
|
||||
public var latency(get, never):Float;
|
||||
|
||||
@:noCompletion private var __backend:AudioSourceBackend;
|
||||
|
||||
public function new(buffer:AudioBuffer = null, offset:Int = 0, length:Null<Int> = null, loops:Int = 0)
|
||||
/**
|
||||
Creates a new `AudioSource` instance.
|
||||
@param buffer The `AudioBuffer` to associate with this `AudioSource`.
|
||||
@param offset The starting offset within the audio buffer, in samples.
|
||||
@param length The length of the audio to play, in milliseconds. If `null`, the full buffer is used.
|
||||
@param loops The number of times to loop the audio. `0` means no looping.
|
||||
**/
|
||||
public function new(buffer:AudioBuffer = null, offset:Float = 0, length:Null<Float> = null, loops:Int = 0)
|
||||
{
|
||||
this.buffer = buffer;
|
||||
this.offset = offset;
|
||||
@@ -35,104 +106,136 @@ class AudioSource
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
this.loops = loops;
|
||||
|
||||
if (buffer != null)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
this.loops = loops;
|
||||
}
|
||||
|
||||
public function dispose():Void
|
||||
/**
|
||||
Releases any resources used by this `AudioSource`.
|
||||
**/
|
||||
inline public function dispose():Void
|
||||
{
|
||||
__backend.dispose();
|
||||
activeSources.remove(this);
|
||||
}
|
||||
|
||||
@:noCompletion private function init():Void
|
||||
@:noCompletion inline private function init():Void
|
||||
{
|
||||
__backend.init();
|
||||
activeSources.push(this);
|
||||
}
|
||||
|
||||
public function play():Void
|
||||
/**
|
||||
Starts or resumes audio playback.
|
||||
**/
|
||||
inline public function play():Void
|
||||
{
|
||||
__backend.play();
|
||||
}
|
||||
|
||||
public function pause():Void
|
||||
/**
|
||||
Pauses audio playback.
|
||||
**/
|
||||
inline public function pause():Void
|
||||
{
|
||||
__backend.pause();
|
||||
}
|
||||
|
||||
public function stop():Void
|
||||
/**
|
||||
Stops audio playback and resets the playback position to the beginning.
|
||||
**/
|
||||
inline public function stop():Void
|
||||
{
|
||||
__backend.stop();
|
||||
}
|
||||
|
||||
// Get & Set Methods
|
||||
@:noCompletion private function get_currentTime():Float
|
||||
@:noCompletion inline private function get_playing():Bool
|
||||
{
|
||||
@:privateAccess return __backend.playing;
|
||||
}
|
||||
|
||||
@:noCompletion inline private function get_currentTime():Float
|
||||
{
|
||||
return __backend.getCurrentTime();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_currentTime(value:Float):Float
|
||||
@:noCompletion inline private function set_currentTime(value:Float):Float
|
||||
{
|
||||
return __backend.setCurrentTime(value);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_gain():Float
|
||||
@:noCompletion inline private function get_gain():Float
|
||||
{
|
||||
return __backend.getGain();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_gain(value:Float):Float
|
||||
@:noCompletion inline private function set_gain(value:Float):Float
|
||||
{
|
||||
return __backend.setGain(value);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_length():Float
|
||||
@:noCompletion inline private function get_length():Null<Float>
|
||||
{
|
||||
return __backend.getLength();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_length(value:Float):Float
|
||||
@:noCompletion inline private function set_length(value:Null<Float>):Null<Float>
|
||||
{
|
||||
return __backend.setLength(value);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_loops():Int
|
||||
@:noCompletion inline private function get_loops():Int
|
||||
{
|
||||
return __backend.getLoops();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_loops(value:Int):Int
|
||||
@:noCompletion inline private function set_loops(value:Int):Int
|
||||
{
|
||||
return __backend.setLoops(value);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_pitch():Float
|
||||
@:noCompletion inline private function get_loopTime():Float
|
||||
{
|
||||
return __backend.getLoopTime();
|
||||
}
|
||||
|
||||
@:noCompletion inline private function set_loopTime(value:Float):Float
|
||||
{
|
||||
return __backend.setLoopTime(value);
|
||||
}
|
||||
|
||||
@:noCompletion inline private function get_pitch():Float
|
||||
{
|
||||
return __backend.getPitch();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_pitch(value:Float):Float
|
||||
@:noCompletion inline private function set_pitch(value:Float):Float
|
||||
{
|
||||
return __backend.setPitch(value);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_position():Vector4
|
||||
@:noCompletion inline private function get_position():Vector4
|
||||
{
|
||||
return __backend.getPosition();
|
||||
}
|
||||
|
||||
@:noCompletion private function set_position(value:Vector4):Vector4
|
||||
@:noCompletion inline private function set_position(value:Vector4):Vector4
|
||||
{
|
||||
return __backend.setPosition(value);
|
||||
}
|
||||
|
||||
@:noCompletion inline private function get_latency():Float
|
||||
{
|
||||
return __backend.getLatency();
|
||||
}
|
||||
}
|
||||
|
||||
#if flash
|
||||
@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.flash.FlashAudioSource;
|
||||
#elseif (js && html5)
|
||||
#if (js && html5)
|
||||
@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.html5.HTML5AudioSource;
|
||||
#else
|
||||
@:noCompletion private typedef AudioSourceBackend = lime._internal.backend.native.NativeAudioSource;
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
package lime.media.vorbis;
|
||||
|
||||
#if (!lime_doc_gen || lime_vorbis)
|
||||
import haxe.Int64;
|
||||
import haxe.io.Bytes;
|
||||
import lime._internal.backend.native.NativeCFFI;
|
||||
|
||||
#if hl
|
||||
@:keep
|
||||
#end
|
||||
@:access(lime._internal.backend.native.NativeCFFI)
|
||||
class VorbisFile
|
||||
{
|
||||
public var bitstream(default, null):Int;
|
||||
|
||||
@:noCompletion private var handle:Dynamic;
|
||||
|
||||
@:noCompletion var _filePath:String;
|
||||
@:noCompletion var _bytes:Bytes;
|
||||
|
||||
@:noCompletion private function new(handle:Dynamic)
|
||||
{
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
public function bitrate(bitstream:Int = -1):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_bitrate(handle, bitstream);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function bitrateInstant():Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_bitrate_instant(handle);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function clear():Void
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
NativeCFFI.lime_vorbis_file_clear(handle);
|
||||
#end
|
||||
handle = null;
|
||||
_filePath = null;
|
||||
_bytes = null;
|
||||
}
|
||||
|
||||
public function clone():VorbisFile
|
||||
{
|
||||
if (_filePath != null) return fromFile(_filePath);
|
||||
if (_bytes != null) return fromBytes(_bytes);
|
||||
return null;
|
||||
}
|
||||
|
||||
public function comment(bitstream:Int = -1):VorbisComment
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_comment(handle, bitstream);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
var comment = new VorbisComment();
|
||||
comment.userComments = data.userComments;
|
||||
comment.vendor = data.vendor;
|
||||
return comment;
|
||||
}
|
||||
#end
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function crosslap(other:VorbisFile):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_crosslap(handle, other.handle);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public static function fromBytes(bytes:Bytes):VorbisFile
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var handle = NativeCFFI.lime_vorbis_file_from_bytes(bytes);
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
var vorbisFile = new VorbisFile(handle);
|
||||
vorbisFile._bytes = bytes;
|
||||
return vorbisFile;
|
||||
}
|
||||
#end
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function fromFile(path:String):VorbisFile
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var handle = NativeCFFI.lime_vorbis_file_from_file(path);
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
var vorbisFile = new VorbisFile(handle);
|
||||
vorbisFile._filePath = path;
|
||||
return vorbisFile;
|
||||
}
|
||||
#end
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function info(bitstream:Int = -1):VorbisInfo
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_info(handle, bitstream);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
var info = new VorbisInfo();
|
||||
info.bitrateLower = data.bitrateLower;
|
||||
info.bitrateNominal = data.bitrateNominal;
|
||||
info.bitrateUpper = data.bitrateUpper;
|
||||
info.channels = data.channels;
|
||||
info.rate = data.rate;
|
||||
info.version = data.version;
|
||||
return info;
|
||||
}
|
||||
#end
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function pcmSeek(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_pcm_seek(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function pcmSeekLap(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_pcm_seek_lap(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function pcmSeekPage(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_pcm_seek_page(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function pcmSeekPageLap(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_pcm_seek_page_lap(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function pcmTell():Int64
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_pcm_tell(handle);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
return Int64.make(data.high, data.low);
|
||||
}
|
||||
#end
|
||||
|
||||
return Int64.ofInt(0);
|
||||
}
|
||||
|
||||
public function pcmTotal(bitstream:Int = -1):Int64
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_pcm_total(handle, bitstream);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
return Int64.make(data.high, data.low);
|
||||
}
|
||||
#end
|
||||
|
||||
return Int64.ofInt(0);
|
||||
}
|
||||
|
||||
public function rawSeek(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_raw_seek(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function rawSeekLap(pos:Int64):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_raw_seek_lap(handle, pos.low, pos.high);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function rawTell():Int64
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_raw_tell(handle);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
return Int64.make(data.high, data.low);
|
||||
}
|
||||
#end
|
||||
|
||||
return Int64.ofInt(0);
|
||||
}
|
||||
|
||||
public function rawTotal(bitstream:Int = -1):Int64
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_raw_total(handle, bitstream);
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
return Int64.make(data.high, data.low);
|
||||
}
|
||||
#end
|
||||
|
||||
return Int64.ofInt(0);
|
||||
}
|
||||
|
||||
public function read(buffer:Bytes, position:Int, length:Int = 4096, bigEndianPacking:Bool = false, wordSize:Int = 2, signed:Bool = true):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_read(handle, buffer, position, length, bigEndianPacking, wordSize, signed);
|
||||
if (data == null) return 0;
|
||||
bitstream = data.bitstream;
|
||||
return data.returnValue;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
// public function readFilter (buffer:Bytes, length:Int = 4096, endianness:Endian = LITTLE_ENDIAN, wordSize:Int = 2, signed:Bool = true, bitstream:Int = 0, filter, filter_param
|
||||
public function readFloat(pcmChannels:Bytes, samples:Int):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
var data = NativeCFFI.lime_vorbis_file_read_float(handle, pcmChannels, samples);
|
||||
if (data == null) return 0;
|
||||
bitstream = data.bitstream;
|
||||
return data.returnValue;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function seekable():Bool
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_seekable(handle);
|
||||
#else
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
public function serialNumber(bitstream:Int = -1):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_serial_number(handle, bitstream);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function streams():Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_streams(handle);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeSeek(s:Float):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_seek(handle, s);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeSeekLap(s:Float):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_seek_lap(handle, s);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeSeekPage(s:Float):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_seek_page(handle, s);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeSeekPageLap(s:Float):Int
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_seek_page_lap(handle, s);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeTell():Float
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_tell(handle);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
public function timeTotal(bitstream:Int = -1):Float
|
||||
{
|
||||
#if (lime_cffi && lime_vorbis && !macro)
|
||||
return NativeCFFI.lime_vorbis_file_time_total(handle, bitstream);
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -599,17 +599,6 @@ class Sound extends EventDispatcher
|
||||
#end
|
||||
}
|
||||
|
||||
var changeID:Int = 0;
|
||||
public function regen() {
|
||||
var audioBuffer = new AudioBuffer();
|
||||
audioBuffer.bitsPerSample = __buffer.bitsPerSample;
|
||||
audioBuffer.channels = __buffer.channels;
|
||||
audioBuffer.data = __buffer.data;
|
||||
audioBuffer.sampleRate = __buffer.sampleRate;
|
||||
|
||||
__buffer = audioBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
Generates a new SoundChannel object to play back the sound. This method
|
||||
returns a SoundChannel object, which you access to stop the sound and to
|
||||
@@ -636,13 +625,6 @@ class Sound extends EventDispatcher
|
||||
return null;
|
||||
}
|
||||
|
||||
#if !macro
|
||||
if (changeID < funkin.backend.system.Main.changeID) {
|
||||
changeID = funkin.backend.system.Main.changeID;
|
||||
regen();
|
||||
}
|
||||
#end
|
||||
|
||||
if (sndTransform == null)
|
||||
{
|
||||
sndTransform = new SoundTransform();
|
||||
@@ -690,26 +672,10 @@ class Sound extends EventDispatcher
|
||||
#if (js && html5 && howlerjs)
|
||||
return __buffer.src.duration() * 1000;
|
||||
#else
|
||||
if (__buffer.data != null)
|
||||
{
|
||||
// var samples = (__buffer.data.length * 8) / (__buffer.channels * __buffer.bitsPerSample);
|
||||
// return Std.int(samples / __buffer.sampleRate * 1000);
|
||||
var samples = (Int64.make(0, __buffer.data.length) * Int64.ofInt(8)) / Int64.ofInt(__buffer.channels * __buffer.bitsPerSample);
|
||||
var div = Int64.divMod(samples * Int64.ofInt(1000), Int64.ofInt(__buffer.sampleRate));
|
||||
var value = (div.quotient + (div.modulus / __buffer.sampleRate));
|
||||
return Int64.toInt(value);
|
||||
}
|
||||
else if (__buffer.__srcVorbisFile != null)
|
||||
{
|
||||
//var samples = Int64.toInt(__buffer.__srcVorbisFile.pcmTotal());
|
||||
//return Std.int(samples / __buffer.sampleRate * 1000);
|
||||
var samples = __buffer.__srcVorbisFile.pcmTotal();
|
||||
var value = Int64.fromFloat(__buffer.__srcVorbisFile.timeTotal()) * 1000;
|
||||
return Int64.toInt(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
if (__buffer.data != null) return (__buffer.data.length >> 0) / __buffer.channels / (__buffer.bitsPerSample >> 3) / __buffer.sampleRate * 1000;
|
||||
else if (__buffer.__srcVorbisFile != null) {
|
||||
var x = __buffer.__srcVorbisFile.pcmTotal();
|
||||
return (x.high * 4294967296. + (x.low >> 0)) / __buffer.sampleRate * 1000;
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package openfl.media;
|
||||
|
||||
#if !flash
|
||||
import haxe.Int64;
|
||||
|
||||
import openfl.events.Event;
|
||||
import openfl.events.EventDispatcher;
|
||||
#if lime
|
||||
import lime.media.AudioSource;
|
||||
import lime.media.openal.AL;
|
||||
#end
|
||||
|
||||
/**
|
||||
@@ -16,11 +19,19 @@ import lime.media.AudioSource;
|
||||
object to the channel.
|
||||
|
||||
@event soundComplete Dispatched when a sound has finished playing.
|
||||
|
||||
@see [Playing sounds](https://books.openfl.org/openfl-developers-guide/working-with-sound/playing-sounds.html)
|
||||
@see `openfl.media.Sound`
|
||||
**/
|
||||
#if !openfl_debug
|
||||
@:fileXml('tags="haxe,release"')
|
||||
@:noDebug
|
||||
#end
|
||||
#if (lime && lime_cffi)
|
||||
@:access(lime._internal.backend.native.NativeAudioSource)
|
||||
@:access(lime.media.AudioSource)
|
||||
#end
|
||||
@:access(openfl.media.Sound)
|
||||
@:access(openfl.media.SoundMixer)
|
||||
@:final @:keep class SoundChannel extends EventDispatcher
|
||||
{
|
||||
@@ -28,7 +39,13 @@ import lime.media.AudioSource;
|
||||
The current amplitude(volume) of the left channel, from 0(silent) to 1
|
||||
(full amplitude).
|
||||
**/
|
||||
public var leftPeak(default, null):Float;
|
||||
public var leftPeak(get, null):Float;
|
||||
|
||||
/**
|
||||
The current amplitude(volume) of the right channel, from 0(silent) to 1
|
||||
(full amplitude).
|
||||
**/
|
||||
public var rightPeak(get, null):Float;
|
||||
|
||||
/**
|
||||
When the sound is playing, the `position` property indicates in
|
||||
@@ -45,12 +62,6 @@ import lime.media.AudioSource;
|
||||
**/
|
||||
public var position(get, set):Float;
|
||||
|
||||
/**
|
||||
The current amplitude(volume) of the right channel, from 0(silent) to 1
|
||||
(full amplitude).
|
||||
**/
|
||||
public var rightPeak(default, null):Float;
|
||||
|
||||
/**
|
||||
The SoundTransform object assigned to the sound channel. A SoundTransform
|
||||
object includes properties for setting volume, panning, left speaker
|
||||
@@ -58,13 +69,23 @@ import lime.media.AudioSource;
|
||||
**/
|
||||
public var soundTransform(get, set):SoundTransform;
|
||||
|
||||
/**
|
||||
self explanatory
|
||||
*/
|
||||
public var loopTime(get, set):Float;
|
||||
public var endTime(get, set):Null<Float>;
|
||||
public var pitch(get, set):Float;
|
||||
public var loops(get, set):Int;
|
||||
|
||||
@:noCompletion private var __sound:Sound;
|
||||
@:noCompletion private var __isValid:Bool;
|
||||
@:noCompletion private var __soundTransform:SoundTransform;
|
||||
@:noCompletion private var __lastPeakTime:Float;
|
||||
@:noCompletion private var __leftPeak:Float;
|
||||
@:noCompletion private var __rightPeak:Float;
|
||||
#if lime
|
||||
@:noCompletion private var __source:AudioSource;
|
||||
@:noCompletion private var __audioSource(get, set):AudioSource;
|
||||
@:noCompletion private var __audioSource(get, set):AudioSource; // forward??? compatibility??
|
||||
#end
|
||||
|
||||
#if openfljs
|
||||
@@ -87,28 +108,10 @@ import lime.media.AudioSource;
|
||||
{
|
||||
super(this);
|
||||
|
||||
leftPeak = 1;
|
||||
rightPeak = 1;
|
||||
if (soundTransform != null) __soundTransform = soundTransform;
|
||||
else __soundTransform = new SoundTransform();
|
||||
|
||||
if (soundTransform != null)
|
||||
{
|
||||
__soundTransform = soundTransform;
|
||||
}
|
||||
else
|
||||
{
|
||||
__soundTransform = new SoundTransform();
|
||||
}
|
||||
|
||||
#if lime
|
||||
if (source != null)
|
||||
{
|
||||
__source = source;
|
||||
__source.onComplete.add(source_onComplete);
|
||||
__isValid = true;
|
||||
|
||||
__source.play();
|
||||
}
|
||||
#end
|
||||
__initAudioSource(source);
|
||||
|
||||
SoundMixer.__registerSoundChannel(this);
|
||||
}
|
||||
@@ -134,6 +137,7 @@ import lime.media.AudioSource;
|
||||
|
||||
#if lime
|
||||
__source.onComplete.remove(source_onComplete);
|
||||
__source.onLoop.remove(source_onLoop);
|
||||
__source.dispose();
|
||||
__source = null;
|
||||
#end
|
||||
@@ -145,6 +149,90 @@ import lime.media.AudioSource;
|
||||
this.soundTransform = soundTransform;
|
||||
}
|
||||
|
||||
@:noCompletion private function __updatePeaks(time:Float):Bool
|
||||
{
|
||||
if (Math.abs(time - __lastPeakTime) < 8) return false;
|
||||
|
||||
__lastPeakTime = time;
|
||||
__leftPeak = __rightPeak = 0;
|
||||
|
||||
#if !macro
|
||||
if (!__isValid) return false;
|
||||
|
||||
var buffer = __source.buffer;
|
||||
var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1);
|
||||
var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize), size = 0, buf;
|
||||
|
||||
#if lime_cffi
|
||||
var backend = __source.__backend, i = 0;
|
||||
if (backend.streamed) {
|
||||
size = backend.bufferSizes[i = backend.bufferSizes.length - backend.queuedBuffers];
|
||||
buf = backend.bufferDatas[i].buffer;
|
||||
pos -= Math.floor(backend.bufferTimes[i] * buffer.sampleRate * buffer.channels * wordSize);
|
||||
}
|
||||
else
|
||||
#end {
|
||||
buf = buffer.data #if !js .buffer #end;
|
||||
size = #if js buf.byteLength #else buf.length #end;
|
||||
}
|
||||
|
||||
var s = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0;
|
||||
pos -= pos % wordSize;
|
||||
|
||||
while (s > 0) {
|
||||
if (wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - bitSize;
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= #if js buf[pos] #else buf.get(pos) #end << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
}
|
||||
if (b > bitSize) b -= bitUnsignedSize;
|
||||
}
|
||||
((c % 2 == 0) ? (if (__leftPeak < b) __leftPeak = b) : (if (__rightPeak < b) __rightPeak = b));
|
||||
|
||||
if (pos >= size) #if lime_cffi {
|
||||
if (!backend.streamed || ++i >= backend.bufferDatas.length) break;
|
||||
size = backend.bufferSizes[i];
|
||||
buf = backend.bufferDatas[i].buffer;
|
||||
pos = 0;
|
||||
}
|
||||
#else break; #end
|
||||
|
||||
if (++c > buffer.channels) {
|
||||
c = 0;
|
||||
s--;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
|
||||
if (buffer.channels == 1) __rightPeak = __leftPeak / bitSize;
|
||||
else {
|
||||
__leftPeak = __leftPeak / bitSize;
|
||||
__rightPeak = __rightPeak / bitSize;
|
||||
}
|
||||
#end
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@:noCompletion private function __initAudioSource(source:#if lime AudioSource #else Dynamic #end):Void
|
||||
{
|
||||
#if lime
|
||||
__source = source;
|
||||
if (__source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
__source.onComplete.add(source_onComplete);
|
||||
__source.onLoop.add(source_onLoop);
|
||||
__isValid = true;
|
||||
|
||||
__source.play();
|
||||
#end
|
||||
}
|
||||
|
||||
// Get & Set Methods
|
||||
@:noCompletion private function get_position():Float
|
||||
{
|
||||
@@ -167,27 +255,6 @@ import lime.media.AudioSource;
|
||||
return value;
|
||||
}
|
||||
|
||||
@:noCompletion private function get_pitch():Float
|
||||
{
|
||||
if (!__isValid) return 1;
|
||||
|
||||
#if lime
|
||||
return __source.pitch;
|
||||
#else
|
||||
return 1;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function set_pitch(value:Float):Float
|
||||
{
|
||||
if (!__isValid) return 1;
|
||||
|
||||
#if lime
|
||||
__source.pitch = value;
|
||||
#end
|
||||
return value;
|
||||
}
|
||||
|
||||
@:noCompletion private function get_soundTransform():SoundTransform
|
||||
{
|
||||
return __soundTransform.clone();
|
||||
@@ -203,7 +270,7 @@ import lime.media.AudioSource;
|
||||
var pan = SoundMixer.__soundTransform.pan + __soundTransform.pan;
|
||||
|
||||
if (pan < -1) pan = -1;
|
||||
else if (pan > 1) pan = 1;
|
||||
if (pan > 1) pan = 1;
|
||||
|
||||
var volume = SoundMixer.__soundTransform.volume * __soundTransform.volume;
|
||||
|
||||
@@ -225,6 +292,107 @@ import lime.media.AudioSource;
|
||||
return value;
|
||||
}
|
||||
|
||||
@:noCompletion private function get_pitch():Float
|
||||
{
|
||||
if (!__isValid) return 1;
|
||||
|
||||
#if lime
|
||||
return __source.pitch;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function set_pitch(value:Float):Float
|
||||
{
|
||||
if (!__isValid) return 1;
|
||||
|
||||
#if lime
|
||||
return __source.pitch = value;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function get_loopTime():Float
|
||||
{
|
||||
if (!__isValid) return -1;
|
||||
|
||||
#if lime
|
||||
return __source.loopTime;
|
||||
#else
|
||||
return -1;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function set_loopTime(value:Float):Float
|
||||
{
|
||||
if (!__isValid) return -1;
|
||||
|
||||
#if lime
|
||||
return __source.loopTime = value;
|
||||
#else
|
||||
return -1;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function get_endTime():Null<Float>
|
||||
{
|
||||
if (!__isValid) return null;
|
||||
|
||||
#if lime
|
||||
return __source.length;
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function set_endTime(value:Null<Float>):Null<Float>
|
||||
{
|
||||
if (!__isValid) return null;
|
||||
|
||||
#if lime
|
||||
return __source.length = value;
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function get_loops():Int
|
||||
{
|
||||
if (!__isValid) return 0;
|
||||
|
||||
#if lime
|
||||
return __source.loops;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function set_loops(value:Int):Int
|
||||
{
|
||||
if (!__isValid) return 0;
|
||||
|
||||
#if lime
|
||||
return __source.loops = value;
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion private function get_leftPeak():Float
|
||||
{
|
||||
__updatePeaks(get_position());
|
||||
return __leftPeak * (soundTransform == null ? 1 : soundTransform.volume);
|
||||
}
|
||||
|
||||
@:noCompletion private function get_rightPeak():Float
|
||||
{
|
||||
__updatePeaks(get_position());
|
||||
return __rightPeak * (soundTransform == null ? 1 : soundTransform.volume);
|
||||
}
|
||||
|
||||
|
||||
// Event Handlers
|
||||
@:noCompletion private function source_onComplete():Void
|
||||
{
|
||||
@@ -234,14 +402,14 @@ import lime.media.AudioSource;
|
||||
dispatchEvent(new Event(Event.SOUND_COMPLETE));
|
||||
}
|
||||
|
||||
function set___audioSource(value:AudioSource):AudioSource {
|
||||
return __source = value;
|
||||
@:noCompletion private function source_onLoop():Void
|
||||
{
|
||||
//dispatchEvent(new Event(Event.SOUND_LOOP));
|
||||
}
|
||||
|
||||
function get___audioSource():AudioSource {
|
||||
return __source;
|
||||
}
|
||||
@:noCompletion private function get___audioSource():AudioSource return __source;
|
||||
@:noCompletion private function set___audioSource(source:AudioSource):AudioSource return __source = source;
|
||||
}
|
||||
#else
|
||||
typedef SoundChannel = flash.media.SoundChannel;
|
||||
#end
|
||||
#end
|
||||
|
||||
@@ -243,16 +243,15 @@ class Assets
|
||||
|
||||
public static function getMusic(id:String, useCache:Bool = true):Sound
|
||||
{
|
||||
#if (lime_vorbis && lime > "7.9.0")
|
||||
var path = getPath(id);
|
||||
// TODO: What if it is a WAV or non-Vorbis file?
|
||||
var vorbisFile = VorbisFile.fromFile(path);
|
||||
var buffer = AudioBuffer.fromVorbisFile(vorbisFile);
|
||||
return Sound.fromAudioBuffer(buffer);
|
||||
#else
|
||||
// TODO: Streaming sound
|
||||
return getSound(id, useCache);
|
||||
#if (lime_vorbis && lime > "7.9.0" && !macro)
|
||||
if (Options.streamedMusic) {
|
||||
var path = getPath(id);
|
||||
// TODO: What if it is a WAV or non-Vorbis file?
|
||||
var vorbisFile = VorbisFile.fromFile(path);
|
||||
if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile));
|
||||
}
|
||||
#end
|
||||
return getSound(id, useCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user