From 956277ff4e8493daf0acd357cf740defd7d70131 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sun, 2 Jul 2023 15:33:34 -0400 Subject: [PATCH] Updates and fixes to Conductor for use with changing BPMs. --- source/funkin/Conductor.hx | 329 +++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 174 deletions(-) diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 7f7e2b356..5af454e34 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -1,23 +1,18 @@ package funkin; -import funkin.play.song.SongData.SongTimeChange; +import funkin.util.Constants; import flixel.util.FlxSignal; +import flixel.math.FlxMath; +import funkin.SongLoad.SwagSong; import funkin.play.song.Song.SongDifficulty; - -typedef BPMChangeEvent = -{ - var stepTime:Int; - var songTime:Float; - var bpm:Float; -} +import funkin.play.song.SongData.SongTimeChange; /** - * A global source of truth for timing information. + * A core class which handles musical timing throughout the game, + * both in gameplay and in menus. */ class Conductor { - static final STEPS_PER_BEAT:Int = 4; - // onBeatHit is called every quarter note // onStepHit is called every sixteenth note // 4/4 = 4 beats per measure = 16 steps per measure @@ -33,138 +28,6 @@ class Conductor // 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second // 7/8 = 3.5 beats per measure = 14 steps per measure - /** - * The current position in the song in milliseconds. - * Updated every frame based on the audio position. - */ - public static var songPosition:Float; - - /** - * Beats per minute of the current song at the current time. - */ - public static var bpm(get, null):Float; - - static function get_bpm():Float - { - if (bpmOverride != null) return bpmOverride; - - if (currentTimeChange == null) return 100; - - return currentTimeChange.bpm; - } - - static var bpmOverride:Null = null; - - /** - * Current position in the song, in whole measures. - */ - public static var currentMeasure(default, null):Int; - - /** - * Current position in the song, in whole beats. - **/ - public static var currentBeat(default, null):Int; - - /** - * Current position in the song, in whole steps. - */ - public static var currentStep(default, null):Int; - - /** - * Current position in the song, in steps and fractions of a step. - */ - public static var currentStepTime(default, null):Float; - - /** - * Duration of a measure in milliseconds. Calculated based on bpm. - */ - public static var measureLengthMs(get, null):Float; - - static function get_measureLengthMs():Float - { - return beatLengthMs * timeSignatureNumerator; - } - - /** - * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm. - */ - public static var beatLengthMs(get, null):Float; - - static function get_beatLengthMs():Float - { - // Tied directly to BPM. - return ((60 / bpm) * 1000); - } - - /** - * Duration of a step (sixteenth) in milliseconds. Calculated based on bpm. - */ - public static var stepLengthMs(get, null):Float; - - static function get_stepLengthMs():Float - { - return beatLengthMs / STEPS_PER_BEAT; - } - - /** - * The numerator of the current time signature (number of notes in a measure) - */ - public static var timeSignatureNumerator(get, null):Int; - - static function get_timeSignatureNumerator():Int - { - if (currentTimeChange == null) return 4; - - return currentTimeChange.timeSignatureNum; - } - - /** - * The numerator of the current time signature (length of notes in a measure) - */ - public static var timeSignatureDenominator(get, null):Int; - - static function get_timeSignatureDenominator():Int - { - if (currentTimeChange == null) return 4; - - return currentTimeChange.timeSignatureDen; - } - - public static var offset:Float = 0; - - // TODO: What's the difference between visualOffset and audioOffset? - public static var visualOffset:Float = 0; - public static var audioOffset:Float = 0; - - // - // Signals - // - - /** - * Signal that is dispatched every measure. - * At 120 BPM 4/4, this is dispatched every 2 seconds. - * At 120 BPM 3/4, this is dispatched every 1.5 seconds. - */ - public static var measureHit(default, null):FlxSignal = new FlxSignal(); - - /** - * Signal that is dispatched every beat. - * At 120 BPM 4/4, this is dispatched every 0.5 seconds. - * At 120 BPM 3/4, this is dispatched every 0.5 seconds. - */ - public static var beatHit(default, null):FlxSignal = new FlxSignal(); - - /** - * Signal that is dispatched when a step is hit. - * At 120 BPM 4/4, this is dispatched every 0.125 seconds. - * At 120 BPM 3/4, this is dispatched every 0.125 seconds. - */ - public static var stepHit(default, null):FlxSignal = new FlxSignal(); - - // - // Internal Variables - // - /** * The list of time changes in the song. * There should be at least one time change (at the beginning of the song) to define the BPM. @@ -176,11 +39,104 @@ class Conductor */ static var currentTimeChange:SongTimeChange; - public static var lastSongPos:Float; + /** + * The current position in the song in milliseconds. + * Updated every frame based on the audio position. + */ + public static var songPosition:Float = 0; /** - * The number of beats (whole notes) in a measure. + * Beats per minute of the current song at the current time. */ + public static var bpm(get, null):Float; + + static function get_bpm():Float + { + if (bpmOverride != null) return bpmOverride; + + if (currentTimeChange == null) return Constants.DEFAULT_BPM; + + return currentTimeChange.bpm; + } + + /** + * The current value set by `forceBPM`. + * If false, BPM is determined by time changes. + */ + static var bpmOverride:Null = null; + + /** + * Duration of a measure in milliseconds. Calculated based on bpm. + */ + public static var measureLengthMs(get, null):Float; + + static function get_measureLengthMs():Float + { + return crochet * timeSignatureNumerator; + } + + /** + * Duration of a beat in milliseconds. Calculated based on bpm. + */ + public static var crochet(get, null):Float; + + static function get_crochet():Float + { + return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC); + } + + /** + * Duration of a step (quarter) in milliseconds. Calculated based on bpm. + */ + public static var stepCrochet(get, null):Float; + + static function get_stepCrochet():Float + { + return crochet / timeSignatureNumerator; + } + + public static var timeSignatureNumerator(get, null):Int; + + static function get_timeSignatureNumerator():Int + { + if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM; + + return currentTimeChange.timeSignatureNum; + } + + public static var timeSignatureDenominator(get, null):Int; + + static function get_timeSignatureDenominator():Int + { + if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN; + + return currentTimeChange.timeSignatureDen; + } + + /** + * Current position in the song, in beats. + **/ + public static var currentBeat(default, null):Int; + + /** + * Current position in the song, in steps. + */ + public static var currentStep(default, null):Int; + + /** + * Current position in the song, in steps and fractions of a step. + */ + public static var currentStepTime(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; + public static var visualOffset:Float = 0; + public static var audioOffset:Float = 0; + public static var offset:Float = 0; + + // TODO: Add code to update this. public static var beatsPerMeasure(get, null):Int; static function get_beatsPerMeasure():Int @@ -188,17 +144,16 @@ class Conductor return timeSignatureNumerator; } - /** - * The number of steps (quarter-notes) in a measure. - */ public static var stepsPerMeasure(get, null):Int; static function get_stepsPerMeasure():Int { - // This is always 4, b - return timeSignatureNumerator * 4; + // Is this always x4? + return timeSignatureNumerator * Constants.STEPS_PER_BEAT; } + function new() {} + /** * Forcibly defines the current BPM of the song. * Useful for things like the chart editor that need to manipulate BPM in real time. @@ -208,16 +163,11 @@ class Conductor * WARNING: Avoid this for things like setting the BPM of the title screen music, * you should have a metadata file for it instead. */ - public static function forceBPM(?bpm:Float = null):Void + public static function forceBPM(?bpm:Float = null) { - if (bpm != null) - { - trace('[CONDUCTOR] Forcing BPM to ' + bpm); - } + if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm); else - { trace('[CONDUCTOR] Resetting BPM to default'); - } Conductor.bpmOverride = bpm; } @@ -228,13 +178,12 @@ class Conductor * @param songPosition The current position in the song in milliseconds. * Leave blank to use the FlxG.sound.music position. */ - public static function update(songPosition:Float = null):Void + public static function update(songPosition:Float = null) { if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0; - var oldMeasure:Int = currentMeasure; - var oldBeat:Int = currentBeat; - var oldStep:Int = currentStep; + var oldBeat = currentBeat; + var oldStep = currentStep; Conductor.songPosition = songPosition; @@ -252,14 +201,15 @@ class Conductor } else if (currentTimeChange != null) { - currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs; + // roundDecimal prevents representing 8 as 7.9999999 + currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet, 6); currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentStep / 4); } else { // Assume a constant BPM equal to the forced value. - currentStepTime = (songPosition / stepLengthMs); + currentStepTime = (songPosition / stepCrochet); currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentStep / 4); } @@ -274,40 +224,61 @@ class Conductor { beatHit.dispatch(); } - - if (currentMeasure != oldMeasure) - { - measureHit.dispatch(); - } } - public static function mapTimeChanges(songTimeChanges:Array):Void + public static function mapTimeChanges(songTimeChanges:Array) { timeChanges = []; for (currentTimeChange in songTimeChanges) { + // TODO: Maybe handle this different? + // Do we care about BPM at negative timestamps? + // Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`. + if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0; + + if (currentTimeChange.beatTime == null) + { + if (currentTimeChange.timeStamp <= 0.0) + { + currentTimeChange.beatTime = 0.0; + } + else + { + // Calculate the beat time of this timestamp. + currentTimeChange.beatTime = 0.0; + + if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0) + { + var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1]; + currentTimeChange.beatTime = prevTimeChange.beatTime + + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC); + } + } + } + timeChanges.push(currentTimeChange); } trace('Done mapping time changes: ' + timeChanges); - // Done. + // Update currentStepTime + Conductor.update(Conductor.songPosition); } /** * Given a time in milliseconds, return a time in steps. */ - public static function getTimeInSteps(ms:Float):Int + public static function getTimeInSteps(ms:Float):Float { if (timeChanges.length == 0) { // Assume a constant BPM equal to the forced value. - return Math.floor(ms / stepLengthMs); + return Math.floor(ms / stepCrochet); } else { - var resultStep:Int = 0; + var resultStep:Float = 0; var lastTimeChange:SongTimeChange = timeChanges[0]; for (timeChange in timeChanges) @@ -324,9 +295,19 @@ class Conductor } } - resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepLengthMs); + resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet); return resultStep; } } + + public static function reset():Void + { + beatHit.removeAll(); + stepHit.removeAll(); + + mapTimeChanges([]); + forceBPM(null); + update(0); + } }