2023-12-06 20:04:24 +00:00
|
|
|
package funkin.audio;
|
|
|
|
|
|
|
|
#if flash11
|
|
|
|
import flash.media.Sound;
|
|
|
|
import flash.utils.ByteArray;
|
|
|
|
#end
|
|
|
|
import flixel.sound.FlxSound;
|
|
|
|
import flixel.group.FlxGroup.FlxTypedGroup;
|
|
|
|
import flixel.system.FlxAssets.FlxSoundAsset;
|
2024-01-27 08:24:49 +00:00
|
|
|
import funkin.util.tools.ICloneable;
|
2024-02-09 19:58:57 +00:00
|
|
|
import funkin.audio.waveform.WaveformData;
|
|
|
|
import funkin.audio.waveform.WaveformDataParser;
|
2024-01-31 02:49:49 +00:00
|
|
|
import flixel.math.FlxMath;
|
2023-12-06 20:04:24 +00:00
|
|
|
import openfl.Assets;
|
|
|
|
#if (openfl >= "8.0.0")
|
|
|
|
import openfl.utils.AssetType;
|
|
|
|
#end
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A FlxSound which adds additional functionality:
|
|
|
|
* - Delayed playback via negative song position.
|
|
|
|
*/
|
|
|
|
@:nullSafety
|
2024-01-27 08:24:49 +00:00
|
|
|
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
2023-12-06 20:04:24 +00:00
|
|
|
{
|
2024-02-20 18:37:53 +00:00
|
|
|
static final MAX_VOLUME:Float = 1.0;
|
2024-01-31 02:49:49 +00:00
|
|
|
|
2024-02-28 05:19:08 +00:00
|
|
|
/**
|
|
|
|
* Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
|
|
|
|
*/
|
2023-12-06 20:04:24 +00:00
|
|
|
static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
|
|
|
|
|
2024-01-31 02:49:49 +00:00
|
|
|
public var muted(default, set):Bool = false;
|
|
|
|
|
|
|
|
function set_muted(value:Bool):Bool
|
|
|
|
{
|
|
|
|
if (value == muted) return value;
|
|
|
|
muted = value;
|
|
|
|
updateTransform();
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
override function set_volume(value:Float):Float
|
|
|
|
{
|
|
|
|
// Uncap the volume.
|
|
|
|
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
|
|
|
|
updateTransform();
|
|
|
|
return _volume;
|
|
|
|
}
|
|
|
|
|
2024-01-27 08:24:49 +00:00
|
|
|
public var paused(get, never):Bool;
|
|
|
|
|
|
|
|
function get_paused():Bool
|
|
|
|
{
|
|
|
|
return this._paused;
|
|
|
|
}
|
|
|
|
|
2023-12-07 03:03:36 +00:00
|
|
|
public var isPlaying(get, never):Bool;
|
|
|
|
|
|
|
|
function get_isPlaying():Bool
|
|
|
|
{
|
|
|
|
return this.playing || this._shouldPlay;
|
|
|
|
}
|
|
|
|
|
2024-02-09 19:58:57 +00:00
|
|
|
/**
|
|
|
|
* Waveform data for this sound.
|
|
|
|
* This is lazily loaded, so it will be built the first time it is accessed.
|
|
|
|
*/
|
|
|
|
public var waveformData(get, never):WaveformData;
|
|
|
|
|
|
|
|
var _waveformData:Null<WaveformData> = null;
|
|
|
|
|
|
|
|
function get_waveformData():WaveformData
|
|
|
|
{
|
|
|
|
if (_waveformData == null)
|
|
|
|
{
|
|
|
|
_waveformData = WaveformDataParser.interpretFlxSound(this);
|
|
|
|
if (_waveformData == null) throw 'Could not interpret waveform data!';
|
|
|
|
}
|
|
|
|
return _waveformData;
|
|
|
|
}
|
|
|
|
|
2023-12-06 20:04:24 +00:00
|
|
|
/**
|
|
|
|
* Are we in a state where the song should play but time is negative?
|
|
|
|
*/
|
2023-12-07 03:03:36 +00:00
|
|
|
var _shouldPlay:Bool = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For debug purposes.
|
|
|
|
*/
|
|
|
|
var _label:String = "unknown";
|
2023-12-06 20:04:24 +00:00
|
|
|
|
|
|
|
public function new()
|
|
|
|
{
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
public override function update(elapsedSec:Float)
|
|
|
|
{
|
2023-12-07 03:03:36 +00:00
|
|
|
if (!playing && !_shouldPlay) return;
|
2023-12-06 20:04:24 +00:00
|
|
|
|
|
|
|
if (_time < 0)
|
|
|
|
{
|
|
|
|
var elapsedMs = elapsedSec * Constants.MS_PER_SEC;
|
|
|
|
_time += elapsedMs;
|
|
|
|
if (_time >= 0)
|
|
|
|
{
|
|
|
|
super.play();
|
2023-12-07 03:03:36 +00:00
|
|
|
_shouldPlay = false;
|
2023-12-06 20:04:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
super.update(elapsedSec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-20 19:07:31 +00:00
|
|
|
public function togglePlayback():FunkinSound
|
|
|
|
{
|
|
|
|
if (playing)
|
|
|
|
{
|
|
|
|
pause();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
resume();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2023-12-06 20:04:24 +00:00
|
|
|
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
|
|
|
|
{
|
|
|
|
if (!exists) return this;
|
|
|
|
|
|
|
|
if (forceRestart)
|
|
|
|
{
|
|
|
|
cleanup(false, true);
|
|
|
|
}
|
2023-12-07 03:03:36 +00:00
|
|
|
else if (playing)
|
2023-12-06 20:04:24 +00:00
|
|
|
{
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (startTime < 0)
|
|
|
|
{
|
2023-12-07 03:03:36 +00:00
|
|
|
this.active = true;
|
|
|
|
this._shouldPlay = true;
|
|
|
|
this._time = startTime;
|
2023-12-06 20:04:24 +00:00
|
|
|
this.endTime = endTime;
|
|
|
|
return this;
|
|
|
|
}
|
2023-12-07 03:03:36 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if (_paused)
|
|
|
|
{
|
|
|
|
resume();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
startSound(startTime);
|
|
|
|
}
|
2023-12-06 20:04:24 +00:00
|
|
|
|
2023-12-07 03:03:36 +00:00
|
|
|
this.endTime = endTime;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override function pause():FunkinSound
|
|
|
|
{
|
|
|
|
super.pause();
|
|
|
|
this._shouldPlay = false;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2024-01-14 14:18:32 +00:00
|
|
|
/**
|
|
|
|
* Called when the user clicks to focus on the window.
|
|
|
|
*/
|
|
|
|
override function onFocus():Void
|
|
|
|
{
|
2024-02-28 08:01:20 +00:00
|
|
|
if (!_alreadyPaused)
|
2024-01-14 14:18:32 +00:00
|
|
|
{
|
|
|
|
resume();
|
|
|
|
}
|
2024-02-28 08:01:20 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
trace('Not resuming audio on focus!');
|
|
|
|
}
|
2024-01-14 14:18:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the user tabs away from the window.
|
|
|
|
*/
|
|
|
|
override function onFocusLost():Void
|
|
|
|
{
|
2024-02-28 08:01:20 +00:00
|
|
|
trace('Focus lost, pausing audio!');
|
2024-01-14 14:18:32 +00:00
|
|
|
_alreadyPaused = _paused;
|
|
|
|
pause();
|
|
|
|
}
|
|
|
|
|
2023-12-07 03:03:36 +00:00
|
|
|
public override function resume():FunkinSound
|
|
|
|
{
|
|
|
|
if (this._time < 0)
|
2023-12-06 20:04:24 +00:00
|
|
|
{
|
2023-12-07 03:03:36 +00:00
|
|
|
this._shouldPlay = true;
|
2023-12-06 20:04:24 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-12-07 03:03:36 +00:00
|
|
|
super.resume();
|
2023-12-06 20:04:24 +00:00
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2024-01-31 02:49:49 +00:00
|
|
|
/**
|
|
|
|
* Call after adjusting the volume to update the sound channel's settings.
|
|
|
|
*/
|
|
|
|
@:allow(flixel.sound.FlxSoundGroup)
|
|
|
|
override function updateTransform():Void
|
|
|
|
{
|
|
|
|
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
|
|
|
|
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
|
|
|
|
|
|
|
if (_channel != null) _channel.soundTransform = _transform;
|
|
|
|
}
|
|
|
|
|
2024-01-27 08:24:49 +00:00
|
|
|
public function clone():FunkinSound
|
|
|
|
{
|
|
|
|
var sound:FunkinSound = new FunkinSound();
|
|
|
|
|
|
|
|
// Clone the sound by creating one with the same data buffer.
|
|
|
|
// Reusing the `Sound` object directly causes issues with playback.
|
|
|
|
@:privateAccess
|
|
|
|
sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer);
|
|
|
|
|
|
|
|
// Call init to ensure the FlxSound is properly initialized.
|
|
|
|
sound.init(this.looped, this.autoDestroy, this.onComplete);
|
|
|
|
|
2024-02-09 19:58:57 +00:00
|
|
|
// Oh yeah, the waveform data is the same too!
|
|
|
|
@:privateAccess
|
|
|
|
sound._waveformData = this._waveformData;
|
|
|
|
|
2024-01-27 08:24:49 +00:00
|
|
|
return sound;
|
|
|
|
}
|
|
|
|
|
2023-12-06 20:04:24 +00:00
|
|
|
/**
|
|
|
|
* Creates a new `FunkinSound` object.
|
|
|
|
*
|
|
|
|
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
|
|
|
|
* @param volume How loud to play it (0 to 1).
|
|
|
|
* @param looped Whether to loop this sound.
|
|
|
|
* @param group The group to add this sound to.
|
|
|
|
* @param autoDestroy Whether to destroy this sound when it finishes playing.
|
|
|
|
* Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
|
|
|
|
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
|
|
|
|
* @param onComplete Called when the sound finished playing.
|
|
|
|
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
|
|
|
|
* @return A `FunkinSound` object.
|
|
|
|
*/
|
|
|
|
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
|
|
|
|
?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
|
|
|
|
{
|
|
|
|
var sound:FunkinSound = cache.recycle(construct);
|
|
|
|
|
2024-02-28 05:19:08 +00:00
|
|
|
// Load the sound.
|
|
|
|
// Sets `exists = true` as a side effect.
|
2023-12-06 20:04:24 +00:00
|
|
|
sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete);
|
|
|
|
|
2023-12-07 03:03:36 +00:00
|
|
|
if (embeddedSound is String)
|
|
|
|
{
|
|
|
|
sound._label = embeddedSound;
|
|
|
|
}
|
|
|
|
|
2023-12-06 20:04:24 +00:00
|
|
|
sound.volume = volume;
|
|
|
|
sound.group = FlxG.sound.defaultSoundGroup;
|
2023-12-07 03:03:36 +00:00
|
|
|
sound.persist = true;
|
2023-12-06 20:04:24 +00:00
|
|
|
if (autoPlay) sound.play();
|
|
|
|
|
|
|
|
// Call OnlLoad() because the sound already loaded
|
|
|
|
if (onLoad != null && sound._sound != null) onLoad();
|
|
|
|
|
|
|
|
return sound;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function construct():FunkinSound
|
|
|
|
{
|
|
|
|
var sound:FunkinSound = new FunkinSound();
|
|
|
|
|
|
|
|
cache.add(sound);
|
|
|
|
FlxG.sound.list.add(sound);
|
|
|
|
|
|
|
|
return sound;
|
|
|
|
}
|
|
|
|
}
|