Before anyone asks why vs cam, the code is public, plus i helped most of the audio backend rewrite in the mod too. :P - Static sounds can now also seamlessly loop with AL_SOFT_loop_points extension - Panning stereo sounds is now possible with AL_EXT_STEREO_ANGLES extension (although this was already implemented before) - Improved streaming sounds, should fix most of the lagspike issues - Streaming sounds should no longer regenrate if it's in the nearest timestamp (not for reversing though) ! You still can't access FlxSound.latency because of the new procedures that's not available in lime If there's any crashes with this pls report an issue asap.
965 lines
26 KiB
Haxe
965 lines
26 KiB
Haxe
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.FlxDestroyUtil;
|
|
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;
|
|
|
|
/**
|
|
* Controls how much this object is affected by camera scrolling. `0` = no movement (e.g. a static sound),
|
|
* This is only useful if used with proximity (Initialized once proximity is used).
|
|
* @since raltyMod
|
|
*/
|
|
public var scrollFactor(default, null):FlxPoint;
|
|
|
|
/**
|
|
* 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 _pan:Float;
|
|
var _panAdjust: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();
|
|
onFinish.removeAll();
|
|
|
|
x = y = 0;
|
|
@:bypassAccessor muted = false;
|
|
@:bypassAccessor looped = false;
|
|
@:bypassAccessor loopTime = 0;
|
|
@:bypassAccessor 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
|
|
|
|
if (_transform == null) _transform = new SoundTransform();
|
|
}
|
|
|
|
/**
|
|
* Internal cleanup function for cleaning up this FlxSound.
|
|
* @since raltyMod
|
|
*/
|
|
function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess {
|
|
active = false;
|
|
_lastTime = null;
|
|
|
|
if (destroySound) {
|
|
scrollFactor = FlxDestroyUtil.put(scrollFactor);
|
|
|
|
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;
|
|
|
|
if (autoDestroy) persist = false;
|
|
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;
|
|
|
|
// Distance-based volume control
|
|
if (target != null) {
|
|
var targetPosition = target.getPosition(), position = getPosition();
|
|
var camera = camera;
|
|
if (camera != null) {
|
|
targetPosition.subtract(camera.scroll.x * target.scrollFactor.x, camera.scroll.y * target.scrollFactor.y);
|
|
if (scrollFactor != null) position.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
|
|
else position.subtract(camera.scroll.x, camera.scroll.y);
|
|
}
|
|
|
|
var radialMultiplier = targetPosition.distanceTo(position) / radius;
|
|
|
|
// Make it so it affects the 3d position of the source and not just the panning?
|
|
_volumeAdjust = 1 - FlxMath.bound(radialMultiplier, 0, 1);
|
|
if (proximityPan) _panAdjust = (position.x - targetPosition.x) / radius;
|
|
|
|
targetPosition.put();
|
|
position.put();
|
|
}
|
|
else
|
|
_volumeAdjust = 1.0;
|
|
|
|
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);
|
|
}
|
|
_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() {
|
|
_transform.volume = calcTransformVolume();
|
|
_transform.pan = _pan + _panAdjust;
|
|
if (_channel != null) _channel.soundTransform = _transform;
|
|
}
|
|
|
|
public function calcTransformVolume():Float {
|
|
if (muted) return 0.0;
|
|
|
|
#if FLX_SOUND_SYSTEM
|
|
if (FlxG.sound.muted) return 0.0;
|
|
|
|
// TODO: when flixel-cne is updated, enable this
|
|
//return FlxG.sound.applySoundCurve(FlxG.sound.volume * volume);
|
|
return FlxG.sound.volume * getActualVolume();
|
|
#else
|
|
return getActualVolume();
|
|
#end
|
|
}
|
|
|
|
/**
|
|
* 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, ?scrollFactor:FlxPoint):FlxSound {
|
|
setPosition(x, y);
|
|
if (targetObject != null) this.target = targetObject;
|
|
if (radius != null) this.radius = radius;
|
|
proximityPan = pan;
|
|
|
|
if (this.scrollFactor == null) this.scrollFactor = FlxPoint.get(1, 1);
|
|
if (scrollFactor != null) this.scrollFactor.copyFrom(scrollFactor);
|
|
|
|
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 function setPosition(x = 0.0, y = 0.0):Void {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
|
|
/**
|
|
* Returns the world position of this object.
|
|
*
|
|
* @param result Optional arg for the returning point.
|
|
* @return The world position of this object.
|
|
* @since raltyMod
|
|
*/
|
|
public function getPosition(?result:FlxPoint):FlxPoint {
|
|
if (result == null)
|
|
result = FlxPoint.get();
|
|
|
|
return result.set(x, 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 (group != null ? group.getVolume() : 1.0) * _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 == null #if lime_cffi || _source.__backend.disposed #end)
|
|
makeChannel();
|
|
|
|
if (_channel != null) {
|
|
#if FLX_PITCH
|
|
_timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0;
|
|
pitch = _pitch;
|
|
#end
|
|
|
|
updateTransform();
|
|
_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_pan():Float return _pan;
|
|
inline function set_pan(v:Float):Float {
|
|
_pan = FlxMath.bound(v, -1, 1);
|
|
updateTransform();
|
|
return _pan;
|
|
}
|
|
|
|
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 getFakeTime():Float {
|
|
if (_source.playing && _realPitch > 0 && _lastTime != null)
|
|
return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation;
|
|
else
|
|
return _time;
|
|
}
|
|
function get_time():Float {
|
|
if (_channel == null || @:privateAccess !_channel.__isValid || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected) return _time;
|
|
|
|
final sourceTime = _source.currentTime - _source.offset - _offset;
|
|
if (!_source.playing || _realPitch <= 0) {
|
|
_lastTime = null;
|
|
return _time = sourceTime;
|
|
}
|
|
|
|
final fakeTime = getFakeTime();
|
|
if (sourceTime != _time) {
|
|
_lastTime = FlxG.game.getTicks();
|
|
if ((_timeInterpolation = 1 - Math.min(fakeTime - sourceTime, 1000) * 0.001) < 1 && _timeInterpolation > .9)
|
|
return _time = fakeTime;
|
|
else {
|
|
_timeInterpolation = 1;
|
|
return _time = sourceTime;
|
|
}
|
|
}
|
|
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 {
|
|
_source.offset = 0;
|
|
_source.currentTime = time + _offset;
|
|
}
|
|
}
|
|
|
|
_lastTime = null;
|
|
return _time = time;
|
|
}
|
|
|
|
function get_offset():Float return _offset;
|
|
function set_offset(offset:Float):Float {
|
|
if (_offset == (_offset = offset)) return offset;
|
|
//time = time + _offset;
|
|
return offset;
|
|
}
|
|
|
|
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)
|
|
]);
|
|
}
|
|
} |