mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-23 16:17:53 -05:00
528 lines
16 KiB
Haxe
528 lines
16 KiB
Haxe
package funkin;
|
|
|
|
import funkin.util.Constants;
|
|
import flixel.util.FlxSignal;
|
|
import flixel.math.FlxMath;
|
|
import funkin.data.song.SongData.SongTimeChange;
|
|
import funkin.data.song.SongDataUtils;
|
|
|
|
/**
|
|
* A core class which handles musical timing throughout the game,
|
|
* both in gameplay and in menus.
|
|
*/
|
|
@:nullSafety
|
|
class Conductor
|
|
{
|
|
// onBeatHit is called every quarter note
|
|
// onStepHit is called every sixteenth note
|
|
// 4/4 = 4 beats per measure = 16 steps per measure
|
|
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
|
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
|
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
|
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
|
// 3/4 = 3 beats per measure = 12 steps per measure
|
|
// (IDENTICAL TO 4/4 but shorter measure length)
|
|
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
|
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
|
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
|
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
|
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
|
|
|
/**
|
|
* The current instance of the Conductor.
|
|
* If one doesn't currently exist, a new one will be created.
|
|
*
|
|
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
|
|
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
|
|
*/
|
|
public static var instance:Conductor = new Conductor();
|
|
|
|
/**
|
|
* Signal fired when the current Conductor instance advances to a new measure.
|
|
*/
|
|
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
|
|
|
/**
|
|
* Signal fired when the current Conductor instance advances to a new beat.
|
|
*/
|
|
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
|
|
|
/**
|
|
* Signal fired when the current Conductor instance advances to a new step.
|
|
*/
|
|
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
var timeChanges:Array<SongTimeChange> = [];
|
|
|
|
/**
|
|
* The most recent time change for the current song position.
|
|
*/
|
|
public var currentTimeChange(default, null):Null<SongTimeChange>;
|
|
|
|
/**
|
|
* The current position in the song in milliseconds.
|
|
* Update this every frame based on the audio position using `Conductor.instance.update()`.
|
|
*/
|
|
public var songPosition(default, null):Float = 0;
|
|
|
|
/**
|
|
* Beats per minute of the current song at the current time.
|
|
*/
|
|
public var bpm(get, never):Float;
|
|
|
|
function get_bpm():Float
|
|
{
|
|
if (bpmOverride != null) return bpmOverride;
|
|
|
|
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
|
|
|
|
return currentTimeChange.bpm;
|
|
}
|
|
|
|
/**
|
|
* Beats per minute of the current song at the start time.
|
|
*/
|
|
public var startingBPM(get, never):Float;
|
|
|
|
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.
|
|
*/
|
|
var bpmOverride:Null<Float> = null;
|
|
|
|
/**
|
|
* Duration of a measure in milliseconds. Calculated based on bpm.
|
|
*/
|
|
public var measureLengthMs(get, never):Float;
|
|
|
|
function get_measureLengthMs():Float
|
|
{
|
|
return beatLengthMs * timeSignatureNumerator;
|
|
}
|
|
|
|
/**
|
|
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
|
*/
|
|
public var beatLengthMs(get, never):Float;
|
|
|
|
function get_beatLengthMs():Float
|
|
{
|
|
// Tied directly to BPM.
|
|
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
|
}
|
|
|
|
/**
|
|
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
|
|
*/
|
|
public var stepLengthMs(get, never):Float;
|
|
|
|
function get_stepLengthMs():Float
|
|
{
|
|
return beatLengthMs / timeSignatureNumerator;
|
|
}
|
|
|
|
/**
|
|
* The numerator for the current time signature (the `3` in `3/4`).
|
|
*/
|
|
public var timeSignatureNumerator(get, never):Int;
|
|
|
|
function get_timeSignatureNumerator():Int
|
|
{
|
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
|
|
|
return currentTimeChange.timeSignatureNum;
|
|
}
|
|
|
|
/**
|
|
* The denominator for the current time signature (the `4` in `3/4`).
|
|
*/
|
|
public var timeSignatureDenominator(get, never):Int;
|
|
|
|
function get_timeSignatureDenominator():Int
|
|
{
|
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
|
|
|
return currentTimeChange.timeSignatureDen;
|
|
}
|
|
|
|
/**
|
|
* Current position in the song, in measures.
|
|
*/
|
|
public var currentMeasure(default, null):Int = 0;
|
|
|
|
/**
|
|
* Current position in the song, in beats.
|
|
*/
|
|
public var currentBeat(default, null):Int = 0;
|
|
|
|
/**
|
|
* Current position in the song, in steps.
|
|
*/
|
|
public var currentStep(default, null):Int = 0;
|
|
|
|
/**
|
|
* Current position in the song, in measures and fractions of a measure.
|
|
*/
|
|
public var currentMeasureTime(default, null):Float = 0;
|
|
|
|
/**
|
|
* Current position in the song, in beats and fractions of a measure.
|
|
*/
|
|
public var currentBeatTime(default, null):Float = 0;
|
|
|
|
/**
|
|
* Current position in the song, in steps and fractions of a step.
|
|
*/
|
|
public var currentStepTime(default, null):Float = 0;
|
|
|
|
/**
|
|
* An offset tied to the current chart file to compensate for a delay in the instrumental.
|
|
*/
|
|
public var instrumentalOffset:Float = 0;
|
|
|
|
/**
|
|
* The instrumental offset, in terms of steps.
|
|
*/
|
|
public var instrumentalOffsetSteps(get, never):Float;
|
|
|
|
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.
|
|
*/
|
|
public var formatOffset:Float = 0;
|
|
|
|
/**
|
|
* An offset set by the user to compensate for input lag.
|
|
*/
|
|
public var inputOffset:Float = 0;
|
|
|
|
/**
|
|
* The number of beats in a measure. May be fractional depending on the time signature.
|
|
*/
|
|
public var beatsPerMeasure(get, never):Float;
|
|
|
|
function get_beatsPerMeasure():Float
|
|
{
|
|
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
|
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
|
}
|
|
|
|
/**
|
|
* The number of steps in a measure.
|
|
* TODO: I don't think this can be fractional?
|
|
*/
|
|
public var stepsPerMeasure(get, never):Int;
|
|
|
|
function get_stepsPerMeasure():Int
|
|
{
|
|
// TODO: Is this always an integer?
|
|
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
|
}
|
|
|
|
public 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.
|
|
*
|
|
* Set to null to reset to the BPM defined by the timeChanges.
|
|
*
|
|
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
|
* you should have a metadata file for it instead.
|
|
*/
|
|
public function forceBPM(?bpm:Float):Void
|
|
{
|
|
if (bpm != null)
|
|
{
|
|
trace('[CONDUCTOR] Forcing BPM to ${bpm}');
|
|
}
|
|
else
|
|
{
|
|
trace('[CONDUCTOR] Resetting BPM to default');
|
|
}
|
|
|
|
this.bpmOverride = bpm;
|
|
}
|
|
|
|
/**
|
|
* Update the conductor with the current song position.
|
|
* BPM, current step, etc. will be re-calculated based on the song position.
|
|
*
|
|
* @param songPosition The current position in the song in milliseconds.
|
|
* Leave blank to use the FlxG.sound.music position.
|
|
*/
|
|
public function update(?songPos:Float):Void
|
|
{
|
|
if (songPos == null)
|
|
{
|
|
// Take into account instrumental and file format song offsets.
|
|
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
|
|
}
|
|
|
|
var oldMeasure:Float = this.currentMeasure;
|
|
var oldBeat:Float = this.currentBeat;
|
|
var oldStep:Float = this.currentStep;
|
|
|
|
// Set the song position we are at (for purposes of calculating note positions, etc).
|
|
this.songPosition = songPos;
|
|
|
|
currentTimeChange = timeChanges[0];
|
|
if (this.songPosition > 0.0)
|
|
{
|
|
for (i in 0...timeChanges.length)
|
|
{
|
|
if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
|
|
|
if (this.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 && this.songPosition > 0.0)
|
|
{
|
|
// roundDecimal prevents representing 8 as 7.9999999
|
|
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
|
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
|
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
|
this.currentStep = Math.floor(currentStepTime);
|
|
this.currentBeat = Math.floor(currentBeatTime);
|
|
this.currentMeasure = Math.floor(currentMeasureTime);
|
|
}
|
|
else
|
|
{
|
|
// Assume a constant BPM equal to the forced value.
|
|
this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
|
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
|
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
|
this.currentStep = Math.floor(currentStepTime);
|
|
this.currentBeat = Math.floor(currentBeatTime);
|
|
this.currentMeasure = Math.floor(currentMeasureTime);
|
|
}
|
|
|
|
// Only fire the signal if we are THE Conductor.
|
|
if (this == Conductor.instance)
|
|
{
|
|
// FlxSignals are really cool.
|
|
if (currentStep != oldStep)
|
|
{
|
|
Conductor.stepHit.dispatch();
|
|
}
|
|
|
|
if (currentBeat != oldBeat)
|
|
{
|
|
Conductor.beatHit.dispatch();
|
|
}
|
|
|
|
if (currentMeasure != oldMeasure)
|
|
{
|
|
Conductor.measureHit.dispatch();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply the `SongTimeChange` data from the song metadata to this Conductor.
|
|
* @param songTimeChanges The SongTimeChanges.
|
|
*/
|
|
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
|
|
{
|
|
timeChanges = [];
|
|
|
|
// Sort in place just in case it's out of order.
|
|
SongDataUtils.sortTimeChanges(songTimeChanges);
|
|
|
|
for (songTimeChange 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 (songTimeChange.timeStamp < 0.0) songTimeChange.timeStamp = 0.0;
|
|
|
|
if (songTimeChange.timeStamp <= 0.0)
|
|
{
|
|
songTimeChange.beatTime = 0.0;
|
|
}
|
|
else
|
|
{
|
|
// Calculate the beat time of this timestamp.
|
|
songTimeChange.beatTime = 0.0;
|
|
|
|
if (songTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
|
{
|
|
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
|
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
|
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
|
4);
|
|
}
|
|
}
|
|
|
|
timeChanges.push(songTimeChange);
|
|
}
|
|
|
|
if (timeChanges.length > 0)
|
|
{
|
|
trace('Done mapping time changes: ${timeChanges}');
|
|
}
|
|
|
|
// Update currentStepTime
|
|
this.update(Conductor.instance.songPosition);
|
|
}
|
|
|
|
/**
|
|
* Given a time in milliseconds, return a time in steps.
|
|
* @param ms The time in milliseconds.
|
|
* @return The time in steps.
|
|
*/
|
|
public function getTimeInSteps(ms:Float):Float
|
|
{
|
|
if (timeChanges.length == 0)
|
|
{
|
|
// Assume a constant BPM equal to the forced value.
|
|
return Math.floor(ms / stepLengthMs);
|
|
}
|
|
else
|
|
{
|
|
var resultStep:Float = 0;
|
|
|
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
|
for (timeChange in timeChanges)
|
|
{
|
|
if (ms >= timeChange.timeStamp)
|
|
{
|
|
lastTimeChange = timeChange;
|
|
resultStep = lastTimeChange.beatTime * 4;
|
|
}
|
|
else
|
|
{
|
|
// This time change is after the requested time.
|
|
break;
|
|
}
|
|
}
|
|
|
|
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
|
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
|
|
resultStep += resultFractionalStep;
|
|
|
|
return resultStep;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a time in steps and fractional steps, return a time in milliseconds.
|
|
* @param stepTime The time in steps.
|
|
* @return The time in milliseconds.
|
|
*/
|
|
public function getStepTimeInMs(stepTime:Float):Float
|
|
{
|
|
if (timeChanges.length == 0)
|
|
{
|
|
// Assume a constant BPM equal to the forced value.
|
|
return stepTime * stepLengthMs;
|
|
}
|
|
else
|
|
{
|
|
var resultMs:Float = 0;
|
|
|
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
|
for (timeChange in timeChanges)
|
|
{
|
|
if (stepTime >= timeChange.beatTime * 4)
|
|
{
|
|
lastTimeChange = timeChange;
|
|
resultMs = lastTimeChange.timeStamp;
|
|
}
|
|
else
|
|
{
|
|
// This time change is after the requested time.
|
|
break;
|
|
}
|
|
}
|
|
|
|
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
|
resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
|
|
|
|
return resultMs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a time in beats and fractional beats, return a time in milliseconds.
|
|
* @param beatTime The time in beats.
|
|
* @return The time in milliseconds.
|
|
*/
|
|
public function getBeatTimeInMs(beatTime:Float):Float
|
|
{
|
|
if (timeChanges.length == 0)
|
|
{
|
|
// Assume a constant BPM equal to the forced value.
|
|
return beatTime * stepLengthMs * Constants.STEPS_PER_BEAT;
|
|
}
|
|
else
|
|
{
|
|
var resultMs:Float = 0;
|
|
|
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
|
for (timeChange in timeChanges)
|
|
{
|
|
if (beatTime >= timeChange.beatTime)
|
|
{
|
|
lastTimeChange = timeChange;
|
|
resultMs = lastTimeChange.timeStamp;
|
|
}
|
|
else
|
|
{
|
|
// This time change is after the requested time.
|
|
break;
|
|
}
|
|
}
|
|
|
|
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
|
resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
|
|
|
|
return resultMs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add variables of the current Conductor instance to the Flixel debugger.
|
|
*/
|
|
public static function watchQuick():Void
|
|
{
|
|
FlxG.watch.addQuick('songPosition', Conductor.instance.songPosition);
|
|
FlxG.watch.addQuick('bpm', Conductor.instance.bpm);
|
|
FlxG.watch.addQuick('currentMeasureTime', Conductor.instance.currentMeasureTime);
|
|
FlxG.watch.addQuick('currentBeatTime', Conductor.instance.currentBeatTime);
|
|
FlxG.watch.addQuick('currentStepTime', Conductor.instance.currentStepTime);
|
|
}
|
|
|
|
/**
|
|
* Reset the Conductor, replacing the current instance with a fresh one.
|
|
*/
|
|
public static function reset():Void
|
|
{
|
|
Conductor.instance = new Conductor();
|
|
}
|
|
}
|