mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-12-08 04:58:48 +00:00
Updates and fixes to Conductor for use with changing BPMs.
This commit is contained in:
parent
93f653969a
commit
956277ff4e
|
|
@ -1,23 +1,18 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
import funkin.play.song.SongData.SongTimeChange;
|
import funkin.util.Constants;
|
||||||
import flixel.util.FlxSignal;
|
import flixel.util.FlxSignal;
|
||||||
|
import flixel.math.FlxMath;
|
||||||
|
import funkin.SongLoad.SwagSong;
|
||||||
import funkin.play.song.Song.SongDifficulty;
|
import funkin.play.song.Song.SongDifficulty;
|
||||||
|
import funkin.play.song.SongData.SongTimeChange;
|
||||||
typedef BPMChangeEvent =
|
|
||||||
{
|
|
||||||
var stepTime:Int;
|
|
||||||
var songTime:Float;
|
|
||||||
var bpm:Float;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
class Conductor
|
||||||
{
|
{
|
||||||
static final STEPS_PER_BEAT:Int = 4;
|
|
||||||
|
|
||||||
// onBeatHit is called every quarter note
|
// onBeatHit is called every quarter note
|
||||||
// onStepHit is called every sixteenth note
|
// onStepHit is called every sixteenth note
|
||||||
// 4/4 = 4 beats per measure = 16 steps per measure
|
// 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
|
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
// 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<Float> = 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.
|
* 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.
|
* 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;
|
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<Float> = 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;
|
public static var beatsPerMeasure(get, null):Int;
|
||||||
|
|
||||||
static function get_beatsPerMeasure():Int
|
static function get_beatsPerMeasure():Int
|
||||||
|
|
@ -188,17 +144,16 @@ class Conductor
|
||||||
return timeSignatureNumerator;
|
return timeSignatureNumerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of steps (quarter-notes) in a measure.
|
|
||||||
*/
|
|
||||||
public static var stepsPerMeasure(get, null):Int;
|
public static var stepsPerMeasure(get, null):Int;
|
||||||
|
|
||||||
static function get_stepsPerMeasure():Int
|
static function get_stepsPerMeasure():Int
|
||||||
{
|
{
|
||||||
// This is always 4, b
|
// Is this always x4?
|
||||||
return timeSignatureNumerator * 4;
|
return timeSignatureNumerator * Constants.STEPS_PER_BEAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function new() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcibly defines the current BPM of the song.
|
* Forcibly defines the current BPM of the song.
|
||||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
* 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,
|
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||||
* you should have a metadata file for it instead.
|
* 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)
|
if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Resetting BPM to default');
|
trace('[CONDUCTOR] Resetting BPM to default');
|
||||||
}
|
|
||||||
Conductor.bpmOverride = bpm;
|
Conductor.bpmOverride = bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,13 +178,12 @@ class Conductor
|
||||||
* @param songPosition The current position in the song in milliseconds.
|
* @param songPosition The current position in the song in milliseconds.
|
||||||
* Leave blank to use the FlxG.sound.music position.
|
* 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;
|
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
|
||||||
|
|
||||||
var oldMeasure:Int = currentMeasure;
|
var oldBeat = currentBeat;
|
||||||
var oldBeat:Int = currentBeat;
|
var oldStep = currentStep;
|
||||||
var oldStep:Int = currentStep;
|
|
||||||
|
|
||||||
Conductor.songPosition = songPosition;
|
Conductor.songPosition = songPosition;
|
||||||
|
|
||||||
|
|
@ -252,14 +201,15 @@ class Conductor
|
||||||
}
|
}
|
||||||
else if (currentTimeChange != null)
|
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);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentStep / 4);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Assume a constant BPM equal to the forced value.
|
// Assume a constant BPM equal to the forced value.
|
||||||
currentStepTime = (songPosition / stepLengthMs);
|
currentStepTime = (songPosition / stepCrochet);
|
||||||
currentStep = Math.floor(currentStepTime);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentStep / 4);
|
||||||
}
|
}
|
||||||
|
|
@ -274,40 +224,61 @@ class Conductor
|
||||||
{
|
{
|
||||||
beatHit.dispatch();
|
beatHit.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMeasure != oldMeasure)
|
|
||||||
{
|
|
||||||
measureHit.dispatch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
|
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||||
{
|
{
|
||||||
timeChanges = [];
|
timeChanges = [];
|
||||||
|
|
||||||
for (currentTimeChange in songTimeChanges)
|
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);
|
timeChanges.push(currentTimeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace('Done mapping time changes: ' + timeChanges);
|
trace('Done mapping time changes: ' + timeChanges);
|
||||||
|
|
||||||
// Done.
|
// Update currentStepTime
|
||||||
|
Conductor.update(Conductor.songPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a time in milliseconds, return a time in steps.
|
* 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)
|
if (timeChanges.length == 0)
|
||||||
{
|
{
|
||||||
// Assume a constant BPM equal to the forced value.
|
// Assume a constant BPM equal to the forced value.
|
||||||
return Math.floor(ms / stepLengthMs);
|
return Math.floor(ms / stepCrochet);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var resultStep:Int = 0;
|
var resultStep:Float = 0;
|
||||||
|
|
||||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||||
for (timeChange in timeChanges)
|
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;
|
return resultStep;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function reset():Void
|
||||||
|
{
|
||||||
|
beatHit.removeAll();
|
||||||
|
stepHit.removeAll();
|
||||||
|
|
||||||
|
mapTimeChanges([]);
|
||||||
|
forceBPM(null);
|
||||||
|
update(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue