diff --git a/flixel/FlxGame.hx b/flixel/FlxGame.hx index 7483f4e27b..88e4759825 100644 --- a/flixel/FlxGame.hx +++ b/flixel/FlxGame.hx @@ -548,6 +548,11 @@ class FlxGame extends Sprite // we need to clear bitmap cache only after previous state is destroyed, which will reset useCount for FlxGraphic objects FlxG.bitmap.clearCache(); + // sound datas too. + #if FLX_SOUND_SYSTEM + FlxG.sound.clearCache(); + #end + // Finally assign and create the new state _state = _nextState.createInstance(); _state._constructor = _nextState.getConstructor(); diff --git a/flixel/sound/FlxSound.hx b/flixel/sound/FlxSound.hx index ceec469da5..b571ee5ea8 100644 --- a/flixel/sound/FlxSound.hx +++ b/flixel/sound/FlxSound.hx @@ -1,12 +1,9 @@ package flixel.sound; -import flixel.FlxBasic; -import flixel.FlxG; -import flixel.math.FlxMath; -import flixel.math.FlxPoint; -import flixel.system.FlxAssets.FlxSoundAsset; -import flixel.tweens.FlxTween; -import flixel.util.FlxStringUtil; +import lime.media.AudioBuffer; +import lime.media.AudioEffect; +import lime.media.AudioSource; + import openfl.events.Event; import openfl.events.IEventDispatcher; import openfl.media.Sound; @@ -15,809 +12,1460 @@ import openfl.media.SoundTransform; import openfl.net.URLRequest; import openfl.utils.ByteArray; +import flixel.FlxBasic; +import flixel.FlxG; +import flixel.FlxObject; +import flixel.math.FlxMath; +import flixel.math.FlxPoint; +import flixel.system.FlxAssets.FlxSoundAsset; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxSignal; +import flixel.util.FlxStringUtil; + /** * This is the universal flixel sound object, used for streaming, music, and sound effects. */ +@:access(lime.media.AudioBuffer) +@:access(lime.media.AudioSource) +@:access(openfl.media.Sound) +@:access(flixel.FlxGame) +@:allow(flixel.system.frontEnds.SoundFrontEnd) class FlxSound extends FlxBasic { + #if FLX_PITCH /** - * The x position of this sound in world coordinates. - * Only really matters if you are doing proximity/panning stuff. + * The default value for the `timeScaledPitch` variable at creation if none is specified in the constructor. */ - public var x:Float; - + public static var defaultTimeScaledPitch:Bool = false; + #end + /** - * The y position of this sound in world coordinates. - * Only really matters if you are doing proximity/panning stuff. + * Plays all of FlxSound instance at the same time. + * + * @param sounds Array list of FlxSounds to play or resume. + * @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. */ - public var y:Float; - + public static function playSounds(sounds:Array):Void + { + var sources:Array = []; + + for (sound in sounds) + { + if (sound == null || !sound.exists) continue; + + if (sound._pausedByHandler) sound.resume(); + else + { + sources.push(sound.source); + + sound._updateVolume(); + #if FLX_PITCH + sound._updatePitch(); + #end + sound._updatePan(); + sound._updateLoop(); + + sound._paused = false; + sound._completed = false; + sound.active = false; + } + } + + AudioSource.playSources(sources); + } + /** - * Whether or not this sound should be automatically destroyed when you switch states. + * Pauses all of FlxSound instance at the same time. + * + * @param sounds Array list of FlxSounds to pause. */ - public var persist:Bool; - + public static function pauseSounds(sounds:Array):Void + { + var sources:Array = []; + + for (sound in sounds) + { + if (sound == null || !sound.exists || !sound.playing) continue; + + sources.push(sound.source); + + sound.get_time(); + sound._timeTicks = null; + sound._pausedByHandler = false; + sound._pausedPlay = false; + sound._paused = true; + sound.active = false; + } + + AudioSource.pauseSources(sources); + } + + /** + * Stops all of FlxSound instance at the same time. + * + * @param sounds Array list of FlxSounds to pause. + */ + public static function stopSounds(sounds:Array):Void + { + var sources:Array = []; + + for (sound in sounds) + { + if (sound == null || !sound.exists || !sound.playing) continue; + + sound._timeTicks = null; + sound._pausedByHandler = false; + sound._pausedPlay = false; + sound._paused = true; + sound.active = false; + + if (sound.autoDestroy) sound.kill(); + else sources.push(sound.source); + } + + AudioSource.pauseSources(sources); + } + + //#if flash /** * The ID3 song name. Defaults to null. Currently only works for streamed sounds. */ public var name(default, null):String; - + /** * The ID3 artist name. Defaults to null. Currently only works for streamed sounds. */ public var artist(default, null):String; - + //#end + /** - * Stores the average wave amplitude of both stereo channels + * Whether or not this sound should be automatically destroyed when you switch states. */ - public var amplitude(default, null):Float; - + public var persist:Bool; + /** - * Just the amplitude of the left stereo channel + * Whether to call `destroy()` when the sound has finished playing. + * since FunkinCrew's Flixel, internally it calls `kill()` to be reused again by FlxG.sound.load. */ - public var amplitudeLeft(default, null):Float; - + public var autoDestroy:Bool; + /** - * Just the amplitude of the right stereo channel + * Whether or not this audio should have proximity stuff. + * @since FunkinCrew's Flixel */ - public var amplitudeRight(default, null):Float; - + public var proximityEnabled(default, set):Bool; + /** - * Whether to call `destroy()` when the sound has finished playing. + * Whether the proximity alters the pan or not. + * Default is true. + * @since FunkinCrew's Flixel */ - public var autoDestroy:Bool; - + public var proximityPan(default, set):Bool; + /** - * Tracker for sound complete callback. If assigned, will be called - * each time when sound reaches its end. + * The x position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. */ - public var onComplete:Void->Void; - + public var x:Float; + /** - * Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this. - * - * Note: On desktop targets this only works with mono sounds, due to limitations of OpenAL. - * More info: [OpenFL Forums - SoundTransform.pan does not work](https://community.openfl.org/t/windows-legacy-soundtransform-pan-does-not-work/6616/2?u=geokureli) + * The y position of this sound in world coordinates. + * Only really matters if you are doing proximity/panning stuff. */ - public var pan(get, set):Float; - + public var y:Float; + + /** + * 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). + * Default is 0, 0. + * @since FunkinCrew's Flixel + */ + public var scrollFactor(default, null):FlxPoint; + + /** + * The sound's "target" (for proximity and panning). + */ + public var target:Null; + + /** + * The maximum effective radius of this sound (for proximity and panning). + * Default is 1024. + * @since FunkinCrew's Flixel + */ + public var radius:Float; + + /** + * The sound group this sound belongs to, can only be in one group. + * NOTE: This setter is deprecated, use `group.add(sound)` or `group.remove(sound)`. + */ + public var group(default, set):FlxSoundGroup; + /** * 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. + * Whether or not the sound is currently paused. + * @since FunkinCrew's Flixel + */ + public var paused(get, never):Bool; + + /** + * Whether or not the sound has been completed. + * @since FunkinCrew's Flixel + */ + public var completed(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. + * @since FunkinCrew's Flixel + */ + public var muted(get, 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. + * @since FunkinCrew's Flixel + */ + public var timeScaledPitch(get, set):Bool; #end - + /** - * The position in runtime of the music playback in milliseconds. - * If set while paused, changes only come into effect after a `resume()` call. + * Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this. */ - public var time(get, set):Float; - + public var pan(get, set):Float; + /** - * The length of the sound in milliseconds. + * The duration/length of the sound in milliseconds. * @since 4.2.0 */ public var length(get, never):Float; - + /** - * The sound group this sound belongs to, can only be in one group. - * NOTE: This setter is deprecated, use `group.add(sound)` or `group.remove(sound)`. + * Whether or not this sound should loop. */ - public var group(default, set):FlxSoundGroup; - + public var looped(get, set):Bool; + /** - * Whether or not this sound should loop. + * The number of times this sound was restarted, via the `looped` flag. + * Automatically incremented on loops, and reset to 0 when restarted. + * @since 6.2.0 + */ + public var loopCount(default, null):Int; + + /** + * The number of times this sound should loop, where `-1` loops forever, and `1` is + * repeated once. This field is ignored if `looped` is `false`. + * Default is '-1'. + * @since 6.2.0 */ - public var looped:Bool; - + public var loopUntil(default, set):Int; + /** - * In case of looping, the point (in milliseconds) from where to restart the sound when it loops back + * The time (in milliseconds) from where to restart the sound when it loops back * @since 4.1.0 */ - public var loopTime:Float = 0; - + public var loopTime(get, 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:Null; - + public var endTime(get, set):Null; + /** - * The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`) - * @since 4.1.0 + * The position in runtime of the sound playback in milliseconds. + * If set while paused, changes only come into effect after a `resume()` call. */ - public var fadeTween:FlxTween; - + public var time(get, set):Float; + /** - * Internal tracker for a Flash sound object. + * The offset for this sound. + * Useful for just generally offsetting this sound without affecting time. + * @since FunkinCrew's Flixel */ - @:allow(flixel.system.frontEnds.SoundFrontEnd.load) - var _sound:Sound; - + public var offset:Float; + /** - * Internal tracker for a Flash sound channel object. + * The current latency of this sound in milliseconds. + * @since FunkinCrew's Flixel */ - var _channel:SoundChannel; - + public var latency(get, never):Float; + /** - * Internal tracker for a Flash sound transform object. + * The peak of this current sound playback. + * NOTE: It is in linear signal, not in volume. */ - var _transform:SoundTransform; - + public var amplitude(get, never):Float; + /** - * Internal tracker for whether the sound is paused or not (not the same as stopped). + * The peak of this current sound playback, seperated to channels. + * NOTE: It is in linear signal, not in volume. + * @since FunkinCrew's Flixel */ - var _paused:Bool; - + public var amplitudes(get, never):Array; + /** - * Internal tracker for volume. + * Just the amplitude of the left stereo channel. + * NOTE: It is in linear signal, not in volume. */ - var _volume:Float; - + public var amplitudeLeft(get, never):Float; + /** - * Internal tracker for sound channel position. + * Just the amplitude of the right stereo channel. + * NOTE: It is in linear signal, not in volume. */ - var _time:Float = 0; - + public var amplitudeRight(get, never):Float; + /** - * Internal tracker for sound length, so that length can still be obtained while a sound is paused, because _sound becomes null. + * Whether or not this sound is loaded yet. + * @since FunkinCrew's Flixel */ - var _length:Float = 0; - - #if FLX_PITCH + public var loaded(default, null):Bool; + /** - * Internal tracker for pitch. + * The current `FlxSoundData` to load in this sound. + * @since FunkinCrew's Flixel */ - var _pitch:Float = 1.0; - #end - + public var data(default, null):FlxSoundData; + /** - * Internal tracker for total volume adjustment. + * The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`) + * @since 4.1.0 */ - var _volumeAdjust:Float = 1.0; - + public var fadeTween:FlxTween; + /** - * Internal tracker for the sound's "target" (for proximity and panning). + * The internal lime 'AudioSource' to playback sounds. + * @since FunkinCrew's Flixel */ - var _target:FlxObject; - + public final source:AudioSource; + /** - * Internal tracker for the maximum effective radius of this sound (for proximity and panning). + * Signal that is dispatched on sound complete. + * @since FunkinCrew's Flixel */ - var _radius:Float; - + public final onFinish:FlxSignal = new FlxSignal(); + /** - * Internal tracker for whether to pan the sound left and right. Default is false. + * Signal that is dispatched on sound destroy then clears all of the dispatchers. + * @since FunkinCrew's Flixel */ - var _proximityPan:Bool; - + public final onDestroy:FlxSignal = new FlxSignal(); + /** - * Helper var to prevent the sound from playing after focus was regained when it was already paused. + * Tracker for sound complete callback. If assigned, will be called + * each time whenever sound reaches its end. */ - var _resumeOnFocus:Bool = false; - + //@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.") + public var onComplete:Void->Void; + + var _paused:Bool; + var _completed:Bool; + var _volume:Float; + var _volumeAdjust:Float; + var _muted:Bool; + #if FLX_PITCH + var _timeScaledPitch:Bool; + var _pitch:Float; + //var _pitchAdjust:Float; + #end + var _pan:Float; + var _panAdjust:Float; + var _looped:Bool; + var _loopTime:Float; + var _endTime:Null; + var _lastTime:Float; + var _timeTicks:Null; + var _timeInterpolation:Float; + var _pausedByHandler:Bool; + var _pausedPlay:Bool; + var _amplitude:Float; + var _amplitudes:Array; + var _amplitudeUpdated:Bool; + var _point:FlxPoint; + var _point2:FlxPoint; + /** * The FlxSound constructor gets all the variables initialized, but NOT ready to play a sound yet. */ public function new() { super(); - reset(); + + source = new AudioSource(); + source.onComplete.add(stopped); + + _amplitudes = [0, 0]; + + reset(true); } - + /** - * An internal function for clearing all the variables used by sounds. + * A function for clearing all the variables used by sounds. + * @param force Should it reset everything instead of just only source properties. */ - function reset():Void + public function reset(force = false):Void { - destroy(); - - x = 0; - y = 0; - - _time = 0; - _paused = false; - _volume = 1.0; - _volumeAdjust = 1.0; - looped = false; - loopTime = 0.0; - endTime = 0.0; - _target = null; - _radius = 0; - _proximityPan = false; - visible = false; - amplitude = 0; - amplitudeLeft = 0; - amplitudeRight = 0; + stop(); + autoDestroy = false; - - if (_transform == null) - _transform = new SoundTransform(); - _transform.pan = 0; - } - - override public function destroy():Void - { - // Prevents double destroy - if (group != null) - group.remove(this); - - _transform = null; - exists = false; - active = false; - _target = null; - name = null; - artist = null; - - if (_channel != null) - { - _channel.removeEventListener(Event.SOUND_COMPLETE, stopped); - _channel.stop(); - _channel = null; - } - - if (_sound != null) + x = y = 0; + + if (force) { - _sound.removeEventListener(Event.ID3, gotID3); - _sound = null; + clearEffects(); + + persist = false; + loopUntil = -1; + proximityEnabled = false; + proximityPan = true; + scrollFactor?.set(0, 0); + target = null; + radius = 1024; + + looped = false; + loopTime = 0; + endTime = null; + + #if FLX_PITCH + _timeScaledPitch = defaultTimeScaledPitch; + #end } - - onComplete = null; - - super.destroy(); + + offset = 0; + _paused = true; + _lastTime = 0; + _timeTicks = null; + _volume = 1; + _volumeAdjust = 1; + _muted = false; + #if FLX_PITCH + _pitch = 1; + //_pitchAdjust = 1; + #end + _pan = 0; + _panAdjust = 0; + + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + _updateLoop(); + } + + /** + * Destroy this FlxSound from memory. + */ + override function destroy():Void + { + kill(); + source.dispose(); + + _point = FlxDestroyUtil.put(_point); + _point2 = FlxDestroyUtil.put(_point2); } - + /** * Handles fade out, fade in, panning, proximity, and amplitude operations each frame. */ - override public function update(elapsed:Float):Void - { - if (!playing) - return; - - _time = _channel.position; - - var radialMultiplier:Float = 1.0; - - // Distance-based volume control - if (_target != null) + override function update(elapsed:Float):Void + { + if (!playing) return; + + _amplitudeUpdated = false; + + if (proximityEnabled) { - var targetPosition = _target.getPosition(); - radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / _radius; - targetPosition.put(); - radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1); - - if (_proximityPan) + _point = getPosition(_point); + _point2 = target != null ? target.getPosition(_point2) : FlxPoint.get(); + + final camera = camera; + if (camera != null) { - var d:Float = (x - _target.x) / _radius; - _transform.pan = FlxMath.bound(d, -1, 1); + _point2.subtract(camera.scroll.x * target.scrollFactor.x, camera.scroll.y * target.scrollFactor.y); + if (scrollFactor != null) _point.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y); } + + if (_volumeAdjust != (_volumeAdjust = 1 - FlxMath.bound(_point2.distanceTo(_point) / radius, 0, 1))) _updateVolume(); + if (proximityPan && _panAdjust != (_panAdjust = (_point.x - _point2.x) / radius)) _updatePan(); } - - _volumeAdjust = radialMultiplier; - updateTransform(); - - if (_transform.volume > 0) - { - amplitudeLeft = _channel.leftPeak / _transform.volume; - amplitudeRight = _channel.rightPeak / _transform.volume; - amplitude = (amplitudeLeft + amplitudeRight) * 0.5; - } - else - { - amplitudeLeft = 0; - amplitudeRight = 0; - amplitude = 0; - } - - if (endTime != null && _time >= endTime) - stopped(); } - - override public function kill():Void + + /** + * Resets this sound instance when reviving. + */ + override function revive():Void { - super.kill(); - cleanup(false); + reset(true); } - + /** - * One of the main setup functions for sounds, this function loads a sound from an embedded MP3. - * - * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension - * - * @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). + * Kill this sound instance, Internally autoDestroy uses kill() instead of destroy(). + * An alternative method to destroy, but only freeing the unused resources to be reused in later FlxG.sound pool. + * + * @since FunkinCrew's Flixel */ - public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound + override function kill():Void { - if (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)) + onDestroy.dispatch(); + + unload(); + + onDestroy.removeAll(); + onFinish.removeAll(); + onComplete = null; + + if (fadeTween != null) { - if (FlxG.assets.exists(EmbeddedSound, SOUND)) - _sound = FlxG.assets.getSoundUnsafe(EmbeddedSound); - else - FlxG.log.error('Could not find a Sound asset with an ID of \'$EmbeddedSound\'.'); + fadeTween.cancel(); + fadeTween = null; } - - // NOTE: can't pull ID3 info from embedded sound currently - return init(Looped, AutoDestroy, OnComplete); + + super.destroy(); } - + /** - * 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). + * Unloads an asset from the sound playback, good for deattaching data. + * + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). */ - public function loadStream(SoundURL:String, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void, ?OnLoad:Void->Void):FlxSound + public function unload():FlxSound { - cleanup(true); - - _sound = new Sound(); - _sound.addEventListener(Event.ID3, gotID3); - var loadCallback:Event->Void = null; - loadCallback = function(e:Event) + loaded = false; + + source.unload(); + source.buffer = null; + if (data != null) { - (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(); - } + data.decrementUseCount(); + data = null; + } + + alive = false; + exists = false; + + loopTime = 0; + endTime = null; + + return this; + } + + /** + * Loads a sound from the provided sound asset. + * The asset can be an OpenFL Sound instance, Lime AudioBuffer instance, embedded sound, file path or byte array. + * + * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension + * + * @param asset The sound asset to load. + * @param looped Optional. Whether or not this sound should loop endlessly. + * @param loopTime Optional. At which point to start from when audio is looped. + * @param endTime Optional. When does it ends to loop the audio. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when + * the sound finishes playing. + * @param onComplete Called when the sound finishes playing. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + * + * @since 6.2.0 + */ + //public function load + // funkin.audio.FunkinSound already defined load as a static function. + public function loadEmbedded(asset:FlxSoundAsset, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy = false, ?onComplete:Void->Void):FlxSound + { + return init(asset == null ? null : FlxSoundData.fromAsset(asset), looped, loopTime, endTime, autoDestroy, onComplete); + } + + #if FLX_STREAM_SOUND + /** + * Streams a sound from the given file path. Unlike the `load` method, this will load and + * unload chunks of data as the sound plays, keeping memory usage low. This is recommended for + * longer sounds, like music tracks. For shorter sounds like sound effects, it is better to + * use the `load` method, which loads the entire sound into memory before playing it. + * + * Due to a backend limitation, audio streaming is currently only available on native targets + * and OGG/Vorbis audio files. + * ...Not anymore :) with FunkinCrew's Lime. + * + * This does not load sounds from web locations. Use `loadFromURL()` for that, instead. + * + * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension + * + * @param path The ID or asset path to the sound asset. + * @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. + * @param onComplete Called when the sound finishes playing. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + * + * @since 6.2.0 + */ + public function loadStreamed(path:String, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy = false, ?onComplete:Void->Void):FlxSound + { + return init(FlxSoundData.fromAssetKey(path, true), looped, loopTime, endTime, autoDestroy, onComplete); + } + #end + + /** + * Loads a sound from the provided URL. + * + * @param url A string representing the URL of the sound you want to play. + * @param looped Optional. Whether or not this sound should loop endlessly. + * @param loopTime Optional. At which point to start from when audio is looped. + * @param endTime Optional. When does it ends to loop the audio. + * @param autoDestroy Whether or not this FlxSound instance should be destroyed when + * the sound finishes playing. + * @param onComplete Called when the sound finishes playing. + * @param onLoad Called when the sound finishes loading. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + * + * @since 6.2.0 + */ + public function loadFromURL(url:String, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy:Bool = false, + ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound + { + var sound = new Sound(); + #if flash + var gotID3:Event->Void = null; + gotID3 = function(e:Event) { + name = sound.id3.songName; + artist = sound.id3.artist; + sound.removeEventListener(Event.ID3, gotID3); + } + sound.addEventListener(Event.ID3, gotID3); + #end + + 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) + { + init(FlxSoundData.fromSound(sound, url), null, null, null, this.autoDestroy, null); + 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); + sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true); + sound.load(new URLRequest(url)); + + return init(null, looped, loopTime, endTime, autoDestroy, onComplete); + } + + /** + * One of the main setup functions for sounds, this function loads a sound from an embedded MP3. + * + * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension + * + * @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.loadFromURL()` 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). + */ + //@:deprecated("loadEmbedded() is deprecated, use load() instead.") + //public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound + //{ + // return load(EmbeddedSound, 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.loadFromURL()` 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). + */ + @:deprecated("loadStream() is deprecated, use loadFromURL() instead.") + public function loadStream(SoundURL:String, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void, ?OnLoad:Void->Void):FlxSound + { + return loadFromURL(SoundURL, Looped, AutoDestroy, OnComplete, OnLoad); } - + /** * One of the main setup functions for sounds, this function loads a sound from a ByteArray. - * - * @param Bytes A ByteArray object. + * + * @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. + * Default value is false, but `FlxG.sound.play()` and `FlxG.sound.loadFromURL()` will set it to true by default. * @return This FlxSound instance (nice for chaining stuff together, if you're into that). */ + @:deprecated("loadByteArray() is deprecated, use load() instead.") public function loadByteArray(Bytes:ByteArray, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound { - cleanup(true); - - _sound = new Sound(); - _sound.addEventListener(Event.ID3, gotID3); - _sound.loadCompressedDataFromByteArray(Bytes, Bytes.length); - - return init(Looped, AutoDestroy, OnComplete); + return loadEmbedded(Bytes, Looped, AutoDestroy, OnComplete); } - - function init(Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound + + function init(data:FlxSoundData, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy = false, ?onComplete:Void->Void):FlxSound { - looped = Looped; - autoDestroy = AutoDestroy; - updateTransform(); exists = true; - onComplete = OnComplete; - #if FLX_PITCH - pitch = 1; - #end - _length = (_sound == null) ? 0 : _sound.length; - endTime = _length; + alive = true; + + stop(); + + this.autoDestroy = autoDestroy; + this.onComplete = onComplete; + + if (this.data != data) + { + if (data != null && !data.isDestroyed) + { + this.data = data; + data.incrementUseCount(); + + source.buffer = data.buffer; + source.load(); + + loaded = true; + } + else + { + unload(); + } + } + + if (looped != null) this.looped = looped; + if (loopTime != null) this.loopTime = loopTime; + if (endTime != null) this.endTime = endTime; + return this; } - + + /** + * Helper function to set the coordinates of this object. + * Audio positioning is used in conjunction with proximity/panning. + * + * @param x The new X position + * @param y The new Y position + * + * @since FunkinCrew's Flixel + */ + public function setPosition(x = 0.0, y = 0.0) + { + 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 FunkinCrew's Flixel + */ + public function getPosition(?result:FlxPoint):FlxPoint { + if (result == null) + result = FlxPoint.get(); + + return result.set(x, y); + } + /** * 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. + * @param x The X position of the sound. + * @param y The Y position of the sound. + * @param target 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. + * @param scrollFactor Whether scrollfactor 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:Float, Y:Float, TargetObject:FlxObject, Radius:Float, Pan:Bool = true):FlxSound + public function proximity(x = 0.0, y = 0.0, ?target:FlxObject, ?radius:Float, pan = true, ?scrollFactor:FlxPoint):FlxSound { - x = X; - y = Y; - _target = TargetObject; - _radius = Radius; - _proximityPan = Pan; + proximityEnabled = true; + proximityPan = pan; + + setPosition(x, y); + if (target != null) this.target = target; + if (radius != null) this.radius = radius; + if (scrollFactor != null) this.scrollFactor.copyFrom(scrollFactor); + else if (this.scrollFactor == null) this.scrollFactor = FlxPoint.get(0, 0); + return this; } - + /** * 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:Bool = false, StartTime:Float = 0.0, ?EndTime:Float):FlxSound - { - if (!exists) - return this; - - if (ForceRestart) - cleanup(false, true); - else if (playing) // Already playing sound - return this; - - if (_paused) - resume(); - else - startSound(StartTime); - - endTime = EndTime; + * @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`, it'll be the same as previous set of endTime. + * @param volume What volume should the audio be played with. + * @param pitch What pitch should the audio be played with (NOTE: Flash does not support this). + * @param pan What pan should the audio be played with. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function play(forceRestart = false, startTime = 0.0, ?endTime:Float, ?volume:Float, ?pitch:Float, ?pan:Float):FlxSound + { + if (volume != null) _volume = volume; + #if FLX_PITCH + if (pitch != null) _pitch = pitch; + #end + if (pan != null) _pan = pan; + if (endTime != null) this.endTime = endTime; + + if (!loaded || (playing && !forceRestart)) return this; + + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + _updateLoop(); + + if (!_paused) loopCount = 0; + if (!_paused || forceRestart) source.currentTime = startTime + offset; + + if (_pausedByHandler) _pausedPlay = true; + else if (!source.playing) source.play(); + + _paused = false; + _completed = false; + active = true; + return this; } - + + /** + * Call this function to prepare the sound in specific time, then call `resume()` or `FlxSound.playSounds()`. + * Good for playing multiple sounds at the same time, stops completely and override pause status. + * + * @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`, it'll be the same as previous set of endTime. + * @param volume What volume should the audio be played with. + * @param pitch What pitch should the audio be played with (NOTE: Flash does not support this). + * @param pan What pan should the audio be played with. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). + */ + public function prepare(startTime = 0.0, ?endTime:Float, ?volume:Float, ?pitch:Float, ?pan:Float):FlxSound + { + if (volume != null) _volume = volume; + #if FLX_PITCH + if (pitch != null) _pitch = pitch; + #end + if (pan != null) _pan = pan; + if (endTime != null) this.endTime = endTime; + + if (!loaded) return this; + + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + _updateLoop(); + + source.prepare(startTime + offset); + + _paused = true; + _completed = false; + active = false; + + 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); + if (_paused) play(false); 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; - - _time = _channel.position; + source.pause(); + + get_time(); + _timeTicks = null; + _pausedByHandler = false; + _pausedPlay = false; _paused = true; - cleanup(false, false); + active = false; + return this; } - + /** * Call this function to stop this sound. + * + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). */ - public inline function stop():FlxSound + public function stop():FlxSound { - cleanup(autoDestroy, true); + _timeTicks = null; + _pausedByHandler = false; + _pausedPlay = false; + _paused = true; + active = false; + + source.stop(); + if (autoDestroy) kill(); + 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. + * @param duration The amount of time the fade-out operation should take. + * @param to The volume to tween to, 0 by default. + * @param onComplete The callback for when it's done fading. + * @param ease EaseFunction to use for the tween. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). */ - public inline function fadeOut(Duration:Float = 1, ?To:Float = 0, ?onComplete:FlxTween->Void):FlxSound + public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void, ?ease:EaseFunction):FlxSound { - if (fadeTween != null) - fadeTween.cancel(); - fadeTween = FlxTween.num(volume, To, Duration, {onComplete: onComplete}, volumeTween); - + fadeTween?.cancel(); + fadeTween = FlxTween.num(_volume, to, duration, {ease: ease ?? FlxEase.quadIn, 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. + * @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. + * @param onComplete The callback for when it's done fading. + * @param ease EaseFunction to use for the tween. + * @return This FlxSound instance (nice for chaining stuff together, if you're into that). */ - public inline function fadeIn(Duration:Float = 1, From:Float = 0, To:Float = 1, ?onComplete:FlxTween->Void):FlxSound + public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void, ?ease:EaseFunction):FlxSound { - if (!playing) - play(); - - if (fadeTween != null) - fadeTween.cancel(); - - fadeTween = FlxTween.num(From, To, Duration, {onComplete: onComplete}, volumeTween); + if (!playing) play(); + + fadeTween?.cancel(); + fadeTween = FlxTween.num(from, to, duration, {ease: ease ?? FlxEase.quadOut, onComplete: onComplete}, volumeTween); + return this; } - - function volumeTween(f:Float):Void + + function volumeTween(f:Float) volume = f; + + /** + * Adds an audio playback effect for this sound. + * + * @param effect A `lime.media.AudioEffect` instance. + * @since FunkinCrew's Flixel & Lime + */ + public function addEffect(effect:AudioEffect):Void { - volume = f; + source.addEffect(effect); } - + /** - * Returns the currently selected "real" volume of the sound (takes fades and proximity into account). - * - * @return The adjusted volume of the sound. + * Removes an audio playback effect from this sound. + * + * @param effect A `lime.media.AudioEffect` instance. + * @since FunkinCrew's Flixel & Lime */ - public inline function getActualVolume():Float + public function removeEffect(effect:AudioEffect):Void { - return _volume * _volumeAdjust; + source.removeEffect(effect); } - + /** - * 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 + * Clears any existing audio playback effects added in this sound. + * @since FunkinCrew's Flixel & Lime */ - public inline function setPosition(X:Float = 0, Y:Float = 0):Void + public function clearEffects():Void { - x = X; - y = Y; + source.clearEffects(); } - + /** - * Call after adjusting the volume to update the sound channel's settings. + * Returns the audio playback effect stred at the specified index. + * + * @param index An index to the `lime.media.AudioEffect`. + * @return The specified `lime.media.AudioEffect` from the index. + * @since FunkinCrew's Flixel & Lime */ - @:allow(flixel.sound.FlxSoundGroup) - function updateTransform():Void + public function getEffectAt(index:Int):AudioEffect { - _transform.volume = calcTransformVolume(); - - if (_channel != null) - _channel.soundTransform = _transform; + return source.getEffectAt(index); } - - function calcTransformVolume():Float + + /** + * Returns the index of a desired audio playback effect stored. + * + * @param effect A `lime.media.AudioEffect` instance. + * @return The index stored for the desired audio playback effect in this sound. + * @since FunkinCrew's Flixel & Lime + */ + public function getEffectIndex(effect:AudioEffect):Int { - final volume = (group != null ? group.getVolume() : 1.0) * _volume * _volumeAdjust; - - #if FLX_SOUND_SYSTEM - if (FlxG.sound.muted) - return 0.0; - - return FlxG.sound.applySoundCurve(FlxG.sound.volume * volume); - #else - return volume; - #end + return source.getEffectIndex(effect); } - + /** - * An internal helper function used to attempt to start playing - * the sound and populate the _channel variable. + * Returns the currently selected "real" volume of the sound (takes fades and proximity). + * + * @return The adjusted volume of the sound. */ - function startSound(StartTime:Float):Void + public function getActualVolume():Float { - if (_sound == null) - return; - - _time = StartTime; - _paused = false; - _channel = _sound.play(_time, 0, _transform); - if (_channel != null) + return if (_muted) 0; else (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 function getActualPitch():Float + { + return if (_timeScaledPitch) _pitch * FlxG.timeScale; else _pitch; + } + #end + + /** + * Returns the currently selected "real" pan of the sound (takes fades and proximity). + * + * @return The adjusted pan of the sound. + */ + public function getActualPan():Float + { + return _pan + _panAdjust; + } + + /** + * Returns the actual time coming from the internal, can be used for detecting sync error. + * + * @return The actual time of the sound. + */ + public function getActualTime():Float + { + get_time(); + return _lastTime; + } + + function stopped():Void + { + if (onComplete != null) onComplete(); + onFinish.dispatch(); + + if (_looped) { - #if FLX_PITCH - pitch = _pitch; - #end - _channel.addEventListener(Event.SOUND_COMPLETE, stopped); - active = true; + _timeInterpolation = 1; + _timeTicks = FlxG.game.getTicks(); + _lastTime = loopTime; + loopCount++; + _updateLoop(); } else { - exists = false; - active = false; + _completed = true; + if (autoDestroy) kill(); } } - - /** - * An internal helper function used to help Flash - * clean up finished sounds or restart looped sounds. - */ - function stopped(?_):Void + + function set_group(value:FlxSoundGroup):FlxSoundGroup { - if (onComplete != null) - onComplete(); - - if (looped) + if (value != null) value.add(this); + else group.remove(this); + return group; + } + + function set_proximityEnabled(value:Bool):Bool + { + if (proximityEnabled != value && !value) { - cleanup(false); - play(false, loopTime, endTime); + proximityEnabled = value; + + _volumeAdjust = 1; + //_pitchAdjust = 1; + _panAdjust = 0; + _updateVolume(); + //_updatePitch(); + _updatePan(); } - else - cleanup(autoDestroy); + + return value; } - - /** - * An internal helper function used to help Flash clean up (and potentially re-use) finished sounds. - * Will stop the current sound and destroy the associated SoundChannel, plus, - * any other commands ordered by the passed in parameters. - * - * @param destroySound Whether or not to destroy the sound. If this is true, - * the position and fading will be reset as well. - * @param resetPosition Whether or not to reset the position of the sound. - */ - function cleanup(destroySound:Bool, resetPosition:Bool = true):Void + + function set_proximityPan(value:Bool):Bool { - if (destroySound) + if (proximityPan != value && proximityEnabled && !value) { - reset(); - return; + proximityPan = value; + + _panAdjust = 0; + _updatePan(); } - - if (_channel != null) + + return value; + } + + function get_playing():Bool + { + return loaded && source.playing || _pausedPlay; + } + + function get_paused():Bool + { + return _paused; + } + + function get_completed():Bool + { + return _completed; + } + + function get_volume():Float + { + return _volume; + } + + function set_volume(value:Float):Float + { + value = Math.max(value, 0); + if (_volume != value) { - _channel.removeEventListener(Event.SOUND_COMPLETE, stopped); - _channel.stop(); - _channel = null; + _volume = value; + _updateVolume(); } - - active = false; - - if (resetPosition) + + return value; + } + + function get_muted():Bool + { + return _muted; + } + + function set_muted(value:Bool):Bool + { + if (_muted != value) { - _time = 0; - _paused = false; + _muted = value; + _updateVolume(); } + + return value; } - - /** - * Internal event handler for ID3 info (i.e. fetching the song name). - */ - function gotID3(_):Void + + #if FLX_PITCH + function get_pitch():Float { - name = _sound.id3.songName; - artist = _sound.id3.artist; - _sound.removeEventListener(Event.ID3, gotID3); + return _pitch; } - - #if FLX_SOUND_SYSTEM - @:allow(flixel.system.frontEnds.SoundFrontEnd) - function onFocus():Void + + function set_pitch(value:Float):Float { - if (_resumeOnFocus) + value = Math.max(value, 0); + if (_pitch != value) { - _resumeOnFocus = false; - resume(); + _pitch = value; + _updatePitch(); } + + return value; } - - @:allow(flixel.system.frontEnds.SoundFrontEnd) - function onFocusLost():Void + + function get_timeScaledPitch():Bool + { + return _timeScaledPitch; + } + + function set_timeScaledPitch(value:Bool):Bool { - _resumeOnFocus = !_paused; - pause(); + if (_timeScaledPitch != value) + { + _timeScaledPitch = value; + _updatePitch(); + } + + return value; } #end - - @:deprecated("sound.group = myGroup is deprecated, use myGroup.add(sound)") // 5.7.0 - function set_group(value:FlxSoundGroup):FlxSoundGroup + + function get_pan():Float + { + return _pan; + } + + function set_pan(value:Float):Float { - if (value != null) + value = FlxMath.bound(value, -1, 1); + if (_pan != value) { - // add to new group, also removes from prev and calls updateTransform - value.add(this); + _pan = value; + _updatePan(); } - else + + return value; + } + + function get_length():Float + { + return loaded ? data.length - offset : 0; + } + + function get_looped():Bool + { + return _looped; + } + + function set_looped(value:Bool):Bool + { + if (_looped != value) { - // remove from prev group, also calls updateTransform - group.remove(this); + _looped = value; + _updateLoop(); } + return value; } - - inline function get_playing():Bool + + function set_loopUntil(value:Int):Int { - return _channel != null; + value = value < 0 ? -1 : value; + if (loopUntil != value) + { + loopUntil = value; + _updateLoop(); + } + + return value; } - - inline function get_volume():Float + + function get_loopTime():Float { - return _volume; + return _loopTime - offset; } - - function set_volume(Volume:Float):Float + + function set_loopTime(value:Float):Float { - _volume = FlxMath.bound(Volume, 0, 1); - updateTransform(); - return Volume; + value = loaded ? FlxMath.bound(value, -offset, data.length - offset) : -offset; + + var internal = value + offset; + if (_loopTime != internal) + { + _loopTime = internal; + source.loopTime = internal; + } + + return value; } - - #if FLX_PITCH - inline function get_pitch():Float + + function get_endTime():Null { - return _pitch; + return _endTime == null ? null : _endTime - offset; } - - function set_pitch(v:Float):Float + + function set_endTime(value:Null):Null { - if (_channel != null) + value = (loaded && value != null && value > -offset) ? Math.min(value, data.length - offset) : null; + + var internal = value == null ? null : value + offset; + if (_endTime != internal) { - #if (openfl < "9.3.2") - @:privateAccess - if (_channel.__source != null) - _channel.__source.pitch = v; - #else - @:privateAccess - if (_channel.__audioSource != null) - _channel.__audioSource.pitch = v; - #end + _endTime = internal; + if (internal == null) source.length = length; + else source.length = internal; } - - return _pitch = v; + + return value; } - #end - - inline function get_pan():Float + + function get_time():Float { - return _transform.pan; + if (!loaded) return 0.0; + else if (_completed) return source.length - offset; + + final currentTime = source.currentTime - offset; + if (!source.playing) + { + _timeTicks = null; + return _lastTime = currentTime; + } + else if (_timeTicks == null) + { + _timeTicks = FlxG.game.getTicks(); + _timeInterpolation = 1.0; + return _lastTime = currentTime; + } + + final interpolatedTime = _lastTime + (FlxG.game.getTicks() - _timeTicks) * source.pitch * _timeInterpolation; + if (_lastTime != currentTime) + { + _timeTicks = FlxG.game.getTicks(); + if ((_timeInterpolation = 1.0 - (interpolatedTime - currentTime) * 0.001) < 1.0 && _timeInterpolation > 0.9) + { + return _lastTime = interpolatedTime; + } + else + { + _timeInterpolation = 1.0; + return _lastTime = currentTime; + } + } + + return interpolatedTime; } - - inline function set_pan(pan:Float):Float + + function set_time(value:Float):Float { - _transform.pan = pan; - updateTransform(); - return pan; + _timeTicks = null; + value = FlxMath.bound(value, -offset, source.length - offset); + + if (loaded) source.currentTime = value + offset; + else _lastTime = 0; + + return value; } - - inline function get_time():Float + + function get_latency():Float { - return _time; + return source.latency; } - - function set_time(time:Float):Float + + function get_amplitude():Float + { + if (!_amplitudeUpdated) _updateAmplitudes(); + return _amplitude; + } + + function get_amplitudes():Array + { + if (!_amplitudeUpdated) _updateAmplitudes(); + return _amplitudes; + } + + function get_amplitudeLeft():Float { - if (playing) + if (!_amplitudeUpdated) _updateAmplitudes(); + return _amplitudes[0] ?? 0; + } + + function get_amplitudeRight():Float + { + if (!_amplitudeUpdated) _updateAmplitudes(); + return _amplitudes[1] ?? 0; + } + + function updateTransform():Void + { + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + } + + @:allow(flixel.sound.FlxSoundGroup) + function _updateVolume():Void + { + if (_muted || FlxG.sound._muted) source.gain = 0; + else source.gain = calcTransformVolume(); + } + + function calcTransformVolume():Float + { + #if FLX_SOUND_SYSTEM + if (FlxG.sound._muted) return 0; + return FlxG.sound.applySoundCurve(getActualVolume() * FlxG.sound._volume); + #else + return getActualVolume(); + #end + } + + #if FLX_PITCH + function _updatePitch():Void + { + source.pitch = getActualPitch(); + } + #end + + function _updatePan():Void + { + source.pan = getActualPan(); + } + + function _updateLoop():Void + { + if (_looped) { - cleanup(false, true); - startSound(time); + if (loopUntil == -1) source.loops = 999; + else + { + var internal = loopUntil - loopCount; + if (internal != source.loops) source.loops = internal; + } } - return _time = time; + else source.loops = 0; } - - inline function get_length():Float + + function _updateAmplitudes():Void { - return _length; + if (!loaded) return; + + _amplitudeUpdated = true; + _amplitude = 0; + + final peaks = source.peaks; + for (i in 0...peaks.length) + { + _amplitudes[i] = peaks[i]; + if (peaks[i] > _amplitude) _amplitude = peaks[i]; + } } - + override public function toString():String { return FlxStringUtil.getDebugString([ LabelValuePair.weak("playing", playing), LabelValuePair.weak("time", time), + LabelValuePair.weak("offset", offset), LabelValuePair.weak("length", length), - LabelValuePair.weak("volume", volume) + LabelValuePair.weak("volume", volume), + #if FLX_PITCH + LabelValuePair.weak("pitch", pitch) + #end ]); } -} +} \ No newline at end of file diff --git a/flixel/sound/FlxSoundData.hx b/flixel/sound/FlxSoundData.hx new file mode 100644 index 0000000000..05d1ef216c --- /dev/null +++ b/flixel/sound/FlxSoundData.hx @@ -0,0 +1,477 @@ +package flixel.sound; + +import haxe.io.Bytes; +import haxe.Int64; + +import lime.media.AudioBuffer; +import lime.media.AudioContextType; +import lime.media.AudioDecoder; +import lime.media.AudioManager; + +import openfl.media.Sound; +import openfl.utils.Assets; +import openfl.utils.ByteArray; + +import flixel.system.FlxAssets; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxStringUtil; + +/** + * Lime `AudioBuffer` wrapper which is used for audio. + * @since FunkinCrew's Flixel + */ +@:access(lime.media.AudioBuffer) +@:access(openfl.media.Sound) +@:access(flixel.system.frontEnds.AssetFrontEnd) +class FlxSoundData implements IFlxDestroyable +{ + /** + * What minimum of duration or length in milliseconds can it be automatically be persists. + */ + public static var persistMaxDuration:Float = 3000; + + /** + * How much duration or length in milliseconds can it be automatically be assigned + * to be a streamed sound data. + */ + public static var streamMinimumLength:Float = 8000; + + /** + * Should it allow streaming when stream argument to load `FlxSoundData` is not set. + */ + public static var allowStreaming:Bool = true; + + /** + * Creates and caches FlxSoundData object from source asset. + * + * @param asset `FlxSoundAsset` to load. + * @param stream Load this sound data to be streamable instead. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromAsset(asset:FlxSoundAsset, ?stream:Bool, ?key:String, cache = true):FlxSoundData + { + if ((asset is Class)) return fromClass(cast asset, key, cache); + else if ((asset is String)) return fromAssetKey(cast asset, stream, key, cache); + else if ((asset is Bytes)) return fromByteArray(cast asset, stream, key, cache); + else if ((asset is Sound)) return fromSound(cast asset, key, cache); + else if ((asset is AudioBuffer)) return fromAudioBuffer(cast asset, key, cache); + else if ((asset is FlxSoundData)) return cast asset; + + return null; + } + + /** + * Creates and caches FlxSoundData object from openfl.Assets key string. + * + * @param source `openfl.Assets` key string. For example: `"assets/sound.mp3"`. + * @param stream Load this sound data to be streamable instead. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromAssetKey(source:String, ?stream:Bool, ?key:String, cache = true):FlxSoundData + { + if (key == null) key = source; + + #if FLX_SOUND_SYSTEM + if (cache) + { + var soundData = FlxG.sound.getCache(key); + if (soundData != null) return soundData; + } + #end + + final openflAssetExists = Assets.exists(source); + + #if native + var decoder = AudioDecoder.fromFile(openflAssetExists ? Assets.getPath(source) : source); + if (decoder == null) + { + if (!openflAssetExists) return null; + + decoder = AudioDecoder.fromBytes(Assets.getBytes(source)); + if (decoder == null) return null; + } + + if (stream == null) stream = allowStreaming && decoder.total() >= Std.int(streamMinimumLength / 1000.0 * decoder.sampleRate); + return fromAudioBuffer(AudioBuffer.fromDecoder(decoder, stream, true), key, cache); + #else + var buffer = AudioBuffer.fromFile(openflAssetExists ? Assets.getPath(source) : source); + if (buffer == null) + { + if (!openflAssetExists) return null; + + buffer = AudioBuffer.fromBytes(Assets.getBytes(source)); + if (buffer == null) return null; + } + + return fromAudioBuffer(buffer, key, cache); + #end + } + + /** + * Creates and caches `FlxSoundData` object from a compressed byte array object. + * + * @param source `ByteArray` for `FlxSoundData` to use. + * @param stream Load this sound data to be streamable instead. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromByteArray(source:Bytes, ?stream:Bool, ?key:String, cache = true):FlxSoundData + { + #if FLX_SOUND_SYSTEM + if (cache && key != null) + { + var soundData = FlxG.sound.getCache(key); + if (soundData != null) return soundData; + } + #end + + if (source == null) return null; + + #if native + final decoder = AudioDecoder.fromBytes(cast source); + if (decoder == null) return null; + + if (stream == null) stream = allowStreaming && decoder.total() >= Std.int(streamMinimumLength / 1000.0 * decoder.sampleRate); + return fromAudioBuffer(AudioBuffer.fromDecoder(decoder, stream, true), key, cache); + #else + return fromAudioBuffer(AudioBuffer.fromBytes(cast source, stream), key, cache); + #end + } + + /** + * Creates and caches FlxSoundData object from a specified `Class`. + * + * @param source `Class` to create `Sound` for `FlxSoundData` from. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromClass(source:Class, ?key:String, cache = true):FlxSoundData + { + #if FLX_SOUND_SYSTEM + if (cache && key != null) + { + var soundData = FlxG.sound.getCache(key); + if (soundData != null) return soundData; + } + #end + + if (source == null) return null; + + var instance = Type.createInstance(source, []); + if ((instance is Sound)) return fromSound(cast instance, key, cache); + else if ((instance is AudioBuffer)) return fromAudioBuffer(cast instance, key, cache); + + return null; + } + + /** + * Creates and caches `FlxSoundData` object from specified `Sound` object. + * + * @param source `Sound` for `FlxSoundData` to use. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromSound(source:Sound, ?key:String, cache = true):FlxSoundData + { + #if FLX_SOUND_SYSTEM + if (cache && key != null) + { + var soundData = FlxG.sound.getCache(key); + if (soundData != null) return soundData; + } + #end + + // Maybe make it also load pending buffer too...? + if (source == null || source.__buffer == null) return null; + + return fromAudioBuffer(source.__buffer, key, cache); + } + + /** + * Creates and caches `FlxSoundData` object from specified `AudioBuffer` object. + * + * @param source `AudioBuffer` for `FlxSoundData` to use. + * @param key Force the cache to use a specific key to index the sound data. + * @param cache Whether to use sound data caching or not. Default value is `true`, which means automatic caching. + * @return Cached `FlxSoundData` object we just created. + */ + public static function fromAudioBuffer(source:AudioBuffer, ?key:String, cache = true):FlxSoundData + { + if (source == null) return null; + else if (!cache) return createSoundData(source, key, false); + + #if FLX_SOUND_SYSTEM + var localKey:String = FlxG.sound.findKeyForBuffer(source); + if (localKey != null && key == null) return FlxG.sound.getCache(localKey); + #end + + #if FLX_SOUND_SYSTEM + if (key == null) key = generateKey(); + #end + return createSoundData(source, key, cache); + } + + #if FLX_SOUND_SYSTEM + static var _lastUniqueKeyIndex:Int = 0; + static function generateKey():String + { + var baseKey = "soundData"; + var i:Int = _lastUniqueKeyIndex; + var uniqueKey:String; + do + { + i++; + uniqueKey = baseKey + i; + } + while (FlxG.sound.checkCache(uniqueKey)); + + _lastUniqueKeyIndex = i; + return uniqueKey; + } + #end + + static function createSoundData(buffer:AudioBuffer, ?key:String, cache = true):FlxSoundData + { + var soundData:FlxSoundData = null; + + if (cache && key != null) + { + soundData = new FlxSoundData(key, buffer); + #if FLX_SOUND_SYSTEM + FlxG.sound.addCache(soundData); + #end + } + else + { + soundData = new FlxSoundData(null, buffer); + } + + return soundData; + } + + /** + * Key used in the `SoundFrontEnd` cache. + */ + public var key(default, null):String; + + /** + * Whether this sound data object should stay in the cache after state changes or not. + * `destroyOnNoUse` has no effect when this is set to `true`. + */ + public var persist:Bool = false; + + /** + * Whether this `FlxSoundData` should be immediately destroyed when `useCount` becomes zero (defaults to `false`). + * Ignores `unused`, has no effect when `persist` is `true`. + */ + public var destroyOnNoUse(default, set):Bool = false; + + /** + * Usage counter for this `FlxSoundData` object. + */ + public var useCount(default, null):Int = 0; + + /** + * Whether or not is it about to be destroyed in the next clearing cycle. + */ + #if FLX_SOUND_SYSTEM + @:allow(flixel.system.frontEnds.SoundFrontEnd) + #end + public var unused(default, null):Bool; + + /** + * The number of bits per sample in the sound data. + */ + public var bitsPerSample(get, never):Int; + + /** + * The Lime `AudioBuffer` for FlxSound to play. + */ + public var buffer(get, set):AudioBuffer; + + /** + * The number of sound data channels. + * (1 for mono, 2 for stereo, etc). + */ + public var channels(get, never):Int; + + /** + * The sample rate of the sound data, in Hz. + */ + public var sampleRate(get, never):Int; + + /** + * How much samples are in this sound data. + */ + public var samples(get, never):Int64; + + /** + * The duration or length in millseconds of this sound data. + */ + public var length(get, never):Float; + + /** + * Whether `destroy` was called on this sound data. + */ + public var isDestroyed(get, never):Bool; + + /** + * Is this sound data is streamable. + */ + public var isStreamable(get, never):Bool; + + var _buffer:AudioBuffer; + #if native + var _samples:Int64; + #end + + /** + * `FlxSoundData` contructor + */ + public function new(key:String, buffer:AudioBuffer, ?persist:Bool) + { + this.key = key; + this.buffer = buffer; + + if (persist == null) persist = buffer != null && get_length() < persistMaxDuration; + this.persist = persist; + } + + /** + * Free this `FlxSoundData` object from memory. + */ + public function destroy():Void + { + #if FLX_SOUND_SYSTEM + if (key != null) FlxG.sound.removeCache(key, false); + #end + if (_buffer != null) _buffer.dispose(); + _buffer = null; + } + + public function incrementUseCount() + { + useCount++; + + unused = false; + } + + public function decrementUseCount() + { + useCount--; + + checkUseCount(); + } + + function checkUseCount() + { + #if FLX_SOUND_SYSTEM + if (useCount <= 0 && destroyOnNoUse && !persist) destroy(); + #end + } + + inline function get_bitsPerSample():Int + { + #if (js && html5 && howlerjs) + if (_buffer != null && _buffer.bitsPerSample > 0) return _buffer.bitsPerSample; + return 16; + #else + return _buffer != null ? _buffer.bitsPerSample : 0; + #end + } + + inline function get_channels():Int + { + #if (js && html5 && howlerjs) + if (_buffer != null && _buffer.channels > 0) return _buffer.channels; + return 2; + #else + return _buffer != null ? _buffer.channels : 0; + #end + } + + function get_sampleRate():Int + { + #if (js && html5 && howlerjs) + if (_buffer != null && _buffer.sampleRate > 0) return _buffer.sampleRate; + else if (AudioManager.context != null && AudioManager.context.type == AudioContextType.WEB) return Std.int(AudioManager.context.web.sampleRate); + return 44100; + #else + return _buffer != null ? _buffer.sampleRate : 0; + #end + } + + function get_samples():Int64 + { + #if (js && html5 && howlerjs) + return (_buffer != null && _buffer.__srcHowl != null) ? Int64.fromFloat(_buffer.__srcHowl.duration() * get_sampleRate()) : 0; + #else + return _buffer != null ? _samples : 0; + #end + } + + function get_length():Float + { + #if (js && html5 && howlerjs) + return (_buffer?.__srcHowl?.duration() ?? 0) * 1000.0; + #else + return _buffer != null ? (_samples.high * 4294967296.0 + (_samples.low >>> 0)) / _buffer.sampleRate * 1000.0 : 0; + #end + } + + inline function get_isDestroyed():Bool + { + return _buffer == null; + } + + inline function get_isStreamable():Bool + { + return _buffer != null && _buffer.data == null && _buffer.decoder != null; + } + + inline function set_destroyOnNoUse(value:Bool):Bool + { + this.destroyOnNoUse = value; + + checkUseCount(); + + return value; + } + + inline function get_buffer():AudioBuffer + { + return _buffer; + } + + function set_buffer(value:AudioBuffer):AudioBuffer + { + if (_buffer != value && value != null) + { + #if native + if (value.decoder != null) _samples = value.decoder.total(); + else if (value.data != null) _samples = Int64.make(0, Std.int(value.data.length / (value.bitsPerSample >> 3) / value.channels)); + else _samples = 0; + #end + } + return _buffer = value; + } + + public function toString():String + { + return FlxStringUtil.getDebugString([ + LabelValuePair.weak("key", key), + LabelValuePair.weak("useCount", useCount), + LabelValuePair.weak("bitsPerSample", bitsPerSample), + LabelValuePair.weak("channels", channels), + LabelValuePair.weak("sampleRate", sampleRate), + LabelValuePair.weak("length", length) + ]); + } +} \ No newline at end of file diff --git a/flixel/sound/FlxSoundGroup.hx b/flixel/sound/FlxSoundGroup.hx index 9c2fcc1cbd..0540c0969f 100644 --- a/flixel/sound/FlxSoundGroup.hx +++ b/flixel/sound/FlxSoundGroup.hx @@ -45,7 +45,7 @@ class FlxSoundGroup sounds.push(sound); @:bypassAccessor sound.group = this; - sound.updateTransform(); + sound._updateVolume(); return true; } return false; @@ -63,7 +63,7 @@ class FlxSoundGroup @:bypassAccessor sound.group = null; sounds.remove(sound); - sound.updateTransform(); + sound._updateVolume(); return true; } return false; @@ -85,8 +85,7 @@ class FlxSoundGroup */ public function resume():Void { - for (sound in sounds) - sound.resume(); + FlxSound.playSounds(sounds); } /** @@ -103,7 +102,7 @@ class FlxSoundGroup this.volume = volume; for (sound in sounds) { - sound.updateTransform(); + sound._updateVolume(); } return volume; } @@ -113,7 +112,7 @@ class FlxSoundGroup muted = value; for (sound in sounds) { - sound.updateTransform(); + sound._updateVolume(); } return muted; } diff --git a/flixel/system/FlxAssets.hx b/flixel/system/FlxAssets.hx index 9b5f69953a..9979b8859b 100644 --- a/flixel/system/FlxAssets.hx +++ b/flixel/system/FlxAssets.hx @@ -10,14 +10,18 @@ import flixel.graphics.atlas.TexturePackerAtlas; import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFrame; import flixel.graphics.frames.FlxFramesCollection; -import flixel.system.frontEnds.AssetFrontEnd; import flixel.graphics.frames.bmfont.BMFont; +import flixel.sound.FlxSoundData; +import flixel.system.frontEnds.AssetFrontEnd; +import flixel.util.typeLimit.OneOfSix; +import flixel.util.typeLimit.OneOfFive; import flixel.util.typeLimit.OneOfFour; import flixel.util.typeLimit.OneOfThree; import flixel.util.typeLimit.OneOfTwo; import haxe.Json; import haxe.io.Bytes; import haxe.xml.Access; +import lime.media.AudioBuffer; import openfl.display.BitmapData; import openfl.display.Graphics; import openfl.media.Sound; @@ -36,7 +40,7 @@ class VirtualInputData extends #if nme ByteArray #else ByteArrayData #end {} typedef FlxTexturePackerJsonAsset = FlxJsonAsset; typedef FlxAsepriteJsonAsset = FlxJsonAsset; -typedef FlxSoundAsset = OneOfThree>; +typedef FlxSoundAsset = OneOfSix, AudioBuffer, ByteArray>; typedef FlxGraphicAsset = OneOfThree; typedef FlxTilemapGraphicAsset = OneOfFour; typedef FlxBitmapFontGraphicAsset = OneOfFour; diff --git a/flixel/system/frontEnds/AssetFrontEnd.hx b/flixel/system/frontEnds/AssetFrontEnd.hx index 68276214ae..676671cbe6 100644 --- a/flixel/system/frontEnds/AssetFrontEnd.hx +++ b/flixel/system/frontEnds/AssetFrontEnd.hx @@ -90,6 +90,7 @@ class AssetFrontEnd #else public final defaultSoundExtension:String = '.${haxe.macro.Compiler.getDefine("FLX_DEFAULT_SOUND_EXT")}'; #end + public final soundExtensions:Array = ["mp3", "ogg", "wav", "flac", "opus"]; /** * Used by methods like `getAsset`, `getBitmapData`, `getText`, their "unsafe" counterparts and @@ -119,7 +120,10 @@ class AssetFrontEnd sys.io.File.getContent(getPath(id)); case BINARY: sys.io.File.getBytes(getPath(id)); - + case MUSIC: + final buffer = lime.media.AudioBuffer.fromFile(getPath(id), true); + Sound.fromAudioBuffer(buffer); + // Check cache case IMAGE if (canUseCache && Assets.cache.hasBitmapData(id)): Assets.cache.getBitmapData(id); @@ -159,6 +163,7 @@ class AssetFrontEnd case BINARY: Assets.getBytes(id); case IMAGE: Assets.getBitmapData(id, useCache); case SOUND: Assets.getSound(id, useCache); + case MUSIC: Assets.getMusic(id, useCache); case FONT: Assets.getFont(id, useCache); } } @@ -224,6 +229,7 @@ class AssetFrontEnd case BINARY: Assets.loadBytes(id); case IMAGE: Assets.loadBitmapData(id, useCache); case SOUND: Assets.loadSound(id, useCache); + case MUSIC: Assets.loadSound(id, useCache); case FONT: Assets.loadFont(id, useCache); } } @@ -237,6 +243,9 @@ class AssetFrontEnd */ public dynamic function exists(id:String, ?type:FlxAssetType) { + if (type == MUSIC) + type = SOUND; + #if FLX_DEFAULT_SOUND_EXT // add file extension if (type == SOUND) @@ -265,6 +274,9 @@ class AssetFrontEnd */ public dynamic function isLocal(id:String, ?type:FlxAssetType, useCache = true) { + if (type == MUSIC) + type = SOUND; + #if FLX_DEFAULT_SOUND_EXT // add file extension if (type == SOUND) @@ -291,6 +303,9 @@ class AssetFrontEnd */ public dynamic function list(?type:FlxAssetType) { + if (type == MUSIC) + type = SOUND; + #if FLX_STANDARD_ASSETS_DIRECTORY return Assets.list(type.toOpenFlType()); #else @@ -319,7 +334,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return A new BitmapData object */ - public inline function getBitmapDataUnsafe(id:String, useCache = false):BitmapData + public function getBitmapDataUnsafe(id:String, useCache = false):BitmapData { return cast getAssetUnsafe(id, IMAGE, useCache); } @@ -331,7 +346,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return A new BitmapData object */ - public inline function getBitmapData(id:String, useCache = false, ?logStyle:LogStyle):BitmapData + public function getBitmapData(id:String, useCache = false, ?logStyle:LogStyle):BitmapData { return cast getAsset(id, IMAGE, useCache, logStyle); } @@ -343,8 +358,15 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return A new `Sound` object Note: Dos not return a `FlxSound` */ - public inline function getSoundUnsafe(id:String, useCache = true):Sound + public function getSoundUnsafe(id:String, useCache = true):Sound { + var path = new Path(id), s:String; + for (ext in soundExtensions) + { + path.ext = ext; + s = path.toString(); + if (exists(s, SOUND)) return cast getAssetUnsafe(s, SOUND, useCache); + } return cast getAssetUnsafe(addSoundExtIf(id), SOUND, useCache); } @@ -358,8 +380,15 @@ class AssetFrontEnd * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default * @return A new `Sound` object Note: Dos not return a `FlxSound` */ - public inline function getSound(id:String, useCache = true, ?logStyle:LogStyle):Sound + public function getSound(id:String, useCache = true, ?logStyle:LogStyle):Sound { + var path = new Path(id), s:String; + for (ext in soundExtensions) + { + path.ext = ext; + s = path.toString(); + if (exists(s, SOUND)) return cast getAsset(s, SOUND, useCache, logStyle); + } return cast getAsset(addSoundExtIf(id), SOUND, useCache, logStyle); } @@ -371,7 +400,7 @@ class AssetFrontEnd * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default * @return A new `Sound` object Note: Dos not return a `FlxSound` */ - public inline function getSoundAddExt(id:String, useCache = true, ?logStyle:LogStyle):Sound + public function getSoundAddExt(id:String, useCache = true, ?logStyle:LogStyle):Sound { return getSound(addSoundExt(id), useCache, logStyle); } @@ -393,7 +422,125 @@ class AssetFrontEnd return id; } - + + /** + * Gets an instance of a streamed sound. Unlike its "safe" counterpart, there is no log on missing assets. + * Can be set to a custom function to avoid the existing asset system. + * + * Streamed sounds load and unload chunks of audio data during playback, keeping memory usage low. + * The usage of streamed sounds is only recommended for larger audio tracks, such as music. + * + * **Note**: Due to a backend limitation, streamed sounds currently only work on native targets and OGG/Vorbis files. + * ...Not anymore :) with FunkinCrew's Lime. + * + * Trying to stream an unsupported file format will fall back to regular sound loading behavior. + * + * @param id The ID or asset path for the sound + * @return A new `Sound` object Note: Does not return a `FlxSound` + * @since 6.2.0 + */ + public dynamic function streamSoundUnsafe(id:String, useCache = true):Sound + { + var path = new Path(id), s:String; + for (ext in soundExtensions) + { + path.ext = ext; + s = path.toString(); + if (exists(s, SOUND)) return cast getAssetUnsafe(s, MUSIC, useCache); + } + return cast getAssetUnsafe(addSoundExtIf(id), MUSIC, useCache); + } + + /** + * Gets an instance of a streamed sound, logs when the asset is not found. + * + * Streamed sounds load and unload chunks of audio data during playback, keeping memory usage low. + * The usage of streamed sounds is only recommended for larger audio tracks, such as music. + * + * **Note**: Due to a backend limitation, streamed sounds currently only work on native targets and OGG/Vorbis files. + * ...Not anymore :) with FunkinCrew's Lime. + * + * Trying to stream an unsupported file format will fall back to regular sound loading behavior. + * + * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension + * + * @param id The ID or asset path for the sound + * @param useCache Whether to allow use of the asset cache (if one exists) + * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default + * @return A new `Sound` object Note: Does not return a `FlxSound` + * @since 6.2.0 + */ + public function streamSound(id:String, useCache = true, ?logStyle:LogStyle):Sound + { + var path = new Path(id), s:String; + for (ext in soundExtensions) + { + path.ext = ext; + s = path.toString(); + if (exists(s, SOUND)) return cast getAsset(s, MUSIC, useCache, logStyle); + } + return cast getAsset(addSoundExtIf(id), MUSIC, useCache, logStyle); + } + + /** + * Gets an instance of a streamed sound, logs when the asset is not found. + * + * Streamed sounds load and unload chunks of audio data during playback, keeping memory usage low. + * The usage of streamed sounds is only recommended for larger audio tracks, such as music. + * + * **Note**: Due to a backend limitation, streamed sounds currently only work on native targets and OGG/Vorbis files. + * ...Not anymore :) with FunkinCrew's Lime. + * + * Trying to stream an unsupported file format will fall back to regular sound loading behavior. + * + * @param id The ID or asset path for the sound + * @param useCache Whether to allow use of the asset cache (if one exists) + * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default + * @return A new `Sound` object Note: Does not return a `FlxSound` + * @since 6.2.0 + */ + public function streamSoundAddExt(id:String, ?logStyle:LogStyle):Sound + { + return streamSound(addSoundExt(id)); + } + + /** + * Checks whether the sound asset with the specified ID can be streamed. + * + * **Note**: Due to a backend limitation, streamed sounds currently only work on native targets and OGG/Vorbis files. + * ...Not anymore :) with FunkinCrew's Lime. + * + * **Note:** If the `FLX_DEFAULT_SOUND_EXT` flag is enabled, you may omit the file extension + * + * @param file The ID or asset path for the asset + * @return Returns whether the sound can be streamed or not. + * @since 6.2.0 + */ + public function canStreamSound(id:String):Bool + { + #if (lime_funkin && lime_native) + final decoder = lime.media.AudioDecoder.fromFile(Assets.getPath(addSoundExtIf(id))); + if (decoder != null) + { + var seekable = decoder.seekable(); + decoder.dispose(); + + return seekable; + } + #elseif lime_vorbis + // Check if file is really OGG/Vorbis + final vorbis = lime.media.vorbis.VorbisFile.fromFile(Assets.getPath(addSoundExtIf(id))); + if (vorbis != null) + { + vorbis.clear(); + + return true; + } + #end + + return false; + } + /** * Gets the contents of a text-based asset. Unlike its "safe" counterpart, there is no log * on missing assets @@ -403,7 +550,7 @@ class AssetFrontEnd * @param id The ID or asset path for the asset * @param useCache Whether to allow use of the asset cache (if one exists) */ - public inline function getTextUnsafe(id:String, useCache = true):String + public function getTextUnsafe(id:String, useCache = true):String { return cast getAssetUnsafe(id, TEXT, useCache); } @@ -417,7 +564,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default */ - public inline function getText(id:String, useCache = true, ?logStyle:LogStyle):String + public function getText(id:String, useCache = true, ?logStyle:LogStyle):String { return cast getAsset(id, TEXT, useCache, logStyle); } @@ -431,7 +578,7 @@ class AssetFrontEnd * @param id The ID or asset path for the asset * @param useCache Whether to allow use of the asset cache (if one exists) */ - public inline function getXmlUnsafe(id:String, useCache = true) + public function getXmlUnsafe(id:String, useCache = true) { final text = getTextUnsafe(id, useCache); return text != null ? parseXml(text) : null; @@ -446,7 +593,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default */ - public inline function getXml(id:String, useCache = true, ?logStyle:LogStyle) + public function getXml(id:String, useCache = true, ?logStyle:LogStyle) { final text = getText(id, useCache, logStyle); return text != null ? parseXml(text) : null; @@ -461,7 +608,7 @@ class AssetFrontEnd * @param id The ID or asset path for the asset * @param useCache Whether to allow use of the asset cache (if one exists) */ - public inline function getJsonUnsafe(id:String, useCache = true) + public function getJsonUnsafe(id:String, useCache = true) { final text = getTextUnsafe(id, useCache); return text != null ? parseJson(text) : null; @@ -476,7 +623,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default */ - public inline function getJson(id:String, useCache = true, ?logStyle:LogStyle) + public function getJson(id:String, useCache = true, ?logStyle:LogStyle) { final text = getText(id, useCache, logStyle); return text != null ? parseJson(text) : null; @@ -491,7 +638,7 @@ class AssetFrontEnd * @param id The ID or asset path for the asset * @param useCache Whether to allow use of the asset cache (if one exists) */ - public inline function getBytesUnsafe(id:String, useCache = true):Bytes + public function getBytesUnsafe(id:String, useCache = true):Bytes { return cast getAssetUnsafe(id, BINARY, useCache); } @@ -505,7 +652,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default */ - public inline function getBytes(id:String, useCache = true, ?logStyle:LogStyle):Bytes + public function getBytes(id:String, useCache = true, ?logStyle:LogStyle):Bytes { return cast getAsset(id, BINARY, useCache); } @@ -517,7 +664,7 @@ class AssetFrontEnd * @param id The ID or asset path for the asset * @param useCache Whether to allow use of the asset cache */ - public inline function getFontUnsafe(id:String, useCache = true):Font + public function getFontUnsafe(id:String, useCache = true):Font { return cast getAssetUnsafe(id, FONT, useCache); } @@ -529,7 +676,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @param logStyle How to log, if the asset is not found. Uses `LogStyle.ERROR` by default */ - public inline function getFont(id:String, useCache = true, ?logStyle:LogStyle):Font + public function getFont(id:String, useCache = true, ?logStyle:LogStyle):Font { return cast getAsset(id, FONT, useCache, logStyle); } @@ -541,7 +688,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadBitmapData(id:String, useCache = false):Future + public function loadBitmapData(id:String, useCache = false):Future { return cast loadAsset(id, IMAGE, useCache); } @@ -553,7 +700,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadSound(id:String, useCache = true):Future + public function loadSound(id:String, useCache = true):Future { return cast loadAsset(id, SOUND, useCache); } @@ -567,7 +714,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadText(id:String, useCache = true):Future + public function loadText(id:String, useCache = true):Future { return cast loadAsset(id, TEXT, useCache); } @@ -581,7 +728,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadXml(id:String, useCache = true):Future + public function loadXml(id:String, useCache = true):Future { return wrapFuture(loadText(id, useCache), parseXml); } @@ -595,7 +742,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadJson(id:String, useCache = true):Future + public function loadJson(id:String, useCache = true):Future { return wrapFuture(loadText(id, useCache), parseJson); } @@ -609,7 +756,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadBytes(id:String, useCache = true):Future + public function loadBytes(id:String, useCache = true):Future { return cast loadAsset(id, BINARY, useCache); } @@ -621,7 +768,7 @@ class AssetFrontEnd * @param useCache Whether to allow use of the asset cache (if one exists) * @return Returns a `Future` which allows listeners to be added via methods like `onComplete` */ - public inline function loadFont(id:String, useCache = true):Future + public function loadFont(id:String, useCache = true):Future { return cast loadAsset(id, FONT, useCache); } @@ -629,7 +776,7 @@ class AssetFrontEnd /** * Parses a json string, creates and returns a struct */ - public inline function parseJson(jsonText:String) + public function parseJson(jsonText:String) { return Json.parse(jsonText); } @@ -637,7 +784,7 @@ class AssetFrontEnd /** * Parses an xml string, creates and returns an `Xml` object */ - public inline function parseXml(xmlText:String) + public function parseXml(xmlText:String) { return Xml.parse(xmlText); } @@ -672,6 +819,9 @@ enum abstract FlxAssetType(String) /** Audio assets, such as *.ogg or *.wav files */ var SOUND = "sound"; + + /** Music assets, streamable sounds */ + var MUSIC = "music"; /** Text assets */ var TEXT = "text"; @@ -684,6 +834,7 @@ enum abstract FlxAssetType(String) case FONT: AssetType.FONT; case IMAGE: AssetType.IMAGE; case SOUND: AssetType.SOUND; + case MUSIC: AssetType.MUSIC; case TEXT: AssetType.TEXT; } } diff --git a/flixel/system/frontEnds/SoundFrontEnd.hx b/flixel/system/frontEnds/SoundFrontEnd.hx index 84fa91d237..508b15234c 100644 --- a/flixel/system/frontEnds/SoundFrontEnd.hx +++ b/flixel/system/frontEnds/SoundFrontEnd.hx @@ -6,40 +6,68 @@ import flixel.group.FlxGroup; import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.sound.FlxSound; +import flixel.sound.FlxSoundData; import flixel.sound.FlxSoundGroup; import flixel.system.FlxAssets; import flixel.system.ui.FlxSoundTray; import flixel.text.FlxInputText; +import flixel.util.FlxArrayUtil; import flixel.util.FlxSignal; import openfl.media.Sound; +import openfl.utils.Assets; +import lime.media.AudioManager; +import lime.media.AudioBuffer; +import haxe.io.Bytes; /** * Accessed via `FlxG.sound`. */ +@:allow(flixel.sound.FlxSound) @:allow(flixel.FlxG) class SoundFrontEnd { + /** + * How much sounds to keep in the list after clearing between states. + * + * @since FunkinCrew's Flixel + */ + public static var poolMaxSounds:Int = 16; + /** * A handy container for a background music object. */ public var music:FlxSound; + /** + * Whether or not should it automatically switch to a new default playback device if detected. + */ + public var automaticDefaultDevice(get, set):Bool; + + /** + * The current used playback device name to play audios. + */ + public var deviceName(get, set):String; + + /** + * Set this to a number between 0 and 1 to change the global volume. + */ + public var volume(get, set):Float; + /** * Whether or not the game sounds are muted. */ - public var muted(default, set):Bool = false; - - public function set_muted(v:Bool):Bool - { - return muted = #if mobile false #else v #end; - } + public var muted(get, set):Bool; + /** + * A Read only variable to check if it's paused or not. + */ + public var paused(default, null):Bool = false; /** * Set this hook to get a callback whenever the volume changes. * Function should take the form myVolumeHandler(volume:Float). */ - @:deprecated("volumeHandler is deprecated, use onVolumeChange, instead") + //@:deprecated("volumeHandler is deprecated, use onVolumeChange instead") public var volumeHandler:Float->Void; /** @@ -47,6 +75,21 @@ class SoundFrontEnd */ public var onVolumeChange(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** + * Dispatched when the default for the playback device is changed. + */ + public var onDefaultDeviceChanged(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + /** + * Dispatched whenever a playback device is added. + */ + public var onDeviceAdded(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + /** + * Dispatched whenever a playbck device is removed. + */ + public var onDeviceRemoved(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + #if FLX_KEYBOARD /** * The key codes used to increase volume (see FlxG.keys for the keys available). @@ -102,9 +145,15 @@ class SoundFrontEnd public var list(default, null):FlxTypedGroup = new FlxTypedGroup(); /** - * Set this to a number between 0 and 1 to change the global volume. + * Whether or not can it be paused on lost focus (if FlxG.autoPause is true). */ - public var volume(default, set):Float = 1; + public var canAutoPause:Bool = true; + + var _volume:Float = 1.0; + var _muted:Bool = false; + var _lastTimeScale:Float; + var _lostFocusPause:Bool; + var _cache:Map; /** * Set up and play a looping background soundtrack. @@ -170,7 +219,7 @@ class SoundFrontEnd sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); loadHelper(sound, volume, group, autoPlay); // Call OnlLoad() because the sound already loaded - if (onLoad != null && sound._sound != null) + if (onLoad != null && sound.data != null) onLoad(); } else @@ -188,7 +237,7 @@ class SoundFrontEnd } } - sound.loadStream(url, looped, autoDestroy, onComplete, loadCallback); + sound.loadFromURL(url, looped, autoDestroy, onComplete, loadCallback); loadHelper(sound, volume, group); } @@ -197,8 +246,7 @@ class SoundFrontEnd function loadHelper(sound:FlxSound, volume:Float, group:FlxSoundGroup, autoPlay = false):FlxSound { - if (group == null) - group = defaultSoundGroup; + if (group == null) group = defaultSoundGroup; sound.volume = volume; group.add(sound); @@ -216,6 +264,7 @@ class SoundFrontEnd * @param embeddedSound Name of sound assets specified in your .xml project file * @return Cached Sound object */ + @:deprecated("cache() is deprecated, use FlxSoundData.fromAsset() instead.") public inline function cache(embeddedSound:String):Sound { // load the sound into the OpenFL assets cache @@ -233,7 +282,7 @@ class SoundFrontEnd { for (id in FlxG.assets.list(SOUND)) { - cache(id); + FlxSoundData.fromAssetKey(id); } } @@ -253,10 +302,6 @@ class SoundFrontEnd */ public function play(embeddedSound:FlxSoundAsset, volume = 1.0, looped = false, ?group:FlxSoundGroup, autoDestroy = true, ?onComplete:Void->Void):FlxSound { - if ((embeddedSound is String)) - { - embeddedSound = cache(embeddedSound); - } var sound = list.recycle(FlxSound).loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); return loadHelper(sound, volume, group, true); } @@ -282,41 +327,51 @@ class SoundFrontEnd } /** - * Pause all sounds currently playing. + * Pauses every audios that are listed that are about to and currently playing. */ public function pause():Void { - if (music != null && music.exists && music.active) + if (music != null && music.exists) { - music.pause(); + if (music._pausedPlay = music.source.playing) music.source.pause(); + music._pausedByHandler = true; } for (sound in list.members) { - if (sound != null && sound.exists && sound.active) + if (sound != null && sound.exists) { - sound.pause(); + if (sound._pausedPlay = sound.source.playing) sound.source.pause(); + sound._pausedByHandler = true; } } + + paused = true; } /** - * Resume playing existing sounds. + * Resumes back every audios that was playing and plays the pending audios. */ public function resume():Void { - if (music != null && music.exists) + if (music != null && music.exists && music._pausedByHandler) { - music.resume(); + music._pausedByHandler = false; + if (music._pausedPlay) music.source.play(); + music._pausedPlay = false; } for (sound in list.members) { - if (sound != null && sound.exists) + if (sound != null && sound.exists && sound._pausedByHandler) { - sound.resume(); + sound._pausedByHandler = false; + if (sound._pausedPlay) sound.source.play(); + sound._pausedPlay = false; } } + + paused = false; } /** @@ -332,43 +387,192 @@ class SoundFrontEnd music = null; } - for (sound in list.members) + // Effectively removing null sounds and removing destroyed sounds if it exceed max pool count. + var i = list.members.length, n = 0, sound:FlxSound; + while (i-- > 0) { - if (sound != null && (forceDestroy || !sound.persist)) + sound = list.members[i]; + if (sound == null) + { + FlxArrayUtil.swapAndPop(list.members, i); + } + else if (forceDestroy || !sound.persist) { + if (n < poolMaxSounds) n++; + else FlxArrayUtil.swapAndPop(list.members, i); sound.destroy(); } + else + { + n++; + } } + + // bypass the null set accessor. + Reflect.setField(list.members, "length", n); } /** - * Toggles muted, also activating the sound tray. + * Check the local sound data cache to see if a sound data with this key has been loaded already. + * + * @param key The key identifying the sound data. + * @return Whether or not this file can be found in the cache. + * + * @since FunkinCrew's Flixel */ - @:haxe.warning("-WDeprecated") - public function toggleMuted():Void + public inline function checkCache(key:String):Bool { - muted = !muted; + return getCache(key) != null; + } - if (volumeHandler != null) + /** + * Removes and destroys a cached `FlxSoundData` from memory with specified key. + * @param key Key of the cached sound data. + * @param destroy Should it automatically destroys it after removing (Default is `true`). + * + * @since FunkinCrew's Flixel + */ + public function removeCache(key:String, destroy = true):Void + { + if (key == null) return; + + if (destroy) { - volumeHandler(muted ? 0 : volume); + var obj = getCache(key); + if (obj != null) obj.destroy(); } - onVolumeChange.dispatch(muted ? 0 : volume); + Assets.cache.removeSound(key); + _cache.remove(key); + } + + /** + * Caches the specified sound data. + * + * @param soundData The sound data to cache. + * @return The cached sound data. + * + * @since FunkinCrew's Flixel + */ + public inline function addCache(soundData:FlxSoundData):FlxSoundData + { + if (soundData != null && (soundData.key is String)) _cache.set(soundData.key, soundData); + return soundData; + } + + /** + * Gets a cached `FlxSoundData` with specified key. + * @param key Key of the cached sound data. + * @return The `FlxSoundData` with the specified key, or null if the object doesn't exist. + * + * @since FunkinCrew's Flixel + */ + public inline function getCache(key:String):FlxSoundData + { + return _cache.get(key); + } + + /** + * Clears audio data cache (and destroys those auio datas). + * `FlxSoundData` object will be removed and destroyed only if it shouldn't persist in the cache and its useCount is 0. + * + * @since FunkinCrew's Flixel + */ + public function clearCache():Void + { + if (_cache == null) + { + _cache = new Map(); + return; + } + + for (key in _cache.keys()) + { + var obj = _cache.get(key); + if (obj.unused) + { + Assets.cache.removeSound(key); + _cache.remove(key); + obj.destroy(); + } + else if (obj != null && !obj.persist && obj.useCount <= 0) + { + obj.unused = true; + } + } + } + + /** + * Completely resets audio data cache, which means destroying ALL of the cached FlxSoundData objects. + * + * @since FunkinCrew's Flixel + */ + public function resetCache():Void + { + if (_cache == null) + { + _cache = new Map(); + return; + } + + for (key in _cache.keys()) removeCache(key); + } + + /** + * Removes all unused sound datas from cache, + * but skips somes which should persist in cache and shouldn't be destroyed on no use. + * + * @since FunkinCrew's Flixel + */ + public function clearUnused():Void + { + for (key in _cache.keys()) + { + var obj = _cache.get(key); + if (obj != null && obj.useCount <= 0 && !obj.persist && obj.destroyOnNoUse) + { + Assets.cache.removeSound(key); + _cache.remove(key); + obj.destroy(); + } + } + } + + /** + * Gets a key from a cached AudioBuffer. + * + * @param buffer AudioBuffer to find in the cache. + * @return The AudioBuffer's key or null if there isn't such AudioBuffer in cache. + * + * @since FunkinCrew's Flixel + */ + public function findKeyForBuffer(buffer:AudioBuffer):Null + { + for (key in _cache.keys()) + { + var obj = _cache.get(key); + if (obj != null && obj.buffer == buffer) return key; + } + return null; + } + /** + * Toggles muted, also activating the sound tray. + */ + public function toggleMuted():Void + { + muted = !muted; showSoundTray(true); } /** * Changes the volume by a certain amount, also activating the sound tray. */ - public function changeVolume(Amount:Float):Void + public function changeVolume(value:Float):Void { + volume = linearToLog(logToLinear(_volume) + value); muted = false; - volume = logToLinear(volume); - volume += Amount; - volume = linearToLog(volume); - showSoundTray(Amount > 0); + showSoundTray(value > 0); } public function linearToLog(x:Float, minValue:Float = 0.001):Float @@ -385,8 +589,8 @@ class SoundFrontEnd public function logToLinear(x:Float, minValue:Float = 0.001):Float { - // If logarithmic volume is 0, return 0 - if (x <= 0) return 0; + // If logarithmic volume is below than minValue, return 0 + if (x <= minValue) return 0; // Ensure x is between minValue and 1 x = Math.min(1, x); @@ -442,6 +646,11 @@ class SoundFrontEnd function new() { + resetCache(); + + AudioManager.onDefaultPlaybackDeviceChanged.add(onDefaultDeviceChanged.dispatch); + AudioManager.onPlaybackDeviceAdded.add(onDeviceAdded.dispatch); + AudioManager.onPlaybackDeviceRemoved.add(onDeviceRemoved.dispatch); #if FLX_SAVE loadSavedPrefs(); #end @@ -453,12 +662,6 @@ class SoundFrontEnd @:allow(flixel.FlxGame) function update(elapsed:Float):Void { - if (music != null && music.active) - music.update(elapsed); - - if (list != null && list.active) - list.update(elapsed); - #if FLX_KEYBOARD if (!FlxInputText.globalManager.isTyping) { @@ -470,39 +673,40 @@ class SoundFrontEnd changeVolume(-0.1); } #end - } - @:allow(flixel.FlxGame) - function onFocusLost():Void - { - if (music != null) + if (!paused) { - music.onFocusLost(); - } - - for (sound in list.members) - { - if (sound != null) + if (_lastTimeScale != FlxG.timeScale) { - sound.onFocusLost(); + _lastTimeScale = FlxG.timeScale; + if (music != null && music.active) music._updatePitch(); + for (sound in list.members) + { + if (sound != null && sound.active) sound._updatePitch(); + } } + + if (music != null && music.active) music.update(elapsed); + if (list != null && list.active) list.update(elapsed); } } @:allow(flixel.FlxGame) - function onFocus():Void + function onFocusLost():Void { - if (music != null) + if (_lostFocusPause = canAutoPause && FlxG.autoPause && !paused) { - music.onFocus(); + pause(); } + } - for (sound in list.members) + @:allow(flixel.FlxGame) + function onFocus():Void + { + if (_lostFocusPause) { - if (sound != null) - { - sound.onFocus(); - } + _lostFocusPause = false; + resume(); } } @@ -517,30 +721,138 @@ class SoundFrontEnd if (FlxG.save.data.volume != null) { - volume = FlxG.save.data.volume; + set_volume(FlxG.save.data.volume); } if (FlxG.save.data.mute != null) { - muted = FlxG.save.data.mute; + set_muted(FlxG.save.data.mute); } } #end - @:haxe.warning("-WDeprecated") - function set_volume(Volume:Float):Float + function updateVolume():Void { - #if mobile Volume = 1; #end - volume = FlxMath.bound(Volume, 0, 1); + if (music != null && music.exists) + { + music._updateVolume(); + } - if (volumeHandler != null) + for (sound in list.members) { - volumeHandler(muted ? 0 : volume); + if (sound != null && sound.exists) + { + sound._updateVolume(); + } } + } - onVolumeChange.dispatch(muted ? 0 : volume); + inline function get_automaticDefaultDevice():Bool + { + return AudioManager.automaticDefaultPlaybackDevice; + } - return volume; + function set_automaticDefaultDevice(value:Bool):Bool + { + if (AudioManager.automaticDefaultPlaybackDevice != value) + { + AudioManager.automaticDefaultPlaybackDevice = value; + if (value && AudioManager.getCurrentPlaybackDeviceName() != AudioManager.getPlaybackDefaultDeviceName()) + { + AudioManager.refresh(); + } + } + return value; + } + + inline function get_deviceName():String + { + return AudioManager.getCurrentPlaybackDeviceName(); + } + + function set_deviceName(value:String):String + { + if (AudioManager.getCurrentPlaybackDeviceName() != value) + { + if (AudioManager.refresh(value)) return value; + else + { + AudioManager.refresh(); + return AudioManager.getCurrentPlaybackDeviceName(); + } + } + else + { + return value; + } + } + + function get_volume():Float + { + return _volume; + } + + function set_volume(value:Float):Float + { + var prevVolume = _volume; + _volume = FlxMath.bound(value, 0, 1); + + // https://github.com/FunkinCrew/flixel/pull/12 + #if !mobile + // Initially for the audio overhaul changes generally, it was made to use the global volume in AudioManager, + // instead of iterating every FlxSounds, but ensues an issues where sounds outside flixel (openfl, lime, hxvlc) + // are affected too, so this was reverted. -raltyro + /* + if (AudioManager.muted) + { + AudioManager.gain = 0; + } + else + { + AudioManager.gain = applySoundCurve(value); + if (value != _volume) + { + if (volumeHandler != null) volumeHandler(value); + onVolumeChange.dispatch(value); + } + } + */ + + if (!_muted && _volume != prevVolume) + { + updateVolume(); + + if (volumeHandler != null) volumeHandler(value); + onVolumeChange.dispatch(value); + } + #end + + return _volume; + } + + function get_muted():Bool + { + return _muted; + } + + function set_muted(value:Bool):Bool + { + // https://github.com/FunkinCrew/flixel/pull/12 + #if mobile + return _muted = value; + #else + if (_muted != value) + { + _muted = value; + updateVolume(); + + var volume = value ? 0 : _volume; + if (volumeHandler != null) volumeHandler(volume); + onVolumeChange.dispatch(volume); + } + + return value; + #end } } #end diff --git a/flixel/system/macros/FlxDefines.hx b/flixel/system/macros/FlxDefines.hx index 34095c767b..7247070838 100644 --- a/flixel/system/macros/FlxDefines.hx +++ b/flixel/system/macros/FlxDefines.hx @@ -116,6 +116,8 @@ private enum HelperDefines /** The normalized, absolute path of `FLX_CUSTOM_ASSETS_DIRECTORY`, used internally */ FLX_CUSTOM_ASSETS_DIRECTORY_ABS; FLX_NO_DEFAULT_SOUND_EXT; + /** Enables audio streaming related APIs */ + FLX_STREAM_SOUND; } class FlxDefines @@ -312,6 +314,10 @@ class FlxDefines } else // define boolean inversion define(FLX_STANDARD_ASSETS_DIRECTORY); + + #if (lime_funkin || lime_vorbis) + define(FLX_STREAM_SOUND); + #end } static function defineInversion(userDefine:UserDefines, invertedDefine:HelperDefines) diff --git a/flixel/util/typeLimit/OneOfFive.hx b/flixel/util/typeLimit/OneOfFive.hx new file mode 100644 index 0000000000..5b214cde00 --- /dev/null +++ b/flixel/util/typeLimit/OneOfFive.hx @@ -0,0 +1,9 @@ +package flixel.util.typeLimit; + +/** + * Useful to limit a Dynamic function argument's type to the specified + * type parameters. This does NOT make the use of Dynamic type-safe in + * any way (the underlying type is still Dynamic and Std.isOfType() checks + + * casts are necessary). + */ +abstract OneOfFive(Dynamic) from T1 from T2 from T3 from T4 from T5 to T1 to T2 to T3 to T4 to T5 {} diff --git a/flixel/util/typeLimit/OneOfSix.hx b/flixel/util/typeLimit/OneOfSix.hx new file mode 100644 index 0000000000..df9d6c6aa8 --- /dev/null +++ b/flixel/util/typeLimit/OneOfSix.hx @@ -0,0 +1,9 @@ +package flixel.util.typeLimit; + +/** + * Useful to limit a Dynamic function argument's type to the specified + * type parameters. This does NOT make the use of Dynamic type-safe in + * any way (the underlying type is still Dynamic and Std.isOfType() checks + + * casts are necessary). + */ +abstract OneOfSix(Dynamic) from T1 from T2 from T3 from T4 from T5 from T6 to T1 to T2 to T3 to T4 to T5 to T6 {}