From 1abd587645f6c030159a79921ad18786dde038e9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 6 Dec 2023 15:04:24 -0500 Subject: [PATCH 1/6] Rework offsets to be built into the FlxSound --- source/funkin/audio/FunkinSound.hx | 129 ++++++++++++++++++ source/funkin/audio/SoundGroup.hx | 30 ++-- source/funkin/audio/VoicesGroup.hx | 42 ++---- source/funkin/play/song/Song.hx | 7 +- .../handlers/ChartEditorAudioHandler.hx | 3 +- source/funkin/util/assets/SoundUtil.hx | 9 +- 6 files changed, 168 insertions(+), 52 deletions(-) create mode 100644 source/funkin/audio/FunkinSound.hx diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx new file mode 100644 index 000000000..88925f92d --- /dev/null +++ b/source/funkin/audio/FunkinSound.hx @@ -0,0 +1,129 @@ +package funkin.audio; + +#if flash11 +import flash.media.Sound; +import flash.utils.ByteArray; +#end +import flixel.sound.FlxSound; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.system.FlxAssets.FlxSoundAsset; +import openfl.Assets; +#if (openfl >= "8.0.0") +import openfl.utils.AssetType; +#end + +/** + * A FlxSound which adds additional functionality: + * - Delayed playback via negative song position. + */ +@:nullSafety +class FunkinSound extends FlxSound +{ + static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); + + /** + * Are we in a state where the song should play but time is negative? + */ + var shouldPlay:Bool = false; + + public function new() + { + super(); + } + + public override function update(elapsedSec:Float) + { + if (!playing && !shouldPlay) return; + + if (_time < 0) + { + var elapsedMs = elapsedSec * Constants.MS_PER_SEC; + _time += elapsedMs; + if (_time >= 0) + { + _time = 0; + shouldPlay = false; + super.play(); + } + } + else + { + super.update(elapsedSec); + } + } + + public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound + { + if (!exists) return this; + + if (forceRestart) + { + cleanup(false, true); + } + else if (playing || shouldPlay) + { + return this; + } + + if (startTime < 0) + { + shouldPlay = true; + _time = startTime; + this.endTime = endTime; + return this; + } + + if (_paused) + { + resume(); + } + else + { + startSound(startTime); + } + + this.endTime = endTime; + return this; + } + + /** + * Creates a new `FunkinSound` object. + * + * @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead. + * @param volume How loud to play it (0 to 1). + * @param looped Whether to loop this sound. + * @param group The group to add this sound to. + * @param autoDestroy Whether to destroy this sound when it finishes playing. + * Leave this value set to `false` if you want to re-use this `FunkinSound` instance. + * @param autoPlay Whether to play the sound immediately or wait for a `play()` call. + * @param onComplete Called when the sound finished playing. + * @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds. + * @return A `FunkinSound` object. + */ + public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false, + ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound + { + var sound:FunkinSound = cache.recycle(construct); + + sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); + + sound.volume = volume; + sound.group = FlxG.sound.defaultSoundGroup; + if (autoPlay) sound.play(); + + // Call OnlLoad() because the sound already loaded + if (onLoad != null && sound._sound != null) onLoad(); + + return sound; + } + + static function construct():FunkinSound + { + var sound:FunkinSound = new FunkinSound(); + + cache.add(sound); + FlxG.sound.list.add(sound); + + return sound; + } +} diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index ae755e231..15c2296ca 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -2,12 +2,13 @@ package funkin.audio; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.sound.FlxSound; +import funkin.audio.FunkinSound; /** - * A group of FlxSounds that are all synced together. - * Unlike FlxSoundGroup, you cann also control their time and pitch. + * A group of FunkinSounds that are all synced together. + * Unlike FlxSoundGroup, you can also control their time and pitch. */ -class SoundGroup extends FlxTypedGroup +class SoundGroup extends FlxTypedGroup { public var time(get, set):Float; @@ -28,14 +29,13 @@ class SoundGroup extends FlxTypedGroup if (files == null) { // Add an empty voice. - result.add(new FlxSound()); + result.add(new FunkinSound()); return result; } for (sndFile in files) { - var snd:FlxSound = new FlxSound().loadEmbedded(Paths.voices(song, '$sndFile')); - FlxG.sound.list.add(snd); // adds it to sound group for proper volumes + var snd:FunkinSound = FunkinSound.load(Paths.voices(song, '$sndFile')); result.add(snd); // adds it to main group for other shit } @@ -67,9 +67,9 @@ class SoundGroup extends FlxTypedGroup /** * Add a sound to the group. */ - public override function add(sound:FlxSound):FlxSound + public override function add(sound:FunkinSound):FunkinSound { - var result:FlxSound = super.add(sound); + var result:FunkinSound = super.add(sound); if (result == null) return null; @@ -97,7 +97,7 @@ class SoundGroup extends FlxTypedGroup */ public function pause() { - forEachAlive(function(sound:FlxSound) { + forEachAlive(function(sound:FunkinSound) { sound.pause(); }); } @@ -107,7 +107,7 @@ class SoundGroup extends FlxTypedGroup */ public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) { - forEachAlive(function(sound:FlxSound) { + forEachAlive(function(sound:FunkinSound) { sound.play(forceRestart, startTime, endTime); }); } @@ -117,7 +117,7 @@ class SoundGroup extends FlxTypedGroup */ public function resume() { - forEachAlive(function(sound:FlxSound) { + forEachAlive(function(sound:FunkinSound) { sound.resume(); }); } @@ -127,7 +127,7 @@ class SoundGroup extends FlxTypedGroup */ public function stop() { - forEachAlive(function(sound:FlxSound) { + forEachAlive(function(sound:FunkinSound) { sound.stop(); }); } @@ -151,7 +151,7 @@ class SoundGroup extends FlxTypedGroup function set_time(time:Float):Float { - forEachAlive(function(snd) { + forEachAlive(function(snd:FunkinSound) { // account for different offsets per sound? snd.time = time; }); @@ -169,7 +169,7 @@ class SoundGroup extends FlxTypedGroup // in PlayState, adjust the code so that it only mutes the player1 vocal tracks? function set_volume(volume:Float):Float { - forEachAlive(function(snd) { + forEachAlive(function(snd:FunkinSound) { snd.volume = volume; }); @@ -189,7 +189,7 @@ class SoundGroup extends FlxTypedGroup { #if FLX_PITCH trace('Setting audio pitch to ' + val); - forEachAlive(function(snd) { + forEachAlive(function(snd:FunkinSound) { snd.pitch = val; }); #end diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 8c95eb3eb..42f31af70 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -1,12 +1,12 @@ package funkin.audio; -import flixel.sound.FlxSound; +import funkin.audio.FunkinSound; import flixel.group.FlxGroup.FlxTypedGroup; class VoicesGroup extends SoundGroup { - var playerVoices:FlxTypedGroup; - var opponentVoices:FlxTypedGroup; + var playerVoices:FlxTypedGroup; + var opponentVoices:FlxTypedGroup; /** * Control the volume of only the sounds in the player group. @@ -31,14 +31,14 @@ class VoicesGroup extends SoundGroup public function new() { super(); - playerVoices = new FlxTypedGroup(); - opponentVoices = new FlxTypedGroup(); + playerVoices = new FlxTypedGroup(); + opponentVoices = new FlxTypedGroup(); } /** * Add a voice to the player group. */ - public function addPlayerVoice(sound:FlxSound):Void + public function addPlayerVoice(sound:FunkinSound):Void { super.add(sound); playerVoices.add(sound); @@ -46,7 +46,7 @@ class VoicesGroup extends SoundGroup function set_playerVolume(volume:Float):Float { - playerVoices.forEachAlive(function(voice:FlxSound) { + playerVoices.forEachAlive(function(voice:FunkinSound) { voice.volume = volume; }); return playerVolume = volume; @@ -59,10 +59,10 @@ class VoicesGroup extends SoundGroup snd.time = time; }); - playerVoices.forEachAlive(function(voice:FlxSound) { + playerVoices.forEachAlive(function(voice:FunkinSound) { voice.time -= playerVoicesOffset; }); - opponentVoices.forEachAlive(function(voice:FlxSound) { + opponentVoices.forEachAlive(function(voice:FunkinSound) { voice.time -= opponentVoicesOffset; }); @@ -71,7 +71,7 @@ class VoicesGroup extends SoundGroup function set_playerVoicesOffset(offset:Float):Float { - playerVoices.forEachAlive(function(voice:FlxSound) { + playerVoices.forEachAlive(function(voice:FunkinSound) { voice.time += playerVoicesOffset; voice.time -= offset; }); @@ -80,33 +80,17 @@ class VoicesGroup extends SoundGroup function set_opponentVoicesOffset(offset:Float):Float { - opponentVoices.forEachAlive(function(voice:FlxSound) { + opponentVoices.forEachAlive(function(voice:FunkinSound) { voice.time += opponentVoicesOffset; voice.time -= offset; }); return opponentVoicesOffset = offset; } - public override function update(elapsed:Float):Void - { - forEachAlive(function(snd) { - if (snd.time < 0) - { - // Sync the time without calling update(). - // time gets reset if it's negative. - snd.time += elapsed * 1000; - } - else - { - snd.update(elapsed); - } - }); - } - /** * Add a voice to the opponent group. */ - public function addOpponentVoice(sound:FlxSound):Void + public function addOpponentVoice(sound:FunkinSound):Void { super.add(sound); opponentVoices.add(sound); @@ -114,7 +98,7 @@ class VoicesGroup extends SoundGroup function set_opponentVolume(volume:Float):Float { - opponentVoices.forEachAlive(function(voice:FlxSound) { + opponentVoices.forEachAlive(function(voice:FunkinSound) { voice.volume = volume; }); return opponentVolume = volume; diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 1cba42f60..afb9f0dd2 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -2,6 +2,7 @@ package funkin.play.song; import flixel.sound.FlxSound; import funkin.audio.VoicesGroup; +import funkin.audio.FunkinSound; import funkin.data.IRegistryEntry; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongChartData; @@ -532,16 +533,16 @@ class SongDifficulty } // Add player vocals. - if (voiceList[0] != null) result.addPlayerVoice(new FlxSound().loadEmbedded(Assets.getSound(voiceList[0]))); + if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(Assets.getSound(voiceList[0]))); // Add opponent vocals. - if (voiceList[1] != null) result.addOpponentVoice(new FlxSound().loadEmbedded(Assets.getSound(voiceList[1]))); + if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(Assets.getSound(voiceList[1]))); // Add additional vocals. if (voiceList.length > 2) { for (i in 2...voiceList.length) { - result.add(new FlxSound().loadEmbedded(Assets.getSound(voiceList[i]))); + result.add(FunkinSound.load(Assets.getSound(voiceList[i]))); } } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index f82a123a4..18c796cf7 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -3,6 +3,7 @@ package funkin.ui.debug.charting.handlers; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxSound; import funkin.audio.VoicesGroup; +import funkin.audio.FunkinSound; import funkin.play.character.BaseCharacter.CharacterType; import funkin.util.FileUtil; import funkin.util.assets.SoundUtil; @@ -165,7 +166,7 @@ class ChartEditorAudioHandler { var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; var vocalTrackData:Null = state.audioVocalTrackData.get(trackId); - var vocalTrack:Null = SoundUtil.buildFlxSoundFromBytes(vocalTrackData); + var vocalTrack:Null = SoundUtil.buildSoundFromBytes(vocalTrackData); if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); diff --git a/source/funkin/util/assets/SoundUtil.hx b/source/funkin/util/assets/SoundUtil.hx index 872a61609..43602b999 100644 --- a/source/funkin/util/assets/SoundUtil.hx +++ b/source/funkin/util/assets/SoundUtil.hx @@ -1,7 +1,8 @@ package funkin.util.assets; import haxe.io.Bytes; -import flixel.system.FlxSound; +import openfl.media.Sound as OpenFLSound; +import funkin.audio.FunkinSound; class SoundUtil { @@ -11,13 +12,13 @@ class SoundUtil * @param input The byte data. * @return The playable sound, or `null` if loading failed. */ - public static function buildFlxSoundFromBytes(input:Null):Null + public static function buildSoundFromBytes(input:Null):Null { if (input == null) return null; - var openflSound:openfl.media.Sound = new openfl.media.Sound(); + var openflSound:OpenFLSound = new OpenFLSound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length); - var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false); + var output:FunkinSound = FunkinSound.load(openflSound, 1.0, false); return output; } } From 66611c5d2f509e2b9669a9847d7950995c216cc3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 6 Dec 2023 22:03:36 -0500 Subject: [PATCH 2/6] Fixes to FlxSound when pausing before starting --- source/funkin/audio/FunkinSound.hx | 66 +++++++++++++++++++++++------ source/funkin/data/song/SongData.hx | 1 + source/funkin/play/PlayState.hx | 52 +++-------------------- source/funkin/play/song/Song.hx | 6 ++- 4 files changed, 66 insertions(+), 59 deletions(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 88925f92d..40293b0ce 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -21,10 +21,22 @@ class FunkinSound extends FlxSound { static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); + public var isPlaying(get, never):Bool; + + function get_isPlaying():Bool + { + return this.playing || this._shouldPlay; + } + /** * Are we in a state where the song should play but time is negative? */ - var shouldPlay:Bool = false; + var _shouldPlay:Bool = false; + + /** + * For debug purposes. + */ + var _label:String = "unknown"; public function new() { @@ -33,7 +45,7 @@ class FunkinSound extends FlxSound public override function update(elapsedSec:Float) { - if (!playing && !shouldPlay) return; + if (!playing && !_shouldPlay) return; if (_time < 0) { @@ -41,9 +53,8 @@ class FunkinSound extends FlxSound _time += elapsedMs; if (_time >= 0) { - _time = 0; - shouldPlay = false; super.play(); + _shouldPlay = false; } } else @@ -60,29 +71,52 @@ class FunkinSound extends FlxSound { cleanup(false, true); } - else if (playing || shouldPlay) + else if (playing) { return this; } if (startTime < 0) { - shouldPlay = true; - _time = startTime; + this.active = true; + this._shouldPlay = true; + this._time = startTime; this.endTime = endTime; return this; } - - if (_paused) + else { - resume(); + if (_paused) + { + resume(); + } + else + { + startSound(startTime); + } + + this.endTime = endTime; + return this; + } + } + + public override function pause():FunkinSound + { + super.pause(); + this._shouldPlay = false; + return this; + } + + public override function resume():FunkinSound + { + if (this._time < 0) + { + this._shouldPlay = true; } else { - startSound(startTime); + super.resume(); } - - this.endTime = endTime; return this; } @@ -107,8 +141,14 @@ class FunkinSound extends FlxSound sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); + if (embeddedSound is String) + { + sound._label = embeddedSound; + } + sound.volume = volume; sound.group = FlxG.sound.defaultSoundGroup; + sound.persist = true; if (autoPlay) sound.play(); // Call OnlLoad() because the sound already loaded diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index c0fd96855..7886ada4f 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -90,6 +90,7 @@ class SongMetadata result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; + result.offsets = this.offsets; result.timeChanges = this.timeChanges; result.looped = this.looped; result.playData = this.playData; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 6136cf1b7..bd4b0f5e1 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -326,12 +326,6 @@ class PlayState extends MusicBeatSubState */ var overrideMusic:Bool = false; - /** - * After the song starts, the song offset may dictate we wait before the instrumental starts. - * This variable represents that delay, and is subtracted from until it reaches 0, before calling `music.play()` - */ - var songStartDelay:Float = 0.0; - /** * Forcibly disables all update logic while the game moves back to the Menu state. * This is used only when a critical error occurs and the game absolutely cannot continue. @@ -781,26 +775,7 @@ class PlayState extends MusicBeatSubState Conductor.formatOffset = 0.0; } - if (songStartDelay > 0) - { - // Code to handle the song not starting yet (positive instrumental offset in metadata). - // Wait for offset to elapse before actually hitting play on the instrumental. - songStartDelay -= elapsed * 1000; - if (songStartDelay <= 0) - { - FlxG.sound.music.play(); - Conductor.update(); // Normal conductor update. - } - else - { - // Make beat events still happen. - Conductor.update(Conductor.songPosition + elapsed * 1000); - } - } - else - { - Conductor.update(); // Normal conductor update. - } + Conductor.update(); // Normal conductor update. if (!isGamePaused) { @@ -1046,7 +1021,7 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; // Resume - FlxG.sound.music.play(); + FlxG.sound.music.play(FlxG.sound.music.time); if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); @@ -1730,21 +1705,9 @@ class PlayState extends MusicBeatSubState trace('Playing vocals...'); add(vocals); - if (FlxG.sound.music.time < 0) - { - // A positive instrumentalOffset means Conductor.songPosition will still be negative after the countdown elapses. - trace('POSITIVE OFFSET: Waiting to start song...'); - songStartDelay = Math.abs(FlxG.sound.music.time); - FlxG.sound.music.time = 0; - FlxG.sound.music.pause(); - vocals.pause(); - } - else - { - FlxG.sound.music.play(); - vocals.play(); - resyncVocals(); - } + FlxG.sound.music.play(FlxG.sound.music.time); + vocals.play(); + resyncVocals(); #if discord_rpc // Updating Discord Rich Presence (with Time Left) @@ -1753,7 +1716,7 @@ class PlayState extends MusicBeatSubState if (startTimestamp > 0) { - FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; + // FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; handleSkippedNotes(); } } @@ -1767,11 +1730,10 @@ class PlayState extends MusicBeatSubState // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) if (!FlxG.sound.music.playing) return; - if (songStartDelay > 0) return; vocals.pause(); - FlxG.sound.music.play(); + FlxG.sound.music.play(FlxG.sound.music.time); vocals.time = FlxG.sound.music.time; vocals.play(false, FlxG.sound.music.time); diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index afb9f0dd2..9e5de6143 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -455,7 +455,11 @@ class SongDifficulty public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void { var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; - FlxG.sound.playMusic(Paths.inst(this.song.id, suffix), volume, looped); + + FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped); + + // Workaround for a bug where FlxG.sound.music.update() was being called twice. + FlxG.sound.list.remove(FlxG.sound.music); } /** From 7f0caeb0261e52df3dd89c960f3d079631ac8803 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 8 Dec 2023 01:15:22 -0500 Subject: [PATCH 3/6] Work in progress on offsets in Chart Editor state. --- source/funkin/Conductor.hx | 84 ++++++++--- source/funkin/play/PlayState.hx | 17 +-- source/funkin/ui/MusicBeatState.hx | 1 + .../ui/debug/charting/ChartEditorState.hx | 140 +++++++++++++----- .../handlers/ChartEditorAudioHandler.hx | 8 +- .../handlers/ChartEditorDialogHandler.hx | 1 + .../ChartEditorImportExportHandler.hx | 1 + .../handlers/ChartEditorToolboxHandler.hx | 21 +++ 8 files changed, 198 insertions(+), 75 deletions(-) diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index f333b4072..c531678ad 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -45,8 +45,6 @@ class Conductor */ public static var songPosition(default, null):Float = 0; - public static var songPositionNoOffset(default, null):Float = 0; - /** * Beats per minute of the current song at the current time. */ @@ -61,6 +59,21 @@ class Conductor return currentTimeChange.bpm; } + /** + * Beats per minute of the current song at the start time. + */ + public static var startingBPM(get, never):Float; + + static function get_startingBPM():Float + { + if (bpmOverride != null) return bpmOverride; + + var timeChange = timeChanges[0]; + if (timeChange == null) return Constants.DEFAULT_BPM; + + return timeChange.bpm; + } + /** * The current value set by `forceBPM`. * If false, BPM is determined by time changes. @@ -146,18 +159,23 @@ class Conductor */ public static var currentStepTime(default, null):Float; - public static var currentStepTimeNoOffset(default, null):Float; - - public static var beatHit(default, null):FlxSignal = new FlxSignal(); - public static var stepHit(default, null):FlxSignal = new FlxSignal(); - - public static var lastSongPos:Float; - /** * An offset tied to the current chart file to compensate for a delay in the instrumental. */ public static var instrumentalOffset:Float = 0; + /** + * The instrumental offset, in terms of steps. + */ + public static var instrumentalOffsetSteps(get, never):Float; + + static function get_instrumentalOffsetSteps():Float + { + var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator; + + return instrumentalOffset / startingStepLengthMs; + } + /** * An offset tied to the file format of the audio file being played. */ @@ -168,6 +186,9 @@ class Conductor */ public static var inputOffset:Float = 0; + /** + * The number of beats in a measure. May be fractional depending on the time signature. + */ public static var beatsPerMeasure(get, never):Float; static function get_beatsPerMeasure():Float @@ -176,6 +197,10 @@ class Conductor return stepsPerMeasure / Constants.STEPS_PER_BEAT; } + /** + * The number of steps in a measure. + * TODO: I don't think this can be fractional? + */ public static var stepsPerMeasure(get, never):Int; static function get_stepsPerMeasure():Int @@ -184,6 +209,21 @@ class Conductor return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT); } + /** + * Signal fired when the Conductor advances to a new measure. + */ + public static var measureHit(default, null):FlxSignal = new FlxSignal(); + + /** + * Signal fired when the Conductor advances to a new beat. + */ + public static var beatHit(default, null):FlxSignal = new FlxSignal(); + + /** + * Signal fired when the Conductor advances to a new step. + */ + public static var stepHit(default, null):FlxSignal = new FlxSignal(); + function new() {} /** @@ -224,29 +264,29 @@ class Conductor songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0; } + var oldMeasure = currentMeasure; var oldBeat = currentBeat; var oldStep = currentStep; // Set the song position we are at (for purposes of calculating note positions, etc). Conductor.songPosition = songPosition; - // Exclude the offsets we just included earlier. - // This is the "true" song position that the audio should be using. - Conductor.songPositionNoOffset = Conductor.songPosition - instrumentalOffset - formatOffset; - currentTimeChange = timeChanges[0]; - for (i in 0...timeChanges.length) + if (Conductor.songPosition > 0.0) { - if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i]; + for (i in 0...timeChanges.length) + { + if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i]; - if (songPosition < timeChanges[i].timeStamp) break; + if (songPosition < timeChanges[i].timeStamp) break; + } } if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null) { trace('WARNING: Conductor is broken, timeChanges is empty.'); } - else if (currentTimeChange != null) + else if (currentTimeChange != null && Conductor.songPosition > 0.0) { // roundDecimal prevents representing 8 as 7.9999999 currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); @@ -255,9 +295,6 @@ class Conductor currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentBeatTime); currentMeasure = Math.floor(currentMeasureTime); - - currentStepTimeNoOffset = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) - + (songPositionNoOffset - currentTimeChange.timeStamp) / stepLengthMs, 6); } else { @@ -268,8 +305,6 @@ class Conductor currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentBeatTime); currentMeasure = Math.floor(currentMeasureTime); - - currentStepTimeNoOffset = FlxMath.roundDecimal((songPositionNoOffset / stepLengthMs), 4); } // FlxSignals are really cool. @@ -282,6 +317,11 @@ class Conductor { beatHit.dispatch(); } + + if (currentMeasure != oldMeasure) + { + measureHit.dispatch(); + } } public static function mapTimeChanges(songTimeChanges:Array) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index bf6df4423..d53aa09d7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -796,15 +796,6 @@ class PlayState extends MusicBeatSubState } Conductor.update(); // Normal conductor update. - - if (!isGamePaused) - { - // Interpolation type beat - if (Conductor.lastSongPos != Conductor.songPosition) - { - Conductor.lastSongPos = Conductor.songPosition; - } - } } var androidPause:Bool = false; @@ -1171,12 +1162,12 @@ class PlayState extends MusicBeatSubState if (!startingSong && FlxG.sound.music != null - && (Math.abs(FlxG.sound.music.time - (Conductor.songPositionNoOffset)) > 200 - || Math.abs(vocals.checkSyncError(Conductor.songPositionNoOffset)) > 200)) + && (Math.abs(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)) > 200 + || Math.abs(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)) > 200)) { trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(Conductor.songPositionNoOffset)); - trace(FlxG.sound.music.time - (Conductor.songPositionNoOffset)); + if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)); + trace(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)); resyncVocals(); } diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 7bdf8689c..077e9e495 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -84,6 +84,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler { // Display Conductor info in the watch window. FlxG.watch.addQuick("songPosition", Conductor.songPosition); + FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset); FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("bpm", Conductor.bpm); FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a3a8344c8..c1476fd34 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -23,12 +23,14 @@ import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; +import funkin.audio.FunkinSound; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongDataUtils; import funkin.data.song.SongRegistry; import funkin.input.Cursor; @@ -245,6 +247,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Make sure playhead doesn't go outside the song. if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs; + onSongLengthChanged(); + return this.songLengthInMs; } @@ -884,7 +888,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState * Replaced when switching instrumentals. * `null` until an instrumental track is loaded. */ - var audioInstTrack:Null = null; + var audioInstTrack:Null = null; /** * The raw byte data for the instrumental audio tracks. @@ -1156,6 +1160,41 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentSongMetadata.artist = value; } + /** + * Convenience property to get the song offset data for the current variation. + */ + var currentSongOffsets(get, set):SongOffsets; + + function get_currentSongOffsets():SongOffsets + { + if (currentSongMetadata.offsets == null) + { + // Initialize to the default value if not set. + currentSongMetadata.offsets = new SongOffsets(); + } + return currentSongMetadata.offsets; + } + + function set_currentSongOffsets(value:SongOffsets):SongOffsets + { + return currentSongMetadata.offsets = value; + } + + var currentInstrumentalOffset(get, set):Float; + + function get_currentInstrumentalOffset():Float + { + // TODO: Apply for alt instrumentals. + return currentSongOffsets.getInstrumentalOffset(); + } + + function set_currentInstrumentalOffset(value:Float):Float + { + // TODO: Apply for alt instrumentals. + currentSongOffsets.setInstrumentalOffset(value); + return value; + } + /** * The variation ID for the difficulty which is currently being edited. */ @@ -2024,7 +2063,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playbarHeadDragging = true; // If we were dragging the playhead while the song was playing, resume playing. - if (audioInstTrack != null && audioInstTrack.playing) + if (audioInstTrack != null && audioInstTrack.isPlaying) { playbarHeadDraggingWasPlaying = true; stopAudioPlayback(); @@ -2224,8 +2263,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder(); #else // Disable the menu item if we're not on a desktop platform. - var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem); - if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true; + menubarItemGoToBackupsFolder.disabled = true; #end menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog(); @@ -2435,7 +2473,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState super.update(elapsed); // These ones happen even if the modal dialog is open. - handleMusicPlayback(); + handleMusicPlayback(elapsed); handleNoteDisplay(); // These ones only happen if the modal dialog is not open. @@ -2471,7 +2509,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.beatHit()) return false; - if (isMetronomeEnabled && this.subState == null && (audioInstTrack != null && audioInstTrack.playing)) + if (isMetronomeEnabled && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying)) { playMetronomeTick(Conductor.currentBeat % 4 == 0); } @@ -2487,7 +2525,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.stepHit()) return false; - if (audioInstTrack != null && audioInstTrack.playing) + if (audioInstTrack != null && audioInstTrack.isPlaying) { if (healthIconDad != null) healthIconDad.onStepHit(Conductor.currentStep); if (healthIconBF != null) healthIconBF.onStepHit(Conductor.currentStep); @@ -2508,18 +2546,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * Handle syncronizing the conductor with the music playback. */ - function handleMusicPlayback():Void + function handleMusicPlayback(elapsed:Float):Void { - if (audioInstTrack != null && audioInstTrack.playing) + if (audioInstTrack != null) + { + // This normally gets called by FlxG.sound.update() + // but we handle instrumental updates manually to prevent FlxG.sound.music.update() + // from being called twice when we move to the PlayState. + audioInstTrack.update(elapsed); + } + + if (audioInstTrack != null && audioInstTrack.isPlaying) { if (FlxG.mouse.pressedMiddle) { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! var oldStepTime:Float = Conductor.currentStepTime; - var oldSongPosition:Float = Conductor.songPosition; + var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; Conductor.update(audioInstTrack.time); - handleHitsounds(oldSongPosition, Conductor.songPosition); + handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); // Resync vocals. if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { @@ -2535,9 +2581,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // Else, move the entire view. - var oldSongPosition:Float = Conductor.songPosition; + var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; Conductor.update(audioInstTrack.time); - handleHitsounds(oldSongPosition, Conductor.songPosition); + handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); // Resync vocals. if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { @@ -2546,7 +2592,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. - scrollPositionInPixels = Conductor.currentStepTime * GRID_SIZE - playheadPositionInPixels; + scrollPositionInPixels = (Conductor.currentStepTime + Conductor.instrumentalOffsetSteps) * GRID_SIZE - playheadPositionInPixels; // DO NOT move song to scroll position here specifically. @@ -3992,8 +4038,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent; } - var songPosSeconds:String = Std.string(Math.floor((songPos / 1000) % 60)).lpad('0', 2); - var songPosMinutes:String = Std.string(Math.floor((songPos / 1000) / 60)).lpad('0', 2); + var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); + var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2); + if (songPos < 0) songPosMinutes = '-' + songPosMinutes; var songPosString:String = '${songPosMinutes}:${songPosSeconds}'; if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString; @@ -4313,6 +4360,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { super.handleQuickWatch(); + FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0); + FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); @@ -4340,6 +4389,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { autoSave(); + stopWelcomeMusic(); + var startTimestamp:Float = 0; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; @@ -4394,7 +4445,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState }); // Override music. - if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; + if (audioInstTrack != null) + { + FlxG.sound.music = audioInstTrack; + } if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; this.persistentUpdate = false; @@ -4523,6 +4577,23 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent); } + function onSongLengthChanged():Void + { + if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; + if (gridPlayheadScrollArea != null) + { + gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); + gridPlayheadScrollArea.updateHitbox(); + } + + scrollPositionInPixels = 0; + playheadPositionInPixels = 0; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + noteDisplayDirty = true; + moveSongToScrollPosition(); + } + /** * CHART DATA FUNCTIONS */ @@ -4674,11 +4745,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Update the songPosition in the audio tracks. if (audioInstTrack != null) { - audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; + audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instrumentalOffset; // Update the songPosition in the Conductor. Conductor.update(audioInstTrack.time); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time; } - if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; // We need to update the note sprites because we changed the scroll position. noteDisplayDirty = true; @@ -4738,6 +4809,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState moveSongToScrollPosition(); + fadeInWelcomeMusic(7, 10); + // Reapply the volume. var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0; var vocalTargetVolume:Float = menubarItemVolumeVocals.value ?? 1.0; @@ -5034,15 +5107,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (audioInstTrack == null) return; - if (audioInstTrack.playing) + if (audioInstTrack.isPlaying) { - fadeInWelcomeMusic(7, 10); + // Pause stopAudioPlayback(); + fadeInWelcomeMusic(7, 10); } else { - stopWelcomeMusic(); + // Play startAudioPlayback(); + stopWelcomeMusic(); } } @@ -5055,29 +5130,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (audioInstTrack != null) audioInstTrack.pause(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); }; - - songLengthInMs = audioInstTrack.length; - - if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; - if (gridPlayheadScrollArea != null) - { - gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); - gridPlayheadScrollArea.updateHitbox(); - } } else { - trace('[WARN] Instrumental track was null!'); + trace('ERROR: Instrumental track is null!'); } - // Pretty much everything is going to need to be reset. - scrollPositionInPixels = 0; - playheadPositionInPixels = 0; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - noteDisplayDirty = true; + state.songLengthInMs = state.audioInstTrack?.length ?? 1000.0 + Conductor.instrumentalOffset; + + // Many things get reset when song length changes. healthIconsDirty = true; - moveSongToScrollPosition(); } /** diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 18c796cf7..2ba7ca3be 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -140,12 +140,14 @@ class ChartEditorAudioHandler { if (instId == '') instId = 'default'; var instTrackData:Null = state.audioInstTrackData.get(instId); - var instTrack:Null = SoundUtil.buildFlxSoundFromBytes(instTrackData); + var instTrack:Null = SoundUtil.buildSoundFromBytes(instTrackData); if (instTrack == null) return false; stopExistingInstrumental(state); state.audioInstTrack = instTrack; state.postLoadInstrumental(); + // Workaround for a bug where FlxG.sound.music.update() was being called twice. + FlxG.sound.list.remove(instTrack); return true; } @@ -176,17 +178,21 @@ class ChartEditorAudioHandler { case BF: state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); + state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); return true; case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); + state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); return true; case OTHER: state.audioVocalTrackGroup.add(vocalTrack); + // TODO: Add offset for other characters. return true; default: // Do nothing. } } + return false; } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 0e7ba374c..46b4b5dc4 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -673,6 +673,7 @@ class ChartEditorDialogHandler state.songMetadata.set(targetVariation, newSongMetadata); + Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); state.difficultySelectDirty = true; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 23c864a07..1726fe4a2 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -115,6 +115,7 @@ class ChartEditorImportExportHandler state.songChartData = newSongChartData; Conductor.forceBPM(null); // Disable the forced BPM. + Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); state.notePreviewDirty = true; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index 418f57464..933a7219b 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -620,6 +620,27 @@ class ChartEditorToolboxHandler }; inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; + var inputOffsetInst:Null = toolbox.findComponent('inputOffsetInst', NumberStepper); + if (inputOffsetInst == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputOffsetInst component.'; + inputOffsetInst.onChange = function(event:UIEvent) { + if (event.value == null) return; + + state.currentInstrumentalOffset = event.value; + Conductor.instrumentalOffset = event.value; + // Update song length. + state.songLengthInMs = state.audioInstTrack?.length ?? 1000.0 + Conductor.instrumentalOffset; + }; + inputOffsetInst.value = state.currentInstrumentalOffset; + + var inputOffsetVocal:Null = toolbox.findComponent('inputOffsetVocal', NumberStepper); + if (inputOffsetVocal == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputOffsetVocal component.'; + inputOffsetVocal.onChange = function(event:UIEvent) { + if (event.value == null) return; + + state.currentSongMetadata.offsets.setVocalOffset(state.currentSongMetadata.playData.characters.player, event.value); + }; + inputOffsetVocal.value = state.currentSongMetadata.offsets.getVocalOffset(state.currentSongMetadata.playData.characters.player); + var labelScrollSpeed:Null