diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 9012cec12ce..107c8114f30 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -112,9 +112,6 @@ class Conductor */ var songPositionDelta(default, null):Float = 0; - var prevTimestamp:Float = 0; - var prevTime:Float = 0; - /** * Beats per minute of the current song at the current time. */ @@ -421,16 +418,7 @@ class Conductor */ public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false):Void { - var currentTime:Float = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0; - var currentLength:Float = (FlxG.sound.music != null) ? FlxG.sound.music.length : 0.0; - - if (songPos == null) - { - songPos = currentTime; - } - - // Take into account instrumental and file format song offsets. - songPos += applyOffsets ? (combinedOffset) : 0; + if (songPos == null) songPos = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0; var oldMeasure:Float = this.currentMeasure; var oldBeat:Float = this.currentBeat; @@ -439,11 +427,16 @@ class Conductor // If the song is playing, limit the song position to the length of the song or beginning of the song. if (FlxG.sound.music != null && FlxG.sound.music.playing) { - this.songPosition = Math.min(this.combinedOffset, 0).clamp(songPos, currentLength); - this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch; + // Take into account instrumental and file format song offsets. + songPos += applyOffsets ? (combinedOffset * FlxG.sound.music.pitch) : 0; + + this.songPosition = Math.min(this.combinedOffset, 0).clamp(songPos, FlxG.sound.music.length); } else { + // Take into account instrumental and file format song offsets. + songPos += applyOffsets ? (combinedOffset) : 0; + this.songPosition = songPos; } @@ -502,17 +495,6 @@ class Conductor this.onMeasureHit.dispatch(); } - // only update the timestamp if songPosition actually changed - // which it doesn't do every frame! - if (prevTime != this.songPosition) - { - this.songPositionDelta = 0; - - // Update the timestamp for use in-between frames - prevTime = this.songPosition; - prevTimestamp = Std.int(Timer.stamp() * 1000); - } - if (this == Conductor.instance) @:privateAccess SongSequence.update.dispatch(); } @@ -522,7 +504,7 @@ class Conductor */ public function getTimeWithDelta():Float { - return this.songPosition + this.songPositionDelta; + return this.songPosition; } /** @@ -536,10 +518,10 @@ class Conductor public function getTimeWithDiff(?soundToCheck:FlxSound):Float { if (soundToCheck == null) soundToCheck = FlxG.sound.music; - - @:privateAccess - this.songPosition = soundToCheck._channel.position; - return this.songPosition; + return soundToCheck.getActualTime(); + //@:privateAccess + //this.songPosition = soundToCheck.getActualTime(); + //return this.songPosition; } /** diff --git a/source/funkin/FunkinMemory.hx b/source/funkin/FunkinMemory.hx index c164f72ca69..41da1c97f6f 100644 --- a/source/funkin/FunkinMemory.hx +++ b/source/funkin/FunkinMemory.hx @@ -21,10 +21,6 @@ class FunkinMemory static var currentCachedTextures:Map = []; static var previousCachedTextures:Map = []; - static var permanentCachedSounds:Map = []; - static var currentCachedSounds:Map = []; - static var previousCachedSounds:Map = []; - static var purgeFilter:Array = ["/week", "/characters", "/charSelect", "/results"]; /** @@ -102,8 +98,6 @@ class FunkinMemory { preparePurgeTextureCache(); purgeTextureCache(); - preparePurgeSoundCache(); - purgeSoundCache(); #if (cpp || neko || hl) if (callGarbageCollector) funkin.util.MemoryUtil.collect(true); #end @@ -331,21 +325,7 @@ class FunkinMemory */ public static function cacheSound(key:String):Void { - if (currentCachedSounds.exists(key)) return; - - if (previousCachedSounds.exists(key)) - { - // Move the texture from the previous cache to the current cache. - var sound:Null = previousCachedSounds.get(key); - previousCachedSounds.remove(key); - if (sound != null) currentCachedSounds.set(key, sound); - return; - } - - var sound:Null = Assets.getSound(key, true); - if (sound == null) return; - else - currentCachedSounds.set(key, sound); + flixel.sound.FlxSoundData.fromAssetKey(key); } /** @@ -354,64 +334,8 @@ class FunkinMemory */ public static function permanentCacheSound(key:String):Void { - if (permanentCachedSounds.exists(key)) return; - - var sound:Null = Assets.getSound(key, true); - if (sound == null) return; - else - permanentCachedSounds.set(key, sound); - - if (sound != null) currentCachedSounds.set(key, sound); - } - - /** - * Prepares the cache for purging unused sounds. - */ - public static function preparePurgeSoundCache():Void - { - previousCachedSounds = currentCachedSounds.copy(); - - for (key in previousCachedSounds.keys()) - { - if (permanentCachedSounds.exists(key)) - { - previousCachedSounds.remove(key); - } - } - - currentCachedSounds = permanentCachedSounds.copy(); - } - - /** - * Purges unused sounds from the cache. - */ - public static inline function purgeSoundCache():Void - { - for (key in previousCachedSounds.keys()) - { - if (permanentCachedSounds.exists(key)) - { - previousCachedSounds.remove(key); - continue; - } - - var sound:Null = previousCachedSounds.get(key); - if (sound != null) - { - Assets.cache.removeSound(key); - previousCachedSounds.remove(key); - } - } - Assets.cache.clear("songs"); - Assets.cache.clear("music"); - // Felt lazy. - var key = Paths.music("freakyMenu/freakyMenu"); - var sound:Null = Assets.getSound(key, true); - if (sound != null) - { - permanentCachedSounds.set(key, sound); - currentCachedSounds.set(key, sound); - } + var soundData = flixel.sound.FlxSoundData.fromAssetKey(key); + if (soundData != null) soundData.persist = true; } ///// MISC ///// @@ -445,9 +369,6 @@ class FunkinMemory if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key); Assets.cache.clear(key); } - - preparePurgeSoundCache(); - purgeSoundCache(); } /** diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index cddc1ea0993..2571ee271ca 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -41,13 +41,23 @@ class Paths implements ConsoleClass return parts[0]; } - static function getPath(file:String, type:AssetType, library:Null):String + public static function fixPathExtension(path:String, defaultExtension:String):String + { + return if (path.lastIndexOf(".") == -1) '${path}.$defaultExtension'; else path; + } + + public static function normalizePath(path:String, ?defaultExtension:String):String + { + return if (defaultExtension == null) Path.normalize(path); else fixPathExtension(Path.normalize(path), defaultExtension); + } + + public static function getPath(file:String, type:AssetType, ?library:String):String { if (library != null) return getLibraryPath(file, library); if (currentLevel != null) { - var levelPath:String = getLibraryPathForce(file, currentLevel); + var levelPath:String = getLibraryPath(file, currentLevel); if (Assets.exists(levelPath, type)) return levelPath; } @@ -84,47 +94,74 @@ class Paths implements ConsoleClass public static function txt(key:String, ?library:String):String { - return getPath('data/$key.txt', TEXT, library); + return getPath(normalizePath('data/$key', 'txt'), TEXT, library); } public static function frag(key:String, ?library:String):String { - return getPath('shaders/$key.frag', TEXT, library); + return getPath(normalizePath('shaders/$key', 'frag'), TEXT, library); } public static function vert(key:String, ?library:String):String { - return getPath('shaders/$key.vert', TEXT, library); + return getPath(normalizePath('shaders/$key', 'vert'), TEXT, library); } public static function xml(key:String, ?library:String):String { - return getPath('data/$key.xml', TEXT, library); + return getPath(normalizePath('data/$key', 'xml'), TEXT, library); } public static function json(key:String, ?library:String):String { - return getPath('data/$key.json', TEXT, library); + return getPath(normalizePath('data/$key', 'json'), TEXT, library); } - public static function srt(key:String, ?library:String, ?directory:String = "data/"):String + public static function srt(key:String, ?library:String, ?directory:String = "data"):String { - return getPath('$directory$key.srt', TEXT, library); + return getPath(normalizePath('${directory}/$key', 'srt'), TEXT, library); } - public static function sound(key:String, ?library:String):String + public static function sound(key:String, ?library:String, ?directory:String = 'sounds', ?extension:String):String { - return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library); + var normalizedPath = Path.normalize((directory == '' ? '' : directory + '/') + key); + if (extension != null) return getPath(fixPathExtension(normalizedPath, extension), SOUND, library); + + // Attempt to find the sound by looping through the supported file formats. + var path:String; + for (extension in Constants.EXT_SOUNDS) + { + // no need to check if its exists in MUSIC type, as Openfl/Lime AssetLibrary have the same returns for SOUND and MUSIC internally. + if (library != null) + { + path = getLibraryPath(fixPathExtension(normalizedPath, extension), library); + if (Assets.exists(path, SOUND)/* || Assets.exists(path, MUSIC)*/) return path; + } + else + { + if (currentLevel != null) + { + path = getLibraryPath(fixPathExtension(normalizedPath, extension), currentLevel); + if (Assets.exists(path, SOUND)/* || Assets.exists(path, MUSIC)*/) return path; + } + + path = getLibraryPathForce(fixPathExtension(normalizedPath, extension), 'shared'); + if (Assets.exists(path, SOUND)/* || Assets.exists(path, MUSIC)*/) return path; + } + } + + if (library != null) return getLibraryPath(fixPathExtension(normalizedPath, Constants.EXT_SOUND), library); + else return getPreloadPath(fixPathExtension(normalizedPath, Constants.EXT_SOUND)); } - public static function soundRandom(key:String, min:Int, max:Int, ?library:String):String + public static function soundRandom(key:String, min:Int, max:Int, ?library:String, ?extension:String):String { - return sound(key + FlxG.random.int(min, max), library); + return sound(key + FlxG.random.int(min, max), library, null, extension); } - public static function music(key:String, ?library:String):String + public static function music(key:String, ?library:String, ?extension:String):String { - return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library); + return sound(key, library, 'music', extension); } public static function videos(key:String, ?library:String):String @@ -139,24 +176,29 @@ class Paths implements ConsoleClass return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos'); } - public static function voices(song:String, ?suffix:String = ''):String + public static function song(key:String, ?extension:String):String { - if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files + // For web platform that haven't loaded the library "songs" yet. + if (Assets.getLibrary("songs") != null) return sound(key, 'songs', '', extension); + else return getLibraryPathForce(normalizePath(key, extension ?? Constants.EXT_SOUND), 'songs'); + } - return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}'; + public static function voices(song:String, ?suffix:String = '', ?extension:String):String + { + if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files + return Paths.song('${song.toLowerCase()}/Voices$suffix', extension); } /** * Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/ * @param song name of the song to get instrumental for * @param suffix any suffix to add to end of song name, used for `-erect` variants usually - * @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`. + * @param extension The audio file extension of the track. If empty, it'll attempt to find a supported audio file format. * @return String */ - public static function inst(song:String, ?suffix:String = '', withExtension:Bool = true):String + public static function inst(song:String, ?suffix:String = '', ?extension:String):String { - var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : ''; - return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext'; + return Paths.song('${song.toLowerCase()}/Inst$suffix', extension); } public static function image(key:String, ?library:String):String diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index c9941abbaf9..0db5e9a9f2c 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -460,6 +460,55 @@ class Preferences return value; } + #if desktop + /** + * What audio device should it playback sounds to. + * @default Default + */ + public static var audioDevice(get, set):String; + + static function get_audioDevice():String + { + return Save?.instance?.options?.audioDevice ?? "Default"; + } + + static function set_audioDevice(value:String):String + { + var save:Save = Save.instance; + if (value == save.options.audioDevice) return value; + + FlxG.sound.automaticDefaultDevice = value == "Default" || (FlxG.sound.deviceName = value) != value; + + save.options.audioDevice = FlxG.sound.automaticDefaultDevice ? "Default" : value; + Save.system.flush(); + + return value; + } + #end + + #if native + /** + * Should the musics be loaded as streamable instead of static. + * @default `true` + */ + public static var streamedMusic(get, set):Bool; + + static function get_streamedMusic():Bool + { + return Save?.instance?.options?.streamedMusic ?? true; + } + + static function set_streamedMusic(value:Bool):Bool + { + flixel.sound.FlxSoundData.allowStreaming = value; + + var save:Save = Save.instance; + save.options.streamedMusic = value; + Save.system.flush(); + return value; + } + #end + /** * If enabled, the game will hide the mouse when taking a screenshot. * @default `true` @@ -529,6 +578,24 @@ class Preferences setDebugDisplayMode(Preferences.debugDisplay); setDebugDisplayBGOpacity(Preferences.debugDisplayBGOpacity / 100); + #if desktop + // Apply audio device preference, if failed, fallback to Default. + FlxG.sound.deviceName = Preferences.audioDevice; + if (FlxG.sound.deviceName == Preferences.audioDevice) + { + FlxG.sound.automaticDefaultDevice = false; + } + else + { + Preferences.audioDevice = "Default"; + FlxG.sound.automaticDefaultDevice = true; + } + #end + + #if native + flixel.sound.FlxSoundData.allowStreaming = Preferences.streamedMusic; + #end + toggleFramerateCap(Preferences.unlockedFramerate); #if mobile diff --git a/source/funkin/audio/FlxStreamSound.hx b/source/funkin/audio/FlxStreamSound.hx index d5f9547b7a9..19a1093b489 100644 --- a/source/funkin/audio/FlxStreamSound.hx +++ b/source/funkin/audio/FlxStreamSound.hx @@ -1,14 +1,15 @@ package funkin.audio; -import flash.media.Sound; -import flixel.sound.FlxSound; -import flixel.system.FlxAssets.FlxSoundAsset; +import lime.media.AudioBuffer; import openfl.Assets; -#if (openfl >= "8.0.0") +import openfl.media.Sound; import openfl.utils.AssetType; -#end +import flixel.sound.FlxSound; +import flixel.system.FlxAssets.FlxSoundAsset; /** + * USE flixel.sound.FlxSoundData or FlxSound.loadStreamed instead! + * * a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance! */ @:nullSafety @@ -19,29 +20,16 @@ class FlxStreamSound extends FlxSound super(); } - override public function loadEmbedded(EmbeddedSound:Null, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound + override public function loadEmbedded(asset:FlxSoundAsset, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy = false, ?onComplete:Void->Void):FlxStreamSound { - if (EmbeddedSound == null) return this; - - cleanup(true); - - if ((EmbeddedSound is Sound)) - { - _sound = EmbeddedSound; - } - else if ((EmbeddedSound is Class)) + if ((asset is String)) { - _sound = Type.createInstance(EmbeddedSound, []); + super.loadStreamed(asset, looped, loopTime, endTime, autoDestroy, onComplete); } - else if ((EmbeddedSound is String)) + else { - if (Assets.exists(EmbeddedSound, AssetType.SOUND) - || Assets.exists(EmbeddedSound, AssetType.MUSIC)) _sound = Assets.getMusic(EmbeddedSound); - else - FlxG.log.error('Could not find a Sound asset with an ID of \'$EmbeddedSound\'.'); + super.loadEmbedded(asset, looped, loopTime, endTime, autoDestroy, onComplete); } - - // NOTE: can't pull ID3 info from embedded sound currently - return init(Looped, AutoDestroy, OnComplete); + return this; } } diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index f2d62ea1a16..0116e459d1d 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -28,8 +28,6 @@ import openfl.media.SoundMixer; @:nullSafety class FunkinSound extends FlxSound implements ICloneable { - static final MAX_VOLUME:Float = 1.0; - /** * An FlxSignal which is dispatched when the volume changes. */ @@ -55,70 +53,33 @@ class FunkinSound extends FlxSound implements ICloneable */ static var pool(default, null):FlxTypedGroup = new FlxTypedGroup(); - /** - * Calculate the current time of the sound. - * NOTE: You need to `add()` the sound to the scene for `update()` to increment the time. - */ - // - 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 = value.clamp(0.0, MAX_VOLUME); - updateTransform(); - return _volume; - } - - public var paused(get, never):Bool; - - function get_paused():Bool - { - return this._paused; - } - - public var isPlaying(get, never):Bool; - - function get_isPlaying():Bool - { - return this.playing || this._shouldPlay; - } - /** * 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; + public var waveformData(get, never):Null; var _waveformData:Null = null; - function get_waveformData():WaveformData + function get_waveformData():Null { if (_waveformData == null) { _waveformData = WaveformDataParser.interpretFlxSound(this); - if (_waveformData == null) throw 'Could not interpret waveform data!'; + if (_waveformData == null) trace('Could not interpret waveform data!'); } return _waveformData; } /** - * If true, the game will forcefully add this sound's channel to the list of playing sounds. + * Are we in a state where the song should play but time is negative? */ - public var important:Bool = false; + var _shouldPlay:Bool = false; /** - * Are we in a state where the song should play but time is negative? + * The used variable for time when _shoudPlay is true. */ - var _shouldPlay:Bool = false; + var _time:Float = 0; /** * For debug purposes. @@ -130,37 +91,40 @@ class FunkinSound extends FlxSound implements ICloneable super(); } - public override function update(elapsedSec:Float) + public override function update(elapsedSec:Float):Void { - if (!playing && !_shouldPlay) return; - - if (_time < 0) + if (this._shouldPlay && !this._paused) { - var elapsedMs = elapsedSec * Constants.MS_PER_SEC; - _time += elapsedMs; - if (_time >= 0) + this._time += elapsedSec * Constants.MS_PER_SEC; + if (this._time >= -this.offset) { - super.play(); - _shouldPlay = false; + this._shouldPlay = false; + super.play(true, -offset); } } + + super.update(elapsedSec); + } + + public override function loadEmbedded(asset:FlxSoundAsset, ?looped:Bool, ?loopTime:Float, ?endTime:Float, autoDestroy = false, ?onComplete:Void->Void):FunkinSound + { + if (asset is String) + { + this._label = asset; + } else { - super.update(elapsedSec); - - @:privateAccess - { - if (important && _channel != null && !SoundMixer.__soundChannels.contains(_channel)) - { - SoundMixer.__soundChannels.push(_channel); - } - } + this._label = 'unknown'; } + + super.loadEmbedded(asset, looped, loopTime, endTime, autoDestroy, onComplete); + + return this; } public function togglePlayback():FunkinSound { - if (playing) + if (this.playing) { pause(); } @@ -171,105 +135,145 @@ class FunkinSound extends FlxSound implements ICloneable return this; } - public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound + public override function play(forceRestart = false, startTime = 0.0, ?endTime:Float, ?volume:Float, ?pitch:Float, ?pan:Float):FunkinSound { - if (!exists) return this; + if (!this.loaded) return this; - if (forceRestart) + if (forceRestart || !this.playing) { - cleanup(false, true); - } - else if (playing) - { - return this; - } - - if (startTime < 0) - { - this.active = true; - this._shouldPlay = true; - this._time = startTime; - this.endTime = endTime; - return this; - } - else - { - if (_paused) + if (this._paused && !forceRestart) { - resume(); + if (this._shouldPlay) + { + startTime = this.time; + } } - else + else if (!_paused) { - startSound(startTime); + loopCount = 0; } - this.endTime = endTime; - return this; + if (startTime < -this.offset) + { + if (volume != null) this._volume = volume; + #if FLX_PITCH + if (pitch != null) this._pitch = pitch; + #end + if (pan != null) this._pan = pan; + if (endTime != null) this._endTime = endTime; + + this.active = true; + this._shouldPlay = true; + this._time = startTime; + + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + _updateLoop(); + + source.prepare(0); + + return this; + } } + + this._shouldPlay = false; + super.play(forceRestart, startTime, endTime, volume, pitch, pan); + return this; } - public override function pause():FunkinSound + public override function prepare(startTime = 0.0, ?endTime:Float, ?volume:Float, ?pitch:Float, ?pan:Float):FunkinSound { - if (_shouldPlay) + if (!this.loaded) return this; + + if (startTime < -this.offset) { - // This sound will eventually play, but is still at a negative timestamp. - // Manually set the paused flag to ensure proper focus/unfocus behavior. - _shouldPlay = false; - _paused = true; - active = false; + if (volume != null) this._volume = volume; + #if FLX_PITCH + if (pitch != null) this._pitch = pitch; + #end + if (pan != null) this._pan = pan; + if (endTime != null) this._endTime = endTime; + + this.active = false; + this._shouldPlay = true; + this._time = startTime; + + _updateVolume(); + #if FLX_PITCH + _updatePitch(); + #end + _updatePan(); + _updateLoop(); + + source.prepare(0); } else { - super.pause(); + this._shouldPlay = false; + super.prepare(startTime); } + return this; } public override function resume():FunkinSound { - if (this._time < 0) + if (this._shouldPlay) { - // Sound with negative timestamp, restart the timer. - _shouldPlay = true; - _paused = false; - active = true; + // Sound with negative timestamp, resume the timer. + this._paused = false; } else { super.resume(); } + return this; } - /** - * Call after adjusting the volume to update the sound channel's settings. - */ - @:allow(flixel.sound.FlxSoundGroup) - override function updateTransform():Void + public override function pause():FunkinSound { - if (_transform != null) + if (this._shouldPlay) { - _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end - (group != null ? group.volume : 1) * _volume * _volumeAdjust; + // This sound will eventually play, but is still at a negative timestamp. + // Manually set the paused flag to ensure proper focus/unfocus behavior. + this._paused = true; + } + else + { + super.pause(); } - if (_channel != null) + return this; + } + + public override function stop():FunkinSound + { + if (this._shouldPlay) { - _channel.soundTransform = _transform; + // Stops the timer to play for negative timer sounds. + this._shouldPlay = false; + this._paused = true; + this.active = false; } + else + { + super.stop(); + } + + return this; } 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); + @:privateAccess + sound.init(this.data, this.looped, this.loopTime, this.endTime, this.autoDestroy, this.onComplete); // Oh yeah, the waveform data is the same too! @:privateAccess @@ -306,8 +310,7 @@ class FunkinSound extends FlxSound implements ICloneable if (FlxG.sound.music != null) { FlxG.sound.music.fadeTween?.cancel(); - FlxG.sound.music.stop(); - FlxG.sound.music.kill(); + FlxG.sound.music.destroy(); } if (params?.mapTimeChanges ?? true) @@ -329,7 +332,6 @@ class FunkinSound extends FlxSound implements ICloneable var suffix = params.suffix ?? ''; var pathToUse = switch (pathsFunction) { - case MUSIC: Paths.music('$key/$key'); case INST: Paths.inst('$key', suffix); default: Paths.music('$key/$key'); } @@ -343,28 +345,20 @@ class FunkinSound extends FlxSound implements ICloneable if (shouldLoadPartial) { - var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0, + var promise = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0, params.loop ?? true, false, false, params.onComplete); - if (music != null) - { - partialQueue.push(music); + partialQueue.push(promise); - @:nullSafety(Off) - music.future.onComplete(function(partialMusic:Null) - { - FlxG.sound.music = partialMusic; - FlxG.sound.list.remove(FlxG.sound.music); + @:nullSafety(Off) + promise.future.onComplete(function(partialMusic:Null) + { + setMusic(partialMusic); - if (FlxG.sound.music != null && params.onLoad != null) params.onLoad(); - }); + if (partialMusic != null && params.onLoad != null) params.onLoad(); + }); - return true; - } - else - { - return false; - } + return true; } else { @@ -373,7 +367,7 @@ class FunkinSound extends FlxSound implements ICloneable { setMusic(music); - if (FlxG.sound.music != null && params.onLoad != null) params.onLoad(); + if (params.onLoad != null) params.onLoad(); return true; } @@ -420,39 +414,22 @@ class FunkinSound extends FlxSound implements ICloneable * @param persist Whether to keep this `FunkinSound` between states, or destroy it. * @param onComplete Called when the sound finished playing. * @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds. - * @param important If `true`, the sound channel will forcefully be added onto the channel array, even if full. Use sparingly! * @return A `FunkinSound` object, or `null` if the sound could not be loaded. */ - public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false, - persist:Bool = false, ?onComplete:Void->Void, ?onLoad:Void->Void, important:Bool = false):Null + public static function load(embeddedSound:Null, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false, + persist:Bool = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound { - @:privateAccess - if (SoundMixer.__soundChannels.length >= SoundMixer.MAX_ACTIVE_CHANNELS && !important) - { - FlxG.log.error('FunkinSound could not play sound, channels exhausted! Found ${SoundMixer.__soundChannels.length} active sound channels.'); - return null; - } - var sound:FunkinSound = pool.recycle(construct); + // Force it to be exists so it doesn't get claimed by FlxG.sound.load or pool. + sound.alive = true; + sound.exists = true; + // Load the sound. // Sets `exists = true` as a side effect. + @:nullSafety(Off) sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); - - if (embeddedSound is String) - { - sound._label = embeddedSound; - } - else - { - sound._label = 'unknown'; - } - - if (autoPlay) sound.play(); - sound.volume = volume; FlxG.sound.defaultSoundGroup.add(sound); - sound.persist = persist; - sound.important = important; // Make sure to add the sound to the list. // If it's already in, it won't get re-added. @@ -460,8 +437,12 @@ class FunkinSound extends FlxSound implements ICloneable // it will get re-added (then if this was called by playMusic(), removed again) FlxG.sound.list.add(sound); + sound.volume = volume; + sound.persist = persist; + if (autoPlay) sound.play(true, 0); + // Call onLoad() because the sound already loaded - if (onLoad != null && sound._sound != null) onLoad(); + if (onLoad != null && sound.data != null) onLoad(); return sound; } @@ -518,49 +499,44 @@ class FunkinSound extends FlxSound implements ICloneable { // trace('[FunkinSound] Destroying sound "${this._label}"'); super.destroy(); - if (fadeTween != null) - { - fadeTween.cancel(); - fadeTween = null; - } FlxTween.cancelTweensOf(this); this._label = 'unknown'; this._waveformData = null; } - @:access(openfl.media.Sound) - @:access(openfl.media.SoundChannel) - @:access(openfl.media.SoundMixer) - override function startSound(startTime:Float) + override function get_playing():Bool + { + return loaded && (source.playing || _pausedPlay || _shouldPlay); + } + + override function get_time():Float { - if (!important) + if (this._shouldPlay) { - super.startSound(startTime); - return; + return this._time; } + else + { + return super.get_time(); + } + } - _time = startTime; - _paused = false; - - if (_sound == null) return; - - // Create a channel manually if the sound is considered important. - var pan:Float = (SoundMixer.__soundTransform.pan + _transform.pan).clamp(-1, 1); - var volume:Float = (SoundMixer.__soundTransform.volume * _transform.volume).clamp(0, MAX_VOLUME); - - var audioSource:AudioSource = new AudioSource(_sound.__buffer); - audioSource.offset = Std.int(startTime); - audioSource.gain = volume; - - var position:lime.math.Vector4 = audioSource.position; - position.x = pan; - position.z = -1 * Math.sqrt(1 - Math.pow(pan, 2)); - audioSource.position = position; - - _channel = new SoundChannel(_sound, audioSource, _transform); - _channel.addEventListener(Event.SOUND_COMPLETE, stopped); - pitch = _pitch; - active = true; + override function set_time(value:Float):Float + { + if (this._shouldPlay) + { + this._time = value; + if (this.time >= -offset) + { + this._shouldPlay = false; + super.play(true, -offset); + } + return value; + } + else + { + return super.set_time(value); + } } /** @@ -569,9 +545,9 @@ class FunkinSound extends FlxSound implements ICloneable * @param volume * @return A `FunkinSound` object, or `null` if the sound could not be loaded. */ - public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void, important:Bool = false):Null + public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Null { - var result:Null = FunkinSound.load(key, volume, false, true, true, false, onComplete, onLoad, important); + var result:Null = FunkinSound.load(key, volume, false, true, true, false, onComplete, onLoad); return result; } diff --git a/source/funkin/audio/PreviewMusicData.hx b/source/funkin/audio/PreviewMusicData.hx new file mode 100644 index 00000000000..b0a0007f9f7 --- /dev/null +++ b/source/funkin/audio/PreviewMusicData.hx @@ -0,0 +1,389 @@ +package funkin.audio; + +import haxe.io.Bytes; +import lime._internal.format.Base64; +import lime.media.AudioBuffer; +import openfl.utils.Assets; +import flixel.sound.FlxSoundData; +import flixel.tweens.FlxEase; + +#if native +import sys.thread.Mutex; + +import haxe.Int64; +import lime.media.AudioDecoder; +import lime.utils.ArrayBuffer; +import lime.utils.ArrayBufferView; +import lime.utils.ArrayBufferView.ArrayBufferIO; +import lime.utils.Int8Array; +import lime.utils.Int16Array; +import lime.utils.Int32Array; +#elseif web +import js.html.audio.AudioBuffer as JSAudioBuffer; +import lime.utils.Float32Array; +import funkin.util.flixel.sound.FlxPartialSound; +#end + +class PreviewMusicData extends FlxSoundData +{ + /** + * For the audio preview, the duration of the fade-in effect. + */ + public static final FADE_IN_DURATION:Float = 2.0; + + /** + * For the audio preview, the duration of the fade-out effect. + */ + public static final FADE_OUT_DURATION:Float = 2.0; + + /** + * For the audio preview, what easing function to use for fading of the fade-in effect. + */ + public static final FADE_IN_EASE_FUNCTION:EaseFunction = FlxEase.quadOut; + + /** + * For the audio preview, what easing function to use for fading of the fade-out effect. + */ + public static final FADE_OUT_EASE_FUNCTION:EaseFunction = FlxEase.quadIn; + + #if native + var audioBufferProcess:AudioBuffer; + var audioDecoderProcess:PreviewMusicDecoder; + #end + + public function new() + { + super('PreviewMusicData', null, true); + + #if native + audioBufferProcess = new AudioBuffer(); + audioBufferProcess.decoder = audioDecoderProcess = new PreviewMusicDecoder(this); + #end + } + + public function setAssetPath(path:String, startPercentage:Float = 0, endPercentage:Float = 0.15, ?stream:Bool, ?onLoad:PreviewMusicData->Void):Void + { + var cacheName = 'PreviewMusicData-$path-$startPercentage-$endPercentage'; + + var cache = FlxG.sound.getCache(cacheName); + if (cache != null) + { + buffer = cache.buffer; + if (onLoad != null) onLoad(this); + return; + } + + #if native + final openflAssetExists = Assets.exists(path); + var decoder = AudioDecoder.fromFile(openflAssetExists ? Assets.getPath(path) : path); + if (decoder == null) + { + if (!openflAssetExists) return; + + decoder = AudioDecoder.fromBytes(Assets.getBytes(path)); + if (decoder == null) return; + } + + final total = decoder.total(); + final duration = (total.high * 4294967296.0 + total.low) / decoder.sampleRate * 1000.0; + final startTime = startPercentage * duration; + final endTime = endPercentage * duration; + + audioDecoderProcess.resetDecoder(decoder, startTime, endTime); + + if (stream == null) stream = FlxSoundData.allowStreaming && (endTime - startTime) >= FlxSoundData.streamMinimumLength; + if (stream && decoder.seekable()) + { + audioBufferProcess.bitsPerSample = audioDecoderProcess.bitsPerSample; + audioBufferProcess.channels = audioDecoderProcess.channels; + audioBufferProcess.sampleRate = audioDecoderProcess.sampleRate; + buffer = audioBufferProcess; + + if (onLoad != null) onLoad(this); + } + else + { + buffer = new AudioBuffer(); + buffer.decoder = audioDecoderProcess; + buffer.loadAsync((_) -> { + buffer.decoder = null; + + // DEBUG TESTING THE AUDIO. + // var output = sys.io.File.write("../../../../tempPartial.wav", true); + // output.writeString("RIFF"); + // output.writeInt32(36 + buffer.data.byteLength); + // output.writeString("WAVE"); // 4 + // output.writeString("fmt "); // 4 + 4 + // output.writeInt32(16); // 8 + 4 + // output.writeUInt16(1); // 12 + 2 + // output.writeUInt16(buffer.channels); // 14 + 2 + // output.writeInt32(buffer.sampleRate); // 16 + 4 + // output.writeInt32(buffer.sampleRate * buffer.channels * (buffer.bitsPerSample >> 3)); // 20 + 4 + // output.writeUInt16(buffer.channels * (buffer.bitsPerSample >> 3)); // 24 + 2 + // output.writeUInt16(buffer.bitsPerSample); // 26 + 2 + // output.writeString("data"); // 28 + 4 + // output.writeInt32(buffer.data.byteLength); // 32 + 4 = 36 + // output.write(buffer.data.buffer); + // output.close(); + + if (onLoad != null) onLoad(this); + }); + + FlxSoundData.fromAudioBuffer(buffer, cacheName, true); + } + #elseif web + // apparently we have to split the path from the library? according to FunkinSound.loadPartial + var promise = FlxPartialSound.partialLoadFromFile(Paths.stripLibrary(path), startPercentage, endPercentage); + if (promise == null) return; + + @:privateAccess + var soundData = FlxSoundData.createSoundData(null, cacheName, true); + soundData.persist = true; // make it persist, or partialLoadFromFile will fail in next clearCache cycle. + + promise.future.onComplete(function(partialSound) + @:privateAccess { + soundData.buffer = buffer = partialSound.__buffer; + if (buffer.__srcAudioBuffer != null) + { + processFading(buffer.__srcAudioBuffer); + onLoad(this); + } + else + { + buffer.loadAsync((_) -> { + if (buffer.__srcAudioBuffer != null) processFading(buffer.__srcAudioBuffer); + onLoad(this); + }); + } + }); + #end + } + + #if web + private function processFading(buffer:JSAudioBuffer):Void + { + inline function processChannel(arr:Float32Array) + { + var n = Std.int(FADE_IN_DURATION * buffer.sampleRate); + var step = 0; + for (i in 0...n) + { + arr[i] = arr[i] * FADE_IN_EASE_FUNCTION(step / n); + step++; + } + + step = n = Std.int(FADE_OUT_DURATION * buffer.sampleRate); + for (i in (arr.length - n)...arr.length) + { + arr[i] = arr[i] * FADE_OUT_EASE_FUNCTION(step / n); + step--; + } + } + + processChannel(buffer.getChannelData(0)); + processChannel(buffer.getChannelData(1)); + } + #end + + override function destroy():Void + { + #if native + if (audioBufferProcess != null) + { + audioBufferProcess.dispose(); + audioBufferProcess = null; + } + #end + buffer = null; + } +} + +#if native +@:access(lime.utils.ArrayBufferView) +private class PreviewMusicDecoder extends AudioDecoder +{ + public var parent:PreviewMusicData; + public var decoder(default, null):AudioDecoder; + + var bytePerSample:Int; + var startSamples:Int64; + var endSamples:Int64; + var totalSamples:Int64; + var fadeStartSamples:Int64; + var fadeEndSamples:Int64; + var fadeEndStartSampleOffset:Int64; + var fadeEndEndSampleOffset:Int64; + var bufferView:ArrayBufferView; + var bufferInt8View:Int8Array; + var bufferInt16View:Int16Array; + var bufferInt32View:Int32Array; + var mutex:Mutex; + + public function new(parent:PreviewMusicData) + { + super(); + this.parent = parent; + mutex = new Mutex(); + } + + public function resetDecoder(decoder:AudioDecoder, startTime:Float, endTime:Float):Void + { + mutex.acquire(); + + var oldDecoder = this.decoder; + if (oldDecoder != null) oldDecoder.dispose(); + + this.decoder = decoder; + if (decoder != null) + { + bitsPerSample = decoder.bitsPerSample; + channels = decoder.channels; + sampleRate = decoder.sampleRate; + + startSamples = Int64.fromFloat(startTime / 1000 * sampleRate); + endSamples = Int64.fromFloat(endTime / 1000 * sampleRate); + totalSamples = Int64.fromFloat((endTime - startTime) / 1000 * sampleRate); + + var realTotal = decoder.total(); + if (realTotal < totalSamples) totalSamples = realTotal; + + bytePerSample = bitsPerSample >> 3; + fadeStartSamples = Int64.fromFloat(PreviewMusicData.FADE_IN_DURATION * sampleRate); + fadeEndEndSampleOffset = endSamples - startSamples; + fadeEndSamples = Int64.fromFloat(PreviewMusicData.FADE_OUT_DURATION * sampleRate); + fadeEndStartSampleOffset = fadeEndEndSampleOffset - fadeEndSamples; + + // 24 bitsPerSample are incompatible, nor it should be in openal too. + if (bytePerSample == 1) bufferView = bufferInt8View = new Int8Array(0); + else if (bytePerSample == 2) bufferView = bufferInt16View = new Int16Array(0); + else if (bytePerSample == 4) bufferView = bufferInt32View = new Int32Array(0); + + rewind(); + } + + mutex.release(); + } + + override function clone():PreviewMusicDecoder + { + return null; + } + + override function dispose():Void + { + super.dispose(); + } + + override function decode(buffer:ArrayBuffer, pos:Int, len:Int):Int + { + if (decoder == null) return 0; + + mutex.acquire(); + + var currentSampleOffset = decoder.tell() - startSamples; + var remainingBytes = (endSamples - currentSampleOffset) * channels * bytePerSample; + + var result:Int; + if (remainingBytes < len) + { + result = decoder.decode(buffer, pos, Int64.toInt(remainingBytes)); + eof = true; + } + else + { + result = decoder.decode(buffer, pos, len); + eof = decoder.eof; + } + + if (bytePerSample == 3) + { + mutex.release(); + return result; + } + + // Fading processing + if (bufferView.buffer != buffer) bufferView.initBuffer(buffer); + + pos = Std.int(pos / bytePerSample); + len = pos + Std.int(result / bytePerSample); + var channel = 0, b:Int; + while (pos < len) + { + if (currentSampleOffset < fadeStartSamples) + { + switch (bytePerSample) + { + case 1: + bufferInt8View[pos] = Std.int(bufferInt8View[pos] * + PreviewMusicData.FADE_IN_EASE_FUNCTION(currentSampleOffset.low / fadeStartSamples.low).clamp(0, 1)); + case 2: + bufferInt16View[pos] = Std.int(bufferInt16View[pos] * + PreviewMusicData.FADE_IN_EASE_FUNCTION(currentSampleOffset.low / fadeStartSamples.low).clamp(0, 1)); + case 4: + bufferInt32View[pos] = Std.int(bufferInt32View[pos] * + PreviewMusicData.FADE_IN_EASE_FUNCTION(currentSampleOffset.low / fadeStartSamples.low).clamp(0, 1)); + default: + } + } + else if (currentSampleOffset > fadeEndStartSampleOffset) + { + switch (bytePerSample) + { + case 1: + bufferInt8View[pos] = Std.int(bufferInt8View[pos] * + PreviewMusicData.FADE_OUT_EASE_FUNCTION((fadeEndEndSampleOffset - currentSampleOffset).low / fadeEndSamples.low).clamp(0, 1)); + case 2: + bufferInt16View[pos] = Std.int(bufferInt16View[pos] * + PreviewMusicData.FADE_OUT_EASE_FUNCTION((fadeEndEndSampleOffset - currentSampleOffset).low / fadeEndSamples.low).clamp(0, 1)); + case 4: + bufferInt32View[pos] = Std.int(bufferInt32View[pos] * + PreviewMusicData.FADE_OUT_EASE_FUNCTION((fadeEndEndSampleOffset - currentSampleOffset).low / fadeEndSamples.low).clamp(0, 1)); + default: + } + } + + if (++channel == channels) + { + channel = 0; + currentSampleOffset++; + } + pos++; + } + + mutex.release(); + + return result; + } + + override function rewind():Bool + { + var result = startSamples == 0 ? decoder.rewind() : decoder.seek(startSamples); + eof = decoder.eof; + + return result; + } + + override function seek(samples:Int64):Bool + { + var result = decoder.seek(samples + startSamples); + eof = decoder.eof; + + return result; + } + + override function seekable():Bool + { + return true; + } + + override function tell():Int64 + { + return decoder.tell() - startSamples; + } + + override function total():Int64 + { + return totalSamples; + } +} +#end diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 0c26a0dc87b..d8dd198a86c 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -1,12 +1,14 @@ package funkin.audio; import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.sound.FlxSound; import flixel.tweens.FlxTween; /** * A group of FunkinSounds that are all synced together. * Unlike FlxSoundGroup, you can also control their time and pitch. */ +@:access(funkin.audio.FunkinSound) @:nullSafety class SoundGroup extends FlxTypedGroup { @@ -60,10 +62,10 @@ class SoundGroup extends FlxTypedGroup forEachAlive(function(snd) { - if (targetTime == null) targetTime = snd.time; + if (targetTime == null) targetTime = snd.getActualTime(); else { - var diff:Float = snd.time - targetTime; + var diff:Float = snd.getActualTime() - targetTime; if (Math.abs(diff) > Math.abs(error)) error = diff; } }); @@ -107,10 +109,7 @@ class SoundGroup extends FlxTypedGroup */ public function pause() { - forEachAlive(function(sound:FunkinSound) - { - sound.pause(); - }); + if (members != null) FlxSound.pauseSounds(cast members); } /** @@ -118,15 +117,20 @@ class SoundGroup extends FlxTypedGroup */ public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) { + var sounds:Array = []; + forEachAlive(function(sound:FunkinSound) { - if (sound.length < startTime) + if (sound.playing && !forceRestart || sound.length < startTime) { - // trace('Queuing sound (${sound.toString()} past its length! Skipping...)'); return; } - sound.play(forceRestart, startTime, endTime); + + sound.prepare(startTime, endTime); + sounds.push(sound); }); + + FlxSound.playSounds(sounds); } /** @@ -134,10 +138,20 @@ class SoundGroup extends FlxTypedGroup */ public function resume() { + var sounds:Array = []; + forEachAlive(function(sound:FunkinSound) { - sound.resume(); + if (sound.playing || !sound._paused) + { + return; + } + + sound.prepare(sound.time); + sounds.push(sound); }); + + FlxSound.playSounds(sounds); } /** @@ -169,13 +183,7 @@ class SoundGroup extends FlxTypedGroup */ public function stop():Void { - if (members != null) - { - forEachAlive(function(sound:FunkinSound) - { - sound.stop(); - }); - } + if (members != null) FlxSound.stopSounds(cast members); } public override function destroy():Void @@ -194,6 +202,7 @@ class SoundGroup extends FlxTypedGroup super.clear(); } + @:nullSafety(Off) function get_time():Float { if (getFirstAlive() != null) @@ -208,15 +217,27 @@ class SoundGroup extends FlxTypedGroup function set_time(time:Float):Float { - forEachAlive(function(snd:FunkinSound) + // account for different offsets per sound? + + var sounds:Array = []; + + forEachAlive(function(sound:FunkinSound) { - // account for different offsets per sound? - snd.time = time; + if (!sound.loaded || sound.length < time || !sound.playing) + { + return; + } + + sound.prepare(time); + sounds.push(sound); }); + FlxSound.playSounds(sounds); + return time; } + @:nullSafety(Off) function get_playing():Bool { if (getFirstAlive() != null) @@ -229,6 +250,7 @@ class SoundGroup extends FlxTypedGroup } } + @:nullSafety(Off) function get_volume():Float { if (getFirstAlive() != null) @@ -252,11 +274,17 @@ class SoundGroup extends FlxTypedGroup return volume; } + @:nullSafety(Off) function get_muted():Bool { - if (getFirstAlive() != null) return getFirstAlive()?.muted ?? false; + if (getFirstAlive() != null) + { + return getFirstAlive().muted; + } else + { return false; + } } function set_muted(muted:Bool):Bool @@ -269,6 +297,7 @@ class SoundGroup extends FlxTypedGroup return muted; } + @:nullSafety(Off) function get_pitch():Float { #if FLX_PITCH diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index a45008f4b05..41c9d53bbcb 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -1,8 +1,10 @@ package funkin.audio; import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.sound.FlxSound; import funkin.audio.waveform.WaveformData; +@:access(funkin.audio.FunkinSound) @:nullSafety class VoicesGroup extends SoundGroup { @@ -61,23 +63,49 @@ class VoicesGroup extends SoundGroup return playerVolume = volume; } - override function set_time(time:Float):Float + override function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) { - forEachAlive(function(snd) - { - // account for different offsets per sound? - snd.time = time; - }); + var sounds:Array = []; - playerVoices?.forEachAlive(function(voice:FunkinSound) - { - voice.time -= playerVoicesOffset; + forEachAlive(function(sound:FunkinSound) { + var localTime = startTime; + + if (playerVoices?.members.contains(sound) ?? false) localTime -= playerVoicesOffset; + else if (opponentVoices?.members.contains(sound) ?? false) localTime -= opponentVoicesOffset; + + if (sound.playing && !forceRestart || sound.length < localTime) + { + return; + } + + sound.prepare(localTime, endTime); + sounds.push(sound); }); - opponentVoices?.forEachAlive(function(voice:FunkinSound) + + FlxSound.playSounds(sounds); + } + + override function set_time(time:Float):Float + { + // account for different offsets per sound? + + var sounds:Array = []; + + forEachAlive(function(sound:FunkinSound) { - voice.time -= opponentVoicesOffset; + var localTime = time; + + if (playerVoices?.members.contains(sound) ?? false) localTime -= playerVoicesOffset; + else if (opponentVoices?.members.contains(sound) ?? false) localTime -= opponentVoicesOffset; + + if (!sound.loaded || sound.length < localTime || !sound.playing) return; + + sound.prepare(localTime); + sounds.push(sound); }); + FlxSound.playSounds(sounds); + return time; } @@ -85,8 +113,7 @@ class VoicesGroup extends SoundGroup { playerVoices?.forEachAlive(function(voice:FunkinSound) { - voice.time += playerVoicesOffset; - voice.time -= offset; + voice.time += playerVoicesOffset - offset; }); return playerVoicesOffset = offset; } @@ -95,8 +122,7 @@ class VoicesGroup extends SoundGroup { opponentVoices?.forEachAlive(function(voice:FunkinSound) { - voice.time += opponentVoicesOffset; - voice.time -= offset; + voice.time += opponentVoicesOffset - offset; }); return opponentVoicesOffset = offset; } diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx index 7c5bda09100..d1c9bc150e5 100644 --- a/source/funkin/audio/visualize/ABotVis.hx +++ b/source/funkin/audio/visualize/ABotVis.hx @@ -65,7 +65,7 @@ class ABotVis extends FlxTypedSpriteGroup if (snd == null) return; @:privateAccess - analyzer = new SpectralAnalyzer(snd._channel.__audioSource, BAR_COUNT, 0.1, 40); + analyzer = new SpectralAnalyzer(snd.source, BAR_COUNT, 0.1, 40); // A-Bot tuning... analyzer.minDb = -65; analyzer.maxDb = -25; diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx index 7d960efddd2..1e7445deaad 100644 --- a/source/funkin/audio/visualize/VisShit.hx +++ b/source/funkin/audio/visualize/VisShit.hx @@ -115,8 +115,7 @@ class VisShit if (!setBuffer) { // Math.pow3 - @:privateAccess - var buf = snd._channel.__audioSource.buffer; + var buf = snd.data.buffer; // @:privateAccess audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!! diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 461a4796c5f..d63acb85b57 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -195,6 +195,8 @@ class WaveformData var thatChannel = that.channel(channelIndex); var resultChannel = result.channel(channelIndex); + if (thatChannel == null) return this.clone(); + for (index in 0...this.length) { var thisMinSample = thisChannel.minSample(index); diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx index 5a1343bd500..8b36f77b1ab 100644 --- a/source/funkin/audio/waveform/WaveformDataParser.hx +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -17,30 +17,8 @@ class WaveformDataParser { if (sound == null) return null; - // Method 1. This only works if the sound has been played before. - @:privateAccess - var soundBuffer:Null = sound?._channel?.__audioSource?.buffer; - - if (soundBuffer == null) - { - // Method 2. This works if the sound has not been played before. - @:privateAccess - soundBuffer = sound?._sound?.__buffer; - - if (soundBuffer == null) - { - trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}'); - return null; - } - else - { - // trace('[WAVEFORM] Method 2 worked.'); - } - } - else - { - // trace('[WAVEFORM] Method 1 worked.'); - } + var soundBuffer:Null = sound?.data.buffer; + if (soundBuffer == null) return null; return interpretAudioBuffer(soundBuffer); } diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx index 407a4d4dff5..8bd9a9b96d7 100644 --- a/source/funkin/data/song/importer/ChartManifestData.hx +++ b/source/funkin/data/song/importer/ChartManifestData.hx @@ -1,5 +1,7 @@ package funkin.data.song.importer; +import haxe.io.Path; + /** * A helper JSON blob found in `.fnfc` files. */ @@ -47,18 +49,32 @@ class ChartManifestData return '$songId-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}'; } - public function getInstFileName(?variation:String):String + public function getInstFileName(?variation:String, fileEntries:Array):String { if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; - return 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + var instFile = fileEntries.filter(function(file:haxe.zip.Entry):Bool + { + return Path.withoutExtension(file.fileName) == 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'; + }); + + if (instFile[0] == null) return 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + else + return instFile[0].fileName; } - public function getVocalsFileName(charId:String, ?variation:String):String + public function getVocalsFileName(charId:String, ?variation:String, fileEntries:Array):String { if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; - return 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + var vocalFile = fileEntries.filter(function(file:haxe.zip.Entry):Bool + { + return Path.withoutExtension(file.fileName) == 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'; + }); + + if (vocalFile[0] == null) return 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + else + return vocalFile[0].fileName; } /** diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 818f5e7968e..ed296db63de 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -256,7 +256,7 @@ class Countdown var path = noteStyle.getCountdownSoundPath(step); if (path == null) return null; - return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME, null, null, true); + return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME, null, null); } public static function decrement(step:CountdownStep):CountdownStep diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index f002281ffad..58a59280e83 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -144,6 +144,8 @@ class GameOverSubState extends MusicBeatSubState parentPlayState = cast _parentState; + gameOverMusic = FunkinSound.load(null); + // // Set up the visuals // @@ -524,10 +526,11 @@ class GameOverSubState extends MusicBeatSubState } else if (gameOverMusic == null || !gameOverMusic.playing || force) { - if (gameOverMusic != null) gameOverMusic.stop(); - - gameOverMusic = FunkinSound.load(musicPath); - if (gameOverMusic == null) return; + if (gameOverMusic == null) gameOverMusic = FunkinSound.load(musicPath); + { + gameOverMusic.stop(); + gameOverMusic.loadEmbedded(musicPath); + } gameOverMusic.volume = startingVolume; gameOverMusic.looped = !(isEnding || isStarting); diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 22d9f03d811..655f6648470 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -359,7 +359,7 @@ class PauseSubState extends MusicBeatSubState } // Start playing at a random point in the song. - pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2))); + pauseMusic.play(true, FlxG.random.float(0, pauseMusic.length / 2)); pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME); } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index d408b1ac92f..8553afd4de5 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1176,30 +1176,11 @@ class PlayState extends MusicBeatSubState Conductor.instance.formatOffset = 0.0; } - // Lime has some precision loss when getting the sound current position - // Since the notes scrolling is dependant on the sound time that caused it to appear "stuttery" for some people - // As a workaround for that, we lerp the conductor position to the music time to fill the gap in this lost precision making the scrolling smoother - // The previous method where it "guessed" the song position based on the elapsed time had some flaws - // Somtimes the songPosition would exceed the music length causing issues in other places - // And it was frame dependant which we don't like!! if (FlxG.sound.music.playing) { - final audioDiff:Float = Math.round(Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition - Conductor.instance.combinedOffset))); - if (audioDiff <= CONDUCTOR_DRIFT_THRESHOLD) - { - // Only do neat & smooth lerps as long as the lerp doesn't fuck up and go WAY behind the music time triggering false resyncs - final easeRatio:Float = 1.0 - Math.exp(-(MUSIC_EASE_RATIO * playbackRate) * elapsed); - Conductor.instance.update(FlxMath.lerp(Conductor.instance.songPosition, FlxG.sound.music.time + Conductor.instance.combinedOffset, easeRatio), false); - } - else - { - // Fallback to properly update the conductor incase the lerp messed up - // Shouldn't be fallen back to unless you're lagging alot - trace(' WARNING '.bg_yellow().bold() + ' Normal Conductor Update!! are you lagging?'); Conductor.instance.update(); } } - } var pauseButtonCheck:Bool = false; var androidPause:Bool = false; @@ -1561,6 +1542,7 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.pause(); musicPausedBySubState = true; } + vocals?.pause(); // Pause any sounds that are playing and keep track of them. // Vocals are also paused here but are not included as they are handled separately. @@ -1568,26 +1550,12 @@ class PlayState extends MusicBeatSubState { FlxG.sound.list.forEachAlive(function(sound:FlxSound) { - if (!sound.active || sound == FlxG.sound.music) return; - // In case it's a scheduled sound - if (Std.isOfType(sound, FunkinSound)) - { - var funkinSound:FunkinSound = cast sound; - if (funkinSound != null && !funkinSound.isPlaying) return; - } - if (!sound.playing && sound.time >= 0) return; - sound.pause(); + if (!sound.playing || !sound.active || sound == FlxG.sound.music) return; soundsPausedBySubState.add(sound); }); + vocals?.forEach(function(voice:FunkinSound) soundsPausedBySubState.remove(voice)); - vocals?.forEach(function(voice:FunkinSound) - { - soundsPausedBySubState.remove(voice); - }); - } - else - { - vocals?.pause(); + for (sound in soundsPausedBySubState) sound.pause(); } } @@ -1641,47 +1609,11 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; - // Pause any sounds that are playing and keep track of them. - // Vocals are also paused here but are not included as they are handled separately. - if (!isGameOverState) - { - FlxG.sound.list.forEachAlive(function(sound:FlxSound) - { - if (!sound.active || sound == FlxG.sound.music) return; - // In case it's a scheduled sound - if (Std.isOfType(sound, FunkinSound)) - { - var funkinSound:FunkinSound = cast sound; - if (funkinSound != null && !funkinSound.isPlaying) return; - } - if (!sound.playing && sound.time >= 0) return; - sound.pause(); - soundsPausedBySubState.add(sound); - }); - - vocals?.forEach(function(voice:FunkinSound) - { - soundsPausedBySubState.remove(voice); - }); - } - else - { - vocals?.pause(); - } - // Resume vwooshTimer if (!vwooshTimer.finished) vwooshTimer.active = true; - // Resume music if we paused it. - if (musicPausedBySubState) - { - if (FlxG.sound.music != null) FlxG.sound.music.play(); - musicPausedBySubState = false; - } - - // The logic here is that if this sound doesn't auto-destroy - // then it's gonna be reused somewhere, so we just stop it instead. - forEachPausedSound(s -> needsReset ? (s.autoDestroy ? s.destroy() : s.stop()) : s.resume()); + // Stopping a sound will kills itself anyway if s.autoDestroy is true. + forEachPausedSound(s -> needsReset ? s.stop() : s.resume()); // Resume camera tweens if we paused any. for (camTween in cameraTweensPausedBySubState) @@ -1701,8 +1633,10 @@ class PlayState extends MusicBeatSubState currentConversation.resumeMusic(); } - // Re-sync vocals. - if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); + // If we have started the song, resync it instead that plays the song, else resume music if we paused it. + if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(true); + else if (musicPausedBySubState) FlxG.sound.music?.play(); + musicPausedBySubState = false; // Resume the countdown. Countdown.resumeCountdown(); @@ -1893,7 +1827,7 @@ class PlayState extends MusicBeatSubState if (FlxG.sound.music != null) { - var correctSync:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset)); + var correctSync:Float = FlxG.sound.music.getActualTime(); var playerVoicesError:Float = 0; var opponentVoicesError:Float = 0; if (vocals != null && vocals.playing) @@ -1903,22 +1837,22 @@ class PlayState extends MusicBeatSubState { vocals.playerVoices?.forEachAlive(function(voice:FunkinSound) { - var currentRawVoiceTime:Float = voice.time + vocals.playerVoicesOffset; - if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(playerVoicesError)) playerVoicesError = currentRawVoiceTime - correctSync; + var currentRawVoiceTime:Float = voice.getActualTime() + vocals.playerVoicesOffset + Conductor.instance.instrumentalOffset; + if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(playerVoicesError) && voice.playing) playerVoicesError = currentRawVoiceTime - correctSync; }); vocals.opponentVoices?.forEachAlive(function(voice:FunkinSound) { - var currentRawVoiceTime:Float = voice.time + vocals.opponentVoicesOffset; - if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(opponentVoicesError)) opponentVoicesError = currentRawVoiceTime - correctSync; + var currentRawVoiceTime:Float = voice.getActualTime() + vocals.opponentVoicesOffset + Conductor.instance.instrumentalOffset; + if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(opponentVoicesError) && voice.playing) opponentVoicesError = currentRawVoiceTime - correctSync; }); } } if (!startingSong - && (Math.abs(FlxG.sound.music.time - correctSync) > RESYNC_THRESHOLD - || Math.abs(playerVoicesError) > RESYNC_THRESHOLD - || Math.abs(opponentVoicesError) > RESYNC_THRESHOLD)) + && (Math.abs(FlxG.sound.music.getActualTime() - correctSync) / FlxG.sound.music.pitch > RESYNC_THRESHOLD + || Math.abs(playerVoicesError) / FlxG.sound.music.pitch > RESYNC_THRESHOLD + || Math.abs(opponentVoicesError) / FlxG.sound.music.pitch > RESYNC_THRESHOLD)) { trace("VOCALS NEED RESYNC"); if (vocals != null) @@ -1926,7 +1860,6 @@ class PlayState extends MusicBeatSubState trace(playerVoicesError); trace(opponentVoicesError); } - trace(FlxG.sound.music.time); trace(correctSync); resyncVocals(); } @@ -2603,7 +2536,7 @@ class PlayState extends MusicBeatSubState if (!overrideMusic && !isGamePaused && currentChart != null) { - currentChart?.playInst(1.0, currentInstrumental, false); + currentChart?.buildInst(1.0, currentInstrumental, false); } if (FlxG.sound.music == null) @@ -2612,15 +2545,6 @@ class PlayState extends MusicBeatSubState return; } - FlxG.sound.music.onComplete = function() - { - if (mayPauseGame) endSong(skipEndingTransition); - }; - - FlxG.sound.music.pause(); - FlxG.sound.music.time = startTimestamp; - FlxG.sound.music.pitch = playbackRate; - if (Preferences.subtitles) { var subtitlesFile:String = 'songs/${currentSong.id}/subtitles/song-lyrics'; @@ -2631,27 +2555,26 @@ class PlayState extends MusicBeatSubState if (subtitles != null) subtitles.assignSubtitles(subtitlesFile, FlxG.sound.music); } - // Prevent the volume from being wrong. + FlxG.sound.music.onComplete = function() + { + if (mayPauseGame) endSong(skipEndingTransition); + }; + + FlxG.sound.music.pitch = playbackRate; FlxG.sound.music.volume = instrumentalVolume; - if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel(); + FlxG.sound.music.fadeTween?.cancel(); if (vocals != null) { add(vocals); - vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; vocals.pitch = playbackRate; vocals.playerVolume = playerVocalsVolume; vocals.opponentVolume = opponentVocalsVolume; - - // trace('STARTING SONG AT:'); - // trace('${FlxG.sound.music.time}'); - // trace('${vocals.time}'); - - vocals.play(); } - FlxG.sound.music.play(); + Conductor.instance.update(startTimestamp, true); + resyncVocals(true); #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence (with Time Left) @@ -2676,32 +2599,38 @@ class PlayState extends MusicBeatSubState #if FEATURE_NEWGROUNDS Events.logStartSong(currentSong.id, currentVariation); #end - - resyncVocals(); } /** * Resynchronize the vocal tracks if they have become offset from the instrumental. */ - function resyncVocals():Void + function resyncVocals(force = false):Void { - if (vocals == null) return; + if (FlxG.sound.music != null && force || (vocals != null && FlxG.sound.music.playing)) + { + var timeToPlayAt:Float = (Conductor.instance.songPosition - Conductor.instance.combinedOffset).clamp(0, FlxG.sound.music.length); + trace('Resyncing vocals to ${timeToPlayAt}'); - // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) - if (!(FlxG.sound.music?.playing ?? false)) return; + //FlxG.sound.music.play(true, timeToPlayAt); + //vocals?.play(true, timeToPlayAt - Conductor.instance.instrumentalOffset); - var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, - Math.max(Math.min(Conductor.instance.combinedOffset, 0), Conductor.instance.songPosition) - Conductor.instance.combinedOffset); - trace('Resyncing vocals to ${timeToPlayAt}'); + // Use FlxSound.prepare and FlxSound.playSounds so every sounds is played at the same time in hardware. - FlxG.sound.music.pause(); - vocals.pause(); + var sounds:Array = [FlxG.sound.music]; - FlxG.sound.music.time = timeToPlayAt; - FlxG.sound.music.play(false, timeToPlayAt); + FlxG.sound.music.prepare(timeToPlayAt); + if (vocals != null) + { + var vocalsTimeToPlayAt:Float = timeToPlayAt - Conductor.instance.instrumentalOffset; + vocals.forEachAlive(function(sound:FunkinSound) + { + sounds.push(sound); + sound.prepare(vocalsTimeToPlayAt); + }); + } - vocals.time = timeToPlayAt; - vocals.play(false, timeToPlayAt); + FlxSound.playSounds(sounds); + } } /** @@ -3420,8 +3349,9 @@ class PlayState extends MusicBeatSubState */ public function endSong(rightGoddamnNow:Bool = false):Void { - if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; - if (vocals != null) vocals.volume = 0; + FlxG.sound.music?.stop(); + vocals?.stop(); + mayPauseGame = false; isSongEnd = true; @@ -3849,7 +3779,6 @@ class PlayState extends MusicBeatSubState } persistentUpdate = false; - vocals?.stop(); camHUD.alpha = 1; var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies; @@ -4094,7 +4023,7 @@ class PlayState extends MusicBeatSubState handleSkippedNotes(); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); - if (FlxG.sound.music != null && FlxG.sound.music.playing && preventDeath) regenNoteData(FlxG.sound.music.time); + if (FlxG.sound.music != null && FlxG.sound.music.playing && preventDeath) regenNoteData(FlxG.sound.music.getActualTime()); Conductor.instance.update(FlxG.sound?.music?.time ?? 0.0); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index b3c731374fa..ad04a0bae93 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -520,7 +520,7 @@ class ResultState extends MusicBeatSubState // preload the loop music @:nullSafety(Off) - var musicLoop:FunkinSound = FunkinSound.load(mainMusic, 1.0, true, true, false, false, null, null, true); + var musicLoop:FunkinSound = FunkinSound.load(mainMusic, 1.0, true, true, false, false, null, null); // Play the intro music. introMusicAudio = FunkinSound.load(introMusic, 1.0, false, true, true, () -> diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 87791a89ca6..855b3481a11 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -869,13 +869,18 @@ class SongDifficulty } public function playInst(volume:Float = 1.0, instId:String = '', looped:Bool = false):Void + { + buildInst(volume, instId, looped).play(true, 0); + } + + public function buildInst(volume:Float = 1.0, instId:String = '', looped:Bool = false):FunkinSound { var suffix:String = (instId != '') ? '-$instId' : ''; - FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped, false, true, false, null, null, true); + var snd = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped, false, false, false, null, null); + FunkinSound.setMusic(snd); - // Workaround for a bug where FlxG.sound.music.update() was being called twice. - FlxG.sound.list.remove(FlxG.sound.music); + return snd; } /** @@ -1028,14 +1033,14 @@ class SongDifficulty for (playerVoice in playerVoiceList) { if (!Assets.exists(playerVoice)) continue; - result.addPlayerVoice(FunkinSound.load(playerVoice, 1.0, false, false, false, false, null, null, true)); + result.addPlayerVoice(FunkinSound.load(playerVoice, 1.0, false, false, false, false, null, null)); } // Add opponent vocals. for (opponentVoice in opponentVoiceList) { if (!Assets.exists(opponentVoice)) continue; - result.addOpponentVoice(FunkinSound.load(opponentVoice, 1.0, false, false, false, false, null, null, true)); + result.addOpponentVoice(FunkinSound.load(opponentVoice, 1.0, false, false, false, false, null, null)); } if (result.members.length == 0) @@ -1045,7 +1050,7 @@ class SongDifficulty var legacyPath = Paths.voices(this.song.id, '$suffix'); if (Assets.exists(legacyPath)) { - result.addPlayerVoice(FunkinSound.load(legacyPath, 1.0, false, false, false, false, null, null, true)); + result.addPlayerVoice(FunkinSound.load(legacyPath, 1.0, false, false, false, false, null, null)); } } @@ -1055,9 +1060,6 @@ class SongDifficulty result.legacyVoiceUsesPlayer = result.getPlayerVoice(0) != null; } - // Sometimes the sounds don't set their important value to true, so we have to do this manually. - result.forEach((snd:FunkinSound) -> snd.important = true); - result.playerVoicesOffset = offsets.getVocalOffset(characters.player, instId); result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent, instId); diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 52df067cb7c..da2cdee0ea2 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -116,6 +116,8 @@ class Save implements ConsoleClass globalOffset: 0, audioVisualOffset: 0, unlockedFramerate: false, + audioDevice: 'Default', + streamedMusic: true, screenshot: { shouldHideMouse: true, fancyPreview: true, @@ -1228,6 +1230,18 @@ typedef SaveDataOptions = */ var unlockedFramerate:Bool; + /** + * What audio device should it playback sounds to. + * @default 'Default' + */ + var audioDevice:String; + + /** + * Should the musics be loaded as streamable instead of static. + * @default 'true' + */ + var streamedMusic:Bool; + /** * Screenshot options * @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true` diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 466030acaa4..134da894474 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -466,7 +466,7 @@ class CharSelectSubState extends MusicBeatSubState allowInput = true; @:privateAccess - gfChill.analyzer = new SpectralAnalyzer(FlxG.sound.music._channel.__audioSource, 7, 0.1); + gfChill.analyzer = new SpectralAnalyzer(FlxG.sound.music.source, 7, 0.1); #if sys // On native it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5 // So we want to manually change it! @@ -623,7 +623,7 @@ class CharSelectSubState extends MusicBeatSubState allowInput = true; @:privateAccess - gfChill.analyzer = new SpectralAnalyzer(FlxG.sound.music._channel.__audioSource, 7, 0.1); + gfChill.analyzer = new SpectralAnalyzer(FlxG.sound.music.source, 7, 0.1); #if sys // On native it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5 // So we want to manually change it! diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index fb5817bce69..0110f9fca55 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -74,7 +74,7 @@ class WaveformTestState extends MusicBeatState if (FlxG.keys.justPressed.SPACE) { - if (waveformAudio.isPlaying) + if (waveformAudio.playing) { waveformAudio.stop(); } @@ -98,7 +98,7 @@ class WaveformTestState extends MusicBeatState // } } - if (waveformAudio.isPlaying) + if (waveformAudio.playing) { // waveformSprite takes a time in fractional seconds, not milliseconds. var timeSeconds = waveformAudio.time / 1000; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 31e9dd0eb99..429cfba6220 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -150,9 +150,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay'); public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); - // Validation - public static final SUPPORTED_MUSIC_FORMATS:Array = #if sys ['ogg'] #else ['mp3'] #end; - // Layout /** @@ -2676,9 +2673,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return; } - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) return; + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) return; - if (welcomeMusic.isPlaying) return; + if (welcomeMusic.playing) return; if (!welcomeMusic.exists) setupWelcomeMusic(); @@ -2999,7 +2996,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { playbarHeadDragging = true; - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) { playbarHeadDraggingWasPlaying = true; stopAudioPlayback(); @@ -3790,7 +3787,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.beatHit()) return false; - if (metronomeVolume > 0.0 && !isPlaytesting && ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing)) + if (metronomeVolume > 0.0 && !isPlaytesting && ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing)) { var currentMeasureTime:Float = Conductor.instance.getMeasureTimeInMs(Conductor.instance.currentMeasure); var currentStepTime:Float = Conductor.instance.getStepTimeInMs(Conductor.instance.currentStep); @@ -3813,7 +3810,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.stepHit()) return false; - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) { if (healthIconDad != null) healthIconDad.onStepHit(Conductor.instance.currentStep); if (healthIconBF != null) healthIconBF.onStepHit(Conductor.instance.currentStep); @@ -3853,11 +3850,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - if ((!audioInstTrack.isPlaying || (audioVocalTrackGroup.length > 0 && !audioVocalTrackGroup.playing)) + if ((!audioInstTrack.playing || (audioVocalTrackGroup.length > 0 && !audioVocalTrackGroup.playing)) && currentScrollEase != scrollPositionInPixels) easeSongToScrollPosition(currentScrollEase); } - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) { currentScrollEase = scrollPositionInPixels; @@ -4581,7 +4578,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState shouldEase = true; if (shouldPause - && (audioInstTrack?.isPlaying || audioVocalTrackGroup.playing)) stopAudioPlayback(); // Only do this once, not every frame + && (audioInstTrack?.playing || audioVocalTrackGroup.playing)) stopAudioPlayback(); // Only do this once, not every frame // Resync the conductor and audio tracks. if (playheadAmount != 0) this.playheadPositionInPixels += playheadAmount; @@ -4770,7 +4767,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { gridPlayheadScrollAreaPressed = true; // Stop audio playback while dragging on the grid playhead. - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) { playbarHeadDraggingWasPlaying = true; stopAudioPlayback(); @@ -5071,7 +5068,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { notePreviewPlayHeadDragging = true; // Stop audio playback while dragging on the note preview playhead. - if ((audioInstTrack != null && audioInstTrack.isPlaying) || audioVocalTrackGroup.playing) + if ((audioInstTrack != null && audioInstTrack.playing) || audioVocalTrackGroup.playing) { playbarHeadDraggingWasPlaying = true; stopAudioPlayback(); @@ -7293,7 +7290,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState currentScrollEase = this.scrollPositionInPixels; - if (audioInstTrack.isPlaying || audioVocalTrackGroup.playing) + if (audioInstTrack.playing || audioVocalTrackGroup.playing) { // Pause stopAudioPlayback(); diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx index 52da10ead28..91b091f8aa2 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx @@ -109,7 +109,7 @@ class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog vocalsEntry.onClick = function(_event) { - Dialogs.openBinaryFile('Open $charName Vocals', [{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) + Dialogs.openBinaryFile('Open $charName Vocals', FileUtil.FILE_EXTENSION_INFO_AUDIO, function(selectedFile) { if (selectedFile != null && selectedFile.bytes != null) { diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index e3a974b5e44..9fdc8c72bea 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -13,6 +13,7 @@ import funkin.audio.waveform.WaveformSprite; import flixel.util.FlxColor; import haxe.io.Bytes; import haxe.io.Path; +import lime.media.AudioBuffer; /** * Functions for loading audio for the chart editor. @@ -69,6 +70,7 @@ class ChartEditorAudioHandler */ public static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = '', wipeFirst:Bool = false):Bool { + if (AudioBuffer.getCodec(bytes) == null) return false; var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; if (wipeFirst) wipeVocalData(state); state.audioVocalTrackData.set(trackId, bytes); @@ -119,6 +121,7 @@ class ChartEditorAudioHandler */ public static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = '', wipeFirst:Bool = false):Bool { + if (AudioBuffer.getCodec(bytes) == null) return false; if (instId == '') instId = 'default'; if (wipeFirst) wipeInstrumentalData(state); state.audioInstTrackData.set(instId, bytes); @@ -159,8 +162,6 @@ class ChartEditorAudioHandler var instTrack:Null = SoundUtil.buildSoundFromBytes(instTrackData); if (instTrack == null) return false; - instTrack.important = true; - stopExistingInstrumental(state); state.audioInstTrack = instTrack; state.postLoadInstrumental(); @@ -193,8 +194,6 @@ class ChartEditorAudioHandler // early return if (vocalTrack == null) return false; - vocalTrack.important = true; - switch (charType) { case BF: @@ -299,7 +298,7 @@ class ChartEditorAudioHandler if (state.stretchySound1 == null) return; // Prevent spam playing that could cause issues. - if (state.stretchySound1?.isPlaying ?? false || state.stretchySound2?.isPlaying ?? false) return; + if (state.stretchySound1?.playing ?? false || state.stretchySound2?.playing ?? false) return; state.stretchySounds = !state.stretchySounds; state.stretchySound1.play(true); @@ -311,7 +310,7 @@ class ChartEditorAudioHandler if (state.stretchySound2 == null) return; // Prevent spam playing that could cause issues. - if (state.stretchySound1?.isPlaying ?? false || state.stretchySound2?.isPlaying ?? false) return; + if (state.stretchySound1?.playing ?? false || state.stretchySound2?.playing ?? false) return; state.stretchySounds = !state.stretchySounds; state.stretchySound2.play(true); @@ -343,26 +342,14 @@ class ChartEditorAudioHandler var instTrackIds = state.audioInstTrackData.keys().array(); for (key in instTrackIds) { - if (key == 'default') - { - var data:Null = state.audioInstTrackData.get('default'); - if (data == null) - { - trace(' WARNING '.warning() + ' Failed to access inst track ($key)'); - continue; - } - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data)); - } - else + var data:Null = state.audioInstTrackData.get(key); + if (data == null) { - var data:Null = state.audioInstTrackData.get(key); - if (data == null) - { - trace(' WARNING '.warning() + ' Failed to access inst track ($key)'); - continue; - } - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data)); + trace(' WARNING '.warning() + ' Failed to access inst track ($key)'); + continue; } + var extension = AudioBuffer.getCodec(data).toFormat(); + zipEntries.push(FileUtil.makeZIPEntryFromBytes(key == 'default' ? 'Inst.${extension}' : 'Inst-${key}.${extension}', data)); } return zipEntries; @@ -386,7 +373,8 @@ class ChartEditorAudioHandler trace(' WARNING '.warning() + ' Failed to access vocal track ($key)'); continue; } - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data)); + var extension = AudioBuffer.getCodec(data).toFormat(); + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.${extension}', data)); } return zipEntries; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 73571789534..f6f6577acdf 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -96,7 +96,7 @@ class ChartEditorDialogHandler state.isHaxeUIDialogOpen = true; state.stopAudioPlayback(); - if (state.welcomeMusic != null && !state.welcomeMusic.isPlaying) state.fadeInWelcomeMusic(); + if (state.welcomeMusic != null && !state.welcomeMusic.playing) state.fadeInWelcomeMusic(); return dialog; } @@ -532,7 +532,7 @@ class ChartEditorDialogHandler instrumentalBox.onClick = function(_) { - Dialogs.openBinaryFile('Open Instrumental', [{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) + Dialogs.openBinaryFile('Open Instrumental', FileUtil.FILE_EXTENSION_INFO_AUDIO, function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { @@ -567,17 +567,8 @@ class ChartEditorDialogHandler } else { - var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? '')) - { - 'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})'; - } - else - { - 'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})'; - } - // Tell the user the load was successful. - state.error('Failed to Load Instrumental', message); + state.error('Failed to Load Instrumental', 'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})'); } }; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index a7497c550dd..b1124b327de 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -309,8 +309,7 @@ class ChartEditorImportExportHandler if (variMetadata == null) continue; var instId:String = variMetadata?.playData?.characters?.instrumental ?? ''; - - var instFileName:String = manifest.getInstFileName(instId); + var instFileName:String = manifest.getInstFileName(instId, fileEntries); var instFileBytes:Bytes = mappedFileEntries.get(instFileName)?.data ?? throw 'Could not locate instrumental ($instFileName).'; if (!ChartEditorAudioHandler.loadInstFromBytes(state, instFileBytes, instId)) throw 'Could not load instrumental ($instFileName).'; @@ -318,7 +317,7 @@ class ChartEditorImportExportHandler var playerVoiceList:Array = variMetadata?.playData.characters?.playerVocals ?? [playerCharId]; for (voice in playerVoiceList) { - var playerVocalsFileName:String = manifest.getVocalsFileName(voice, variation); + var playerVocalsFileName:String = manifest.getVocalsFileName(voice, variation, fileEntries); var playerVocalsFileBytes:Null = mappedFileEntries.get(playerVocalsFileName)?.data; if (playerVocalsFileBytes == null) { @@ -336,7 +335,7 @@ class ChartEditorImportExportHandler var opponentVoiceList:Array = variMetadata?.playData.characters?.opponentVocals ?? [opponentCharId]; for (voice in opponentVoiceList) { - var opponentVocalsFileName:String = manifest.getVocalsFileName(voice, variation); + var opponentVocalsFileName:String = manifest.getVocalsFileName(voice, variation, fileEntries); var opponentVocalsFileBytes:Null = mappedFileEntries.get(opponentVocalsFileName)?.data; if (opponentVocalsFileBytes == null) { diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx index 5cde68f6308..657fef1c1c0 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx @@ -324,7 +324,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox for (index in 0...numberOfTicks) { var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index; - var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformMusic.waveform.waveformData.pointsPerSecond(); + var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformMusic.waveform.waveformData?.pointsPerSecond(); var tickLabel:Label = new Label(); tickLabel.text = formatTime(tickTime); @@ -397,7 +397,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox // Move the audio preview to the playhead position. var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex); + var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData?.indexToSeconds(currentWaveformIndex); audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; } @@ -452,11 +452,11 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos; var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); + var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData?.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos; var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); + var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData?.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); chartEditorState.performCommand(new SetFreeplayPreviewCommand(previewStartPosMs, previewEndPosMs)); @@ -629,7 +629,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox if (audioPreviewTracks.playing) { - var targetScrollPos:Float = waveformMusic.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + var targetScrollPos:Float = waveformMusic.waveform.waveformData?.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); // waveformScrollview.hscrollPos = targetScrollPos; var prevPlayheadAbsolutePos = playheadAbsolutePos; playheadAbsolutePos = targetScrollPos; @@ -652,11 +652,11 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox { var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos; var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); + var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData?.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos; var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); + var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData?.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); // Set the values in milliseconds. freeplayPreviewStart.value = previewStartPosMs; @@ -667,8 +667,8 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox } else { - previewBoxStartPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewStart / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); - previewBoxEndPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewEnd / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + previewBoxStartPosAbsolute = waveformMusic.waveform.waveformData?.secondsToIndex(chartEditorState.currentSongFreeplayPreviewStart / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + previewBoxEndPosAbsolute = waveformMusic.waveform.waveformData?.secondsToIndex(chartEditorState.currentSongFreeplayPreviewEnd / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); freeplayPreviewStart.value = chartEditorState.currentSongFreeplayPreviewStart; freeplayPreviewEnd.value = chartEditorState.currentSongFreeplayPreviewEnd; @@ -679,7 +679,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox { super.refresh(); - waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformMusic.waveform.waveformData.pointsPerSecond()); + waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformMusic.waveform.waveformData?.pointsPerSecond()); var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index 69f15de58bb..54597c6ad0c 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -211,7 +211,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { // We have to change the song time to match the playhead position when we move it. var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex); + var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData?.indexToSeconds(currentWaveformIndex); audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; } @@ -323,7 +323,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox for (index in 0...numberOfTicks) { var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index; - var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformInstrumental.waveform.waveformData.pointsPerSecond(); + var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformInstrumental.waveform.waveformData?.pointsPerSecond(); var tickLabel:Label = new Label(); tickLabel.text = formatTime(tickTime); @@ -396,7 +396,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox // Move the audio preview to the playhead position. var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor)); - var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex); + var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData?.indexToSeconds(currentWaveformIndex); audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; } @@ -424,11 +424,11 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox var deltaMilliseconds:Float = switch (dragWaveform) { case PLAYER: - deltaPixels / waveformPlayer.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + deltaPixels / waveformPlayer.waveform.waveformData?.pointsPerSecond() * Constants.MS_PER_SEC; case OPPONENT: - deltaPixels / waveformOpponent.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + deltaPixels / waveformOpponent.waveform.waveformData?.pointsPerSecond() * Constants.MS_PER_SEC; case INSTRUMENTAL: - deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + deltaPixels / waveformInstrumental.waveform.waveformData?.pointsPerSecond() * Constants.MS_PER_SEC; }; switch (dragWaveform) @@ -732,7 +732,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { trace('Playback time: ${audioPreviewTracks.time}'); - var targetScrollPos:Float = waveformInstrumental.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + var targetScrollPos:Float = waveformInstrumental.waveform.waveformData?.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); // waveformScrollview.hscrollPos = targetScrollPos; var prevPlayheadAbsolutePos = playheadAbsolutePos; playheadAbsolutePos = targetScrollPos; @@ -805,7 +805,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { super.refresh(); - waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformInstrumental.waveform.waveformData.pointsPerSecond()); + waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformInstrumental.waveform.waveformData?.pointsPerSecond()); var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor; diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 45dbcf7ebc4..b789eb5a4aa 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -14,6 +14,7 @@ import flixel.util.FlxColor; import openfl.filters.ShaderFilter; import flixel.util.FlxTimer; import funkin.audio.FunkinSound; +import funkin.audio.PreviewMusicData; import funkin.data.freeplay.player.PlayerRegistry; import funkin.ui.freeplay.dj.BaseFreeplayDJ; import funkin.ui.freeplay.dj.AnimateAtlasFreeplayDJ; @@ -44,7 +45,7 @@ import funkin.ui.AtlasText; import funkin.ui.FullScreenScaleMode; import funkin.ui.MusicBeatSubState; import funkin.ui.freeplay.backcards.*; -import funkin.ui.freeplay.components.DifficultySprite; +import funkin.ui.freeplay.components.*; import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.story.Level; @@ -272,6 +273,8 @@ class FreeplayState extends MusicBeatSubState public var freeplayArrow:Null; + var previewMusicData:Null = null; + public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { var fetchPlayableCharacter = function():PlayableCharacter @@ -2198,6 +2201,8 @@ class FreeplayState extends MusicBeatSubState FlxG.cameras.remove(funnyCam); // Cancel all song preview timers just in case a preview loads after we exit. clearPreviews(); + // Destroy the preview music data. + previewMusicData?.destroy(); } function goBack():Void @@ -2496,7 +2501,11 @@ class FreeplayState extends MusicBeatSubState } // Reset the song preview in case we changed variations (normal->erect etc) - if (currentVariation != previousVariation) playCurSongPreview(); + if (currentVariation != previousVariation) + { + if (FlxG.sound.music != null) FlxG.sound.music.fadeOut(FADE_IN_DELAY); + FlxTimer.wait(FADE_IN_DELAY, playCurSongPreview.bind(currentCapsule)); + } } // Set the album graphic and play the animation if relevant. @@ -2776,6 +2785,7 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); + FlxG.sound.music?.fadeOut(0.2, FlxEase.quadIn); #if FEATURE_TOUCH_CONTROLS if (backButton != null) @@ -2786,6 +2796,7 @@ class FreeplayState extends MusicBeatSubState #end funnyCam.fade(FlxColor.BLACK, 0.2, false, function() { + FlxG.sound.music?.stop(); Paths.setCurrentLevel(cap?.freeplayData?.levelId); LoadingState.loadPlayState({ targetSong: targetSong, @@ -2929,7 +2940,7 @@ class FreeplayState extends MusicBeatSubState if (grpCapsules.countLiving() > 0 && !prepForNewRank && uiStateMachine.canInteract()) { - FlxG.sound.music?.pause(); + if (FlxG.sound.music != null) FlxG.sound.music.fadeOut(FADE_IN_DELAY); FlxTimer.wait(FADE_IN_DELAY, playCurSongPreview.bind(currentCapsule)); currentCapsule.selected = true; @@ -2946,6 +2957,9 @@ class FreeplayState extends MusicBeatSubState { if (daSongCapsule == null) daSongCapsule = currentCapsule; + // Make sure the player is still hovering over the song we want to load preview for + if (!daSongCapsule.selected) return; + var previewVolume:Float = 0.7; if (dj != null) previewVolume *= dj.getMusicPreviewMult(); @@ -2960,12 +2974,10 @@ class FreeplayState extends MusicBeatSubState overrideExisting: true, restartTrack: false }); - if (FlxG.sound.music != null) FlxG.sound.music.fadeIn(2, 0, previewVolume); + FlxG.sound.music.fadeIn(PreviewMusicData.FADE_IN_DURATION, 0, previewVolume, PreviewMusicData.FADE_IN_EASE_FUNCTION); } else { - // Make sure the player is still hovering over the song we want to load preview for - if (!daSongCapsule.selected) return; var previewSong:Null = daSongCapsule?.freeplayData?.data; if (previewSong == null) return; @@ -2985,35 +2997,27 @@ class FreeplayState extends MusicBeatSubState instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; // trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); - FunkinSound.playMusic(previewSong.id, { - startingVolume: 0.0, - overrideExisting: true, - restartTrack: false, - mapTimeChanges: false, // The music metadata is not alongside the audio file so this won't work. - pathsFunction: INST, - suffix: instSuffix, - partialParams: { - loadPartial: true, - start: daSongCapsule?.freeplayData?.previewStartTime ?? 0, - end: daSongCapsule?.freeplayData?.previewEndTime ?? 0.2 - }, - onLoad: function() - { - FlxG.sound.music.fadeIn(2, 0, previewVolume); - - var fadeStart:Float = (FlxG.sound.music.length / 1000) - 2; + if (FlxG.sound.music == null) + { + // Initialize the FlxG.sound.music if it haven't been created. + FunkinSound.setMusic(FunkinSound.load(null)); + } - previewTimers.push(new FlxTimer().start(fadeStart, function(_) - { - FlxG.sound.music.fadeOut(2, 0); - })); + if (previewMusicData == null) previewMusicData = new PreviewMusicData(); + previewMusicData.setAssetPath(Paths.inst(previewSong.id, instSuffix), + daSongCapsule?.freeplayData?.previewStartTime, daSongCapsule?.freeplayData?.previewEndTime, null, function(musicData:PreviewMusicData) + { + // Check again if it's still selected. + if (!daSongCapsule.selected) return; - previewTimers.push(new FlxTimer().start(FlxG.sound.music.length / 1000, function(_) - { - playCurSongPreview(); - })); - }, + FlxG.sound.music.fadeTween?.cancel(); + FlxG.sound.music.unload(); + FlxG.sound.music.loadEmbedded(musicData); + FlxG.sound.music.volume = previewVolume; + FlxG.sound.music.looped = true; + FlxG.sound.music.play(true, 0); }); + if (songDifficulty != null) { Conductor.instance.mapTimeChanges(songDifficulty.timeChanges); diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index c0b3ef72030..e5c4c4cf752 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -39,6 +39,10 @@ class PreferencesMenu extends Page var hudCamera:FlxCamera; var camFollow:FlxObject; + #if desktop + var audioDeviceItem:EnumPreferenceItem; + #end + public function new() { super(); @@ -177,6 +181,25 @@ class PreferencesMenu extends Page }, Preferences.autoFullscreen); #end + #if desktop + audioDeviceItem = createPrefItemEnum('Audio Device', 'What audio device should the game playbacks sounds to.', generateAudioDeviceEnums(), + (key:String, value:String) -> + { + Preferences.audioDevice = value; + }, Preferences.audioDevice); + FlxG.sound.onDefaultDeviceChanged.add(refreshAudioDeviceItem); + FlxG.sound.onDeviceAdded.add(refreshAudioDeviceItem); + FlxG.sound.onDeviceRemoved.add(refreshAudioDeviceItem); + #end + + #if native + createPrefItemCheckbox('Music Streaming', 'Turn this off if your hardware can\'t handle it and freezes the song every few seconds.', + function(value:Bool):Void + { + Preferences.streamedMusic = value; + }, Preferences.streamedMusic); + #end + // disable on mobile since it barely has any effect #if !mobile createPrefItemEnum('VSync', "When enabled, the game attempts to match the framerate with your monitor's refresh rate.", @@ -267,7 +290,7 @@ class PreferencesMenu extends Page * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ - function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool, available:Bool = true):Void + function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool, available:Bool = true):CheckboxPreferenceItem { var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, 120 * (items.length - 1 + 1), defaultValue, available); @@ -281,6 +304,8 @@ class PreferencesMenu extends Page preferenceItems.add(checkbox); preferenceDesc.push(prefDesc); + + return checkbox; } /** @@ -294,13 +319,16 @@ class PreferencesMenu extends Page * @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12) */ function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Float, min:Float, - max:Float, step:Float = 0.1, precision:Int):Void + max:Float, step:Float = 0.1, precision:Int):NumberPreferenceItem { var item = new NumberPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter); + items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); + + return item; } /** @@ -310,7 +338,7 @@ class PreferencesMenu extends Page * @param min Minimum value (default = 0) * @param max Maximum value (default = 100) */ - function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void + function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):NumberPreferenceItem { var newCallback = function(value:Float) { @@ -320,11 +348,15 @@ class PreferencesMenu extends Page { return '${value}%'; }; + var item = new NumberPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter); + items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); + + return item; } /** @@ -333,18 +365,46 @@ class PreferencesMenu extends Page * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ - function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->T->Void, defaultKey:String):Void + function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->T->Void, defaultKey:String):EnumPreferenceItem { var item = new EnumPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, values, defaultKey, onChange); + items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); + + return item; } + #if desktop + function generateAudioDeviceEnums():Map + { + var enums = ['Default' => 'Default']; + var devices = lime.media.AudioManager.getPlaybackDeviceNames(); + + for (device in devices) + { + enums.set(device, device); + } + + return enums; + } + + function refreshAudioDeviceItem(deviceName:String):Void + { + audioDeviceItem.changeEnums(generateAudioDeviceEnums(), Preferences.audioDevice); + } + #end + override function exit():Void { camFollow.setPosition(640, 30); menuCamera.snapToTarget(); + #if desktop + FlxG.sound.onDefaultDeviceChanged.remove(refreshAudioDeviceItem); + FlxG.sound.onDeviceAdded.remove(refreshAudioDeviceItem); + FlxG.sound.onDeviceRemoved.remove(refreshAudioDeviceItem); + #end super.exit(); } } diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx index c254bb48bb8..5f2e43e5fad 100644 --- a/source/funkin/ui/options/items/EnumPreferenceItem.hx +++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx @@ -31,14 +31,23 @@ class EnumPreferenceItem extends TextMenuItem super(x, y, name, function() { var value = map.get(this.currentKey); - callback(this.currentKey, value); + this.onChangeCallback(this.currentKey, value); }); updateHitbox(); - this.map = map; - this.currentKey = defaultKey; this.onChangeCallback = callback; + changeEnums(map, defaultKey); + + lefthandText = new AtlasText(x + 15, y, formatted(defaultKey), AtlasFont.DEFAULT); + + this.fireInstantly = true; + } + + public function changeEnums(map:Map, ?defaultKey:String):Void + { + this.map = map; + if (defaultKey != null) this.currentKey = defaultKey; var i:Int = 0; for (key in map.keys()) @@ -49,10 +58,6 @@ class EnumPreferenceItem extends TextMenuItem if (this.currentKey == key) index = i; i += 1; } - - lefthandText = new AtlasText(x + 15, y, formatted(defaultKey), AtlasFont.DEFAULT); - - this.fireInstantly = true; } override function update(elapsed:Float):Void diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 95868ff6325..dc539f3a12c 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -564,6 +564,11 @@ class Constants */ public static final EXT_DATA = "json"; + /** + * The file extensions supported for loading audio files. + */ + public static final EXT_SOUNDS = ["ogg", "mp3", "wav", "opus", "flac"]; + /** * OTHER */ diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index becb06e2197..ed6011c84a2 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -48,6 +48,11 @@ class FileUtil extension: 'fnfs', label: 'Friday Night Funkin\' Stage', }; + + public static final FILE_EXTENSION_INFO_AUDIO:Array = [{ + extension: 'ogg, mp3, wav, opus, flac', + label: 'Audio File (*.ogg, *.mp3, *.wav, *.opus, *.flac)', + }]; #end /** diff --git a/source/funkin/util/file/FNFCUtil.hx b/source/funkin/util/file/FNFCUtil.hx index 103988d5811..69f4222236a 100644 --- a/source/funkin/util/file/FNFCUtil.hx +++ b/source/funkin/util/file/FNFCUtil.hx @@ -56,7 +56,7 @@ class FNFCUtil var audioVocalTrackGroup = new VoicesGroup(); var instId:String = targetDifficulty.characters.instrumental ?? ''; - var audioInstTrackName:String = manifest.getInstFileName(instId); + var audioInstTrackName:String = manifest.getInstFileName(instId, fileEntries); try { audioInstTrack = loadSoundFromFNFCZipEntries(mappedFileEntries, audioInstTrackName); @@ -72,7 +72,7 @@ class FNFCUtil var playerVocalList:Array = targetDifficulty.characters.playerVocals ?? []; for (playerVocalId in playerVocalList) { - var audioVocalTrackName:String = manifest.getVocalsFileName(playerVocalId, variation); + var audioVocalTrackName:String = manifest.getVocalsFileName(playerVocalId, variation, fileEntries); var audioVocalTrack = loadSoundFromFNFCZipEntries(mappedFileEntries, audioVocalTrackName); try { @@ -88,7 +88,7 @@ class FNFCUtil var opponentVocalList:Array = targetDifficulty.characters.opponentVocals ?? []; for (opponentVocalId in opponentVocalList) { - var audioVocalTrackName:String = manifest.getVocalsFileName(opponentVocalId, variation); + var audioVocalTrackName:String = manifest.getVocalsFileName(opponentVocalId, variation, fileEntries); var audioVocalTrack = loadSoundFromFNFCZipEntries(mappedFileEntries, audioVocalTrackName); try {