Merge branch 'Time-Signature-Changes' into new-conductor

# Conflicts:
#	.gitignore
#	source/funkin/backend/FunkinSprite.hx
#	source/funkin/backend/chart/ChartData.hx
#	source/funkin/backend/chart/EventsData.hx
#	source/funkin/backend/system/Conductor.hx
#	source/funkin/backend/utils/CoolUtil.hx
#	source/funkin/editors/charter/Charter.hx
#	source/funkin/editors/charter/CharterEvent.hx
#	source/funkin/editors/charter/CharterEventScreen.hx
#	source/funkin/editors/charter/CharterMetaDataScreen.hx
#	source/funkin/editors/charter/SongCreationScreen.hx
#	source/funkin/game/PlayState.hx
This commit is contained in:
Ralty
2025-07-16 19:27:05 +07:00
32 changed files with 1288 additions and 311 deletions
-1
View File
@@ -145,7 +145,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
{
if (lastAnimContext != LOCK && beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
{
if(skipNegativeBeats && curBeat < 0) return;
// TODO: find a solution without countedBeat
var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)];
if (anim.name != null && anim.name != "null" && anim.name != "none")
+8 -4
View File
@@ -10,7 +10,7 @@ import openfl.Assets;
using StringTools;
class EventsData {
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
public static var defaultEventsParams:Map<String, Array<EventParamInfo>> = [
"HScript Call" => [
{name: "Function Name", type: TString, defValue: "myFunc"},
@@ -75,8 +75,10 @@ class EventsData {
{name: "Multiplicative?", type: TBool, defValue: true}
],
"Camera Modulo Change" => [
{name: "Modulo Interval (Beats)", type: TInt(1, 9999999, 1), defValue: 4},
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1}
{name: "Modulo Interval", type: TInt(1, 9999999, 1), defValue: 4},
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1},
{name: "Every Beat Type", type: TDropDown(['BEAT', 'MEASURE', 'STEP']), defValue: 'BEAT'},
{name: "Beat Offset", type: TFloat(-10, 10, 0.25, 2), defValue: 0}
],
"Camera Flash" => [
{name: "Reversed?", type: TBool, defValue: false},
@@ -84,7 +86,9 @@ class EventsData {
{name: "Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camHUD"}
],
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, null, 0.001, 3), defValue: 100}],
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}],
"Continuous BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}, {name: "Time (steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4}],
"Time Signature Change" => [{name: "Target Numerator", type: TFloat(1), defValue: 4}, {name: "Target Denominator", type: TFloat(1), defValue: 4}, {name: "Denominator is Steps Per Beat", type: TBool, defValue: false}],
"Scroll Speed Change" => [
{name: "Tween Speed?", type: TBool, defValue: true},
{name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.},
+16 -1
View File
@@ -55,6 +55,18 @@ class FNFLegacyParser {
continue; // Yoshi Engine charts crash fix
}
var newBeatsPerMeasure:Float = section.sectionBeats != null ? section.sectionBeats : data.beatsPerMeasure.getDefault(4); // Default to 4 if sectionBeats is null or undefined (oops :3)
if (newBeatsPerMeasure != beatsPerMeasure) {
beatsPerMeasure = newBeatsPerMeasure;
result.events.push({
time: curTime,
name: "Time Signature Change",
params: [newBeatsPerMeasure, 4]
});
}
if (camFocusedBF != (camFocusedBF = section.mustHitSection)) {
result.events.push({
time: curTime,
@@ -178,7 +190,8 @@ class FNFLegacyParser {
mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false,
bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm,
changeBPM: false,
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false,
sectionBeats: notes[section-1] != null ? notes[section-1].sectionBeats : chart.meta.beatsPerMeasure.getDefault(4)
};
var sectionEndTime:Float = Conductor.getTimeForStep(Conductor.getMeasureLength() * (section+1));
@@ -192,6 +205,8 @@ class FNFLegacyParser {
case "BPM Change":
baseSection.changeBPM = true;
baseSection.bpm = event.params[0];
case "Time Signature Change":
baseSection.sectionBeats = event.params[0];
}
}
notes[section] = baseSection;
+451 -170
View File
@@ -4,56 +4,58 @@ import flixel.FlxState;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.backend.chart.ChartData;
import funkin.backend.system.interfaces.IBeatReceiver;
import funkin.editors.charter.Charter;
typedef BPMChangeEvent =
{
var stepTime:Float;
var songTime:Float;
var bpm:Float;
enum BeatType {
BEAT;
MEASURE;
STEP;
}
class Conductor
@:structInit
class BPMChangeEvent
{
public var songTime:Float;
public var bpm:Float;
public var beatsPerMeasure:Float = 4;
public var stepsPerBeat:Int = 4;
public var endSongTime:Float = 0;
public var endStepTime:Float = 0;
public var continuous:Bool = false;
public var stepTime:Float;
public var beatTime:Float;
public var measureTime:Float;
}
final class Conductor
{
public static function getBeats(?every:BeatType, interval:Float, offset:Float = 0):Float {
final beat = switch(every) {
case MEASURE: curMeasureFloat;
case STEP: curStepFloat;
default: curBeatFloat;
}
if (interval <= 0) return beat - offset;
else return Math.floor((beat - offset) / interval) * interval;
}
/**
* FlxSignals
*/
public static var onMeasureHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBeatHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onStepHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBPMChange:FlxTypedSignal<Float->Void> = new FlxTypedSignal();
/**
* Current BPM
*/
public static var bpm:Float = 100;
/**
* Current Crochet (time per beat), in milliseconds.
*/
public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
/**
* Current StepCrochet (time per step), in milliseconds.
*/
public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
/**
* Number of beats per measure (top number in time signature). Defaults to 4.
*/
public static var beatsPerMeasure:Float = 4;
/**
* Number of steps per beat (bottom number in time signature). Defaults to 4.
*/
public static var stepsPerBeat:Int = 4;
public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
public static var onTimeSignatureChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
/**
* Current position of the song, in milliseconds.
*/
public static var songPosition(get, default):Float;
private static function get_songPosition() {
if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset);
if (songOffset != Options.songOffset) songOffset = Options.songOffset;
return songPosition - songOffset;
}
@@ -62,6 +64,88 @@ class Conductor
*/
public static var songOffset:Float = 0;
/**
* Current bpmChangeMap index
*/
public static var curChangeIndex:Int = 0;
/**
* Current bpmChangeMap
*/
public static var curChange(get, never):BPMChangeEvent;
private static var dummyChange:BPMChangeEvent = {bpm: 100, beatsPerMeasure: 4, stepsPerBeat: 4, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0};
private static function get_curChange()
return bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex];
/**
* Current BPM
*/
public static var bpm(get, never):Float;
private static function get_bpm()
return (curChangeIndex == 0 || bpmChangeMap.length == 0) ? startingBPM : getTimeWithIndexInBPM(songPosition, curChangeIndex);
/**
* Starting BPM
*/
public static var startingBPM(get, never):Float;
private static function get_startingBPM()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[0]).bpm;
/**
* Current Crochet (time per beat), in milliseconds.
* It should be crotchet but ehhh, now it's there for backward compatibility.
*/
public static var crochet(get, never):Float;
private static function get_crochet() return 15000 * stepsPerBeat / bpm;
/**
* Current StepCrochet (time per step), in milliseconds.
*/
public static var stepCrochet(get, never):Float;
private static function get_stepCrochet() return 15000 / bpm;
/**
* Number of beats per mesure (top number in time signature). Defaults to 4.
*/
public static var beatsPerMeasure(get, never):Float;
private static function get_beatsPerMeasure()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).beatsPerMeasure;
/**
* Number of steps per beat. Defaults to 4.
* Not a divisor number for time signature, it does the complete opposite.
* It's because CNE Conductor is based in sixteenth note instead of beat.
*/
public static var stepsPerBeat(get, never):Int;
private static function get_stepsPerBeat()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).stepsPerBeat;
/**
* How much value notes to divide for beat (bottom or divisor number in time signature).
* Only for a convinient way to access divisor instead of multiply by steps per beat.
*/
public static var denominator(get, never):Float;
private static function get_denominator() return FlxMath.roundDecimal(16 / stepsPerBeat, 2);
/**
* Last step from BPM Change
*/
public static var lastStepChange(get, never):Float;
private static function get_lastStepChange() return curChange.stepTime;
/**
* Last beat from BPM Change
*/
public static var lastBeatChange(get, never):Float;
private static function get_lastBeatChange() return curChange.beatTime;
/**
* Last measure from BPM Change
*/
public static var lastMeasureChange(get, never):Float;
private static function get_lastMeasureChange() return curChange.measureTime;
/**
* Current step
*/
@@ -77,7 +161,6 @@ class Conductor
*/
public static var curMeasure:Int = 0;
/**
* Current step, as a `Float` (ex: 4.94, instead of 4)
*/
@@ -100,53 +183,143 @@ class Conductor
/**
* Array of all BPM changes that have been mapped.
*/
public static var bpmChangeMap:Array<BPMChangeEvent> = [];
public static var bpmChangeMap:Array<BPMChangeEvent>;
/**
* Array of all events that have been rejected by the Conductor.
*/
public static var invalidEvents:Array<ChartEvent> = [];
private static var validEventNames:Array<String> = ["BPM Change", "Time Signature Change", "Continuous BPM Change"];
@:dox(hide) public function new() {}
public static function reset() {
songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0;
bpmChangeMap = [];
changeBPM(0);
curChangeIndex = 0;
changeBPM();
}
public static function changeBPM(bpm:Float = 100, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
bpmChangeMap = [{bpm: bpm, beatsPerMeasure: beatsPerMeasure, stepsPerBeat: stepsPerBeat, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0}];
public static function setupSong(SONG:ChartData) {
reset();
mapBPMChanges(SONG);
changeBPM(SONG.meta.bpm, cast SONG.meta.beatsPerMeasure.getDefault(4), cast SONG.meta.stepsPerBeat.getDefault(4));
}
private static function mapBPMChange(curChange:BPMChangeEvent, time:Float, bpm:Float):BPMChangeEvent {
var beatTime:Float, measureTime:Float, stepTime:Float;
stepTime = (curChange.continuous ? curChange.endStepTime : curChange.stepTime) + (time - (curChange.continuous ? curChange.endSongTime : curChange.songTime)) / (15000 / curChange.bpm);
beatTime = curChange.beatTime + (stepTime - curChange.stepTime) / curChange.stepsPerBeat;
measureTime = curChange.measureTime + (beatTime - curChange.beatTime) / curChange.beatsPerMeasure;
bpmChangeMap.push(curChange = {
songTime: time,
stepTime: stepTime,
beatTime: beatTime,
measureTime: measureTime,
bpm: bpm,
beatsPerMeasure: curChange.beatsPerMeasure,
stepsPerBeat: curChange.stepsPerBeat
});
return curChange;
}
/**
* Maps BPM changes from a song.
* @param song Song to map BPM changes from.
*/
public static function mapBPMChanges(song:ChartData)
{
bpmChangeMap = [
{
stepTime: 0,
songTime: 0,
bpm: song.meta.bpm
}
];
public static function mapBPMChanges(song:ChartData) {
var curChange:BPMChangeEvent = {
songTime: 0,
stepTime: 0,
beatTime: 0,
measureTime: 0,
bpm: song.meta.bpm,
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
};
curChangeIndex = 0;
bpmChangeMap = [curChange];
invalidEvents = [];
if (song.events == null) return;
var curBPM:Float = song.meta.bpm;
var songTime:Float = 0;
var stepTime:Float = 0;
// fix the sort first...
var events:Array<ChartEvent> = [for (e in song.events) if (e.params != null && validEventNames.contains(e.name)) e];
events.sort((a, b) -> {
if (MathUtil.equal(a.time, b.time)) return a.name == "Continuous BPM Change" ? 1 : -1;
return Std.int(a.time - b.time);
});
for(e in song.events) if (e.name == "BPM Change" && e.params != null && e.params[0] is Float) {
if (e.params[0] == curBPM) continue;
var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / 4);
stepTime += steps;
songTime = e.time;
curBPM = e.params[0];
for (e in events) curChange = mapEvent(e, curChange);
}
bpmChangeMap.push({
stepTime: stepTime,
songTime: songTime,
bpm: curBPM
private static function mapEvent(e:ChartEvent, curChange:BPMChangeEvent) {
var name = e.name, params = e.params, time = e.time;
if (curChange.continuous && MathUtil.lessThan(time, curChange.endSongTime)) { //ensure that you cannot place any conductor events during a continuous change
invalidEvents.push(e);
Logs.trace('Invalid Conductor event "${e.name}" at ${e.time} (Intersecting continuous change!)', WARNING);
return curChange;
}
if (name == "BPM Change" && params[0] is Float && curChange.bpm != params[0])
curChange = mapBPMChange(curChange, time, params[0]);
else if (name == "Time Signature Change") {
//if (beatsPerMeasure == curChange.beatsPerMeasure && stepsPerBeat == curChange.stepsPerBeat) continue;
/* TODO: make so time sigs doesnt stop the bpm change if its in the duration of bpm change */
if (curChange.songTime != time) curChange = mapBPMChange(curChange, time, curChange.bpm);
curChange.beatsPerMeasure = params[0];
if (params[2]) curChange.stepsPerBeat = params[1];
else curChange.stepsPerBeat = Math.floor(16 / params[1]); // convert from denominator to stepsPerBeat
curChange.stepTime = CoolUtil.floorInt(curChange.stepTime + .99998);
curChange.beatTime = CoolUtil.floorInt(curChange.beatTime + .99998);
curChange.measureTime = CoolUtil.floorInt(curChange.measureTime + .99998);
}
else if (name == "Continuous BPM Change") {
var prevBPM = curChange.bpm;
if (curChange.bpm == params[0]) {
invalidEvents.push(e);
return curChange; //DO NOT!!!!
}
curChange = mapBPMChange(curChange, time, params[0]);
curChange.endSongTime = time + (params[1]) / (curChange.bpm - prevBPM) * Math.log(curChange.bpm / prevBPM) * 15000;
curChange.endStepTime = curChange.stepTime + params[1];
curChange.continuous = true;
}
return curChange;
}
public static function mapCharterBPMChanges(song:ChartData) {
var curChange:BPMChangeEvent = {
songTime: 0,
stepTime: 0,
beatTime: 0,
measureTime: 0,
bpm: song.meta.bpm,
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
};
curChangeIndex = 0;
bpmChangeMap = [curChange];
invalidEvents = [];
for (event in Charter.instance.eventsGroup.members) {
event.events.sort((a, b) -> {
if (MathUtil.equal(a.time, b.time)) return a.name == "Continuous BPM Change" ? 1 : -1;
return 0;
});
var eventTime = Conductor.getStepsInTime(event.step);
for (e in event.events) {
if (!Math.isNaN(eventTime)) e.time = eventTime;
if (validEventNames.contains(e.name)) curChange = mapEvent(e, curChange);
}
}
}
@@ -164,12 +337,10 @@ class Conductor
return;
}
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) {
// update conductor
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset))
songPosition = lastSongPos;
} else {
else
songPosition += songOffset + elapsed * 1000;
}
}
private static function onStateSwitch(newState:FlxState) {
@@ -177,137 +348,247 @@ class Conductor
reset();
}
private static var __lastChange:BPMChangeEvent;
private static var __updateStep:Bool;
private static var __updateBeat:Bool;
private static var __updateMeasure:Bool;
private static function update() {
if (FlxG.state != null && FlxG.state is MusicBeatState) {
var state:MusicBeatState = cast FlxG.state;
if(state.cancelConductorUpdate)
return;
}
if (FlxG.state != null && FlxG.state is MusicBeatState && cast(FlxG.state, MusicBeatState).cancelConductorUpdate) return;
__updateSongPos(FlxG.elapsed);
if (bpm > 0) {
// updates curBeat and stuff
__lastChange = {
stepTime: 0,
songTime: 0,
bpm: 0
};
for (change in Conductor.bpmChangeMap)
{
if (Conductor.songPosition >= change.songTime)
__lastChange = change;
var oldStep = curStep, oldBeat = curBeat, oldMeasure = curMeasure, oldChangeIndex = curChangeIndex;
if ((curChangeIndex = getTimeInChangeIndex(songPosition, curChangeIndex)) > 0) {
var change = curChange;
curStepFloat = getTimeWithBPMInSteps(songPosition, curChangeIndex, getTimeWithIndexInBPM(songPosition, curChangeIndex));
curBeatFloat = change.beatTime + (curStepFloat - change.stepTime) / change.stepsPerBeat;
curMeasureFloat = change.measureTime + (curBeatFloat - change.beatTime) / change.beatsPerMeasure;
}
else
curMeasureFloat = (curBeatFloat = (curStepFloat = songPosition / stepCrochet) / stepsPerBeat) / beatsPerMeasure;
if (curChangeIndex != oldChangeIndex) {
var prev = bpmChangeMap[oldChangeIndex];
if (prev != null) {
if (beatsPerMeasure != prev.beatsPerMeasure || stepsPerBeat != prev.stepsPerBeat)
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
if (curChange.bpm != prev.bpm) onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
}
else {
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
}
}
if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) changeBPM(__lastChange.bpm);
if (__updateStep = (curStep != (curStep = CoolUtil.floorInt(curStepFloat)))) {
if (curStep > oldStep) for (i in oldStep...curStep) onStepHit.dispatch(i + 1);
else onStepHit.dispatch(curStep);
}
curStepFloat = __lastChange.stepTime + ((Conductor.songPosition - __lastChange.songTime) / Conductor.stepCrochet);
curBeatFloat = curStepFloat / stepsPerBeat;
curMeasureFloat = curBeatFloat / beatsPerMeasure;
if (__updateBeat = (curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat)))) {
if (curBeat > oldBeat) for (i in oldBeat...curBeat) onBeatHit.dispatch(i + 1);
else onBeatHit.dispatch(curBeat);
}
var oldStep = curStep;
var oldBeat = curBeat;
var oldMeasure = curMeasure;
if (curStep != (curStep = CoolUtil.floorInt(curStepFloat))) {
if (curStep < oldStep && oldStep - curStep < 2) return;
// updates step
__updateBeat = curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat));
__updateMeasure = __updateBeat && (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)));
if (__updateMeasure = (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)))) {
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) onMeasureHit.dispatch(i + 1);
else onMeasureHit.dispatch(curMeasure);
}
if (curStep > oldStep) {
for(i in oldStep...curStep) {
onStepHit.dispatch(i+1);
if (__updateStep || __updateBeat || __updateMeasure) {
var state = FlxG.state;
while (state != null) {
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
var st = cast(state, IBeatReceiver);
if (__updateStep) {
if (curStep > oldStep) for (i in oldStep...curStep) st.stepHit(i + 1);
else st.stepHit(curStep);
}
if (__updateBeat) {
if (curBeat > oldBeat) for (i in oldBeat...curBeat) st.beatHit(i + 1);
else st.beatHit(curBeat);
}
if (__updateMeasure) {
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) st.measureHit(i + 1);
else st.measureHit(curMeasure);
}
}
if (__updateBeat && curBeat > oldBeat) {
for(i in oldBeat...curBeat) {
onBeatHit.dispatch(i+1);
}
}
if (__updateMeasure && curMeasure > oldMeasure) {
for(i in oldMeasure...curMeasure) {
onMeasureHit.dispatch(i+1);
}
}
if (FlxG.state is IBeatReceiver) {
var state = FlxG.state;
while(state != null) {
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
var st:IBeatReceiver = cast state;
if (curStep > oldStep) {
for(i in oldStep...curStep) {
st.stepHit(i+1);
}
}
if (__updateBeat && curBeat > oldBeat) {
for(i in oldBeat...curBeat) {
st.beatHit(i+1);
}
}
if (__updateMeasure && curMeasure > oldMeasure) {
for(i in oldMeasure...curMeasure) {
st.measureHit(i+1);
}
}
}
state = state.subState;
}
}
state = state.subState;
}
}
}
public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
{
bpm = newBpm;
crochet = ((60 / bpm) * 1000);
stepCrochet = crochet / stepsPerBeat;
Conductor.beatsPerMeasure = beatsPerMeasure;
Conductor.stepsPerBeat = stepsPerBeat;
onBPMChange.dispatch(bpm);
public static function getTimeInChangeIndex(time:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].songTime > time) {
while (--index > 0) if (time > bpmChangeMap[index].songTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].songTime > time) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getTimeForStep(step:Float) {
var bpmChange:BPMChangeEvent = {
stepTime: 0,
songTime: 0,
bpm: bpm
};
for(change in bpmChangeMap)
if (change.stepTime < step && change.stepTime >= bpmChange.stepTime)
bpmChange = change;
return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
public static function getStepsInChangeIndex(stepTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].stepTime > stepTime) {
while (--index > 0) if (stepTime > bpmChangeMap[index].stepTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].stepTime > stepTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getStepForTime(time:Float) {
var bpmChange:BPMChangeEvent = {
stepTime: 0,
songTime: 0,
bpm: bpm
};
for(change in bpmChangeMap)
if (change.songTime < time && change.songTime >= bpmChange.songTime)
bpmChange = change;
return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
public static function getBeatsInChangeIndex(beatTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].beatTime > beatTime) {
while (--index > 0) if (beatTime > bpmChangeMap[index].beatTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].beatTime > beatTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getMeasuresInChangeIndex(measureTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].measureTime > measureTime) {
while (--index > 0) if (measureTime > bpmChangeMap[index].measureTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].measureTime > measureTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getTimeWithIndexInBPM(time:Float, index:Int):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && time < bpmChange.endSongTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (time <= bpmChange.songTime) return prevBPM;
var ratio = (time - bpmChange.songTime) / (bpmChange.endSongTime - bpmChange.songTime);
return Math.pow(prevBPM, 1 - ratio) * Math.pow(bpmChange.bpm, ratio);
}
return bpmChange.bpm;
}
public static function getStepsWithIndexInBPM(stepTime:Float, index:Int):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (stepTime <= bpmChange.stepTime) return prevBPM;
var endStepTime = bpmChange.stepTime + (bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM) / Math.log(bpmChange.bpm / prevBPM) / 15000;
if (stepTime < endStepTime) return FlxMath.remapToRange(stepTime, bpmChange.stepTime, endStepTime, prevBPM, bpmChange.bpm);
}
return bpmChange.bpm;
}
public static function getTimeInBPM(time:Float):Float {
if (bpmChangeMap.length == 0) return dummyChange.bpm;
return getTimeWithIndexInBPM(time, getTimeInChangeIndex(time));
}
public static function getTimeWithBPMInSteps(time:Float, index:Int, bpm:Float):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && time > bpmChange.songTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (time > bpmChange.endSongTime)
return bpmChange.stepTime + (((bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM))
/ Math.log(bpmChange.bpm / prevBPM) + (time - bpmChange.endSongTime) * bpm) / 15000;
else
return bpmChange.stepTime + (time - bpmChange.songTime) * (bpm - prevBPM) / Math.log(bpm / prevBPM) / 15000;
}
else {
return bpmChange.stepTime + (time - bpmChange.songTime) / (15000 / bpm);
}
}
public static function getTimeInBeats(time:Float, from:Int = 0):Float {
var index = getTimeInChangeIndex(time, from);
if (index == -1) return time / (60000 / dummyChange.bpm);
else if (index == 0) return time / (15000 / bpmChangeMap[index].bpm) / bpmChangeMap[index].stepsPerBeat;
else {
var change = bpmChangeMap[index];
return change.beatTime + (getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index)) - change.stepTime) / change.stepsPerBeat;
}
}
public static function getTimeInSteps(time:Float, from:Int = 0):Float {
var index = getTimeInChangeIndex(time, from);
return index < 1 ? time / (15000 / getTimeInBPM(time)) : getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index));
}
@:noCompletion
@:haxe.warning("-WDeprecated")
public static inline function getStepForTime(time:Float):Float return getTimeInSteps(time);
public static function getStepsWithBPMInTime(stepTime:Float, index:Int, bpm:Float):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && stepTime > bpmChange.stepTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
var time = bpmChange.songTime + (stepTime - bpmChange.stepTime) / (bpm - prevBPM) * Math.log(bpm / prevBPM) * 15000;
if (time > bpmChange.endSongTime)
return (15000 * (stepTime - bpmChange.stepTime) - ((bpmChange.endSongTime - bpmChange.songTime) * (bpm - prevBPM))
/ Math.log(bpm / prevBPM)) / bpm + bpmChange.endSongTime;
else
return time;
}
else {
return bpmChange.songTime + (stepTime - bpmChange.stepTime) * (15000 / bpm);
}
}
public static function getMeasuresInTime(measureTime:Float, from:Int = 0):Float {
var index = getMeasuresInChangeIndex(measureTime, from);
if (index == -1) return measureTime * (15000 / dummyChange.bpm);
else if (index == 0) return measureTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat * bpmChangeMap[index].beatsPerMeasure;
else {
var change = bpmChangeMap[index];
var stepTime = change.stepTime + (measureTime - change.measureTime) * change.stepsPerBeat * change.beatsPerMeasure;
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
}
public static function getBeatsInTime(beatTime:Float, from:Int = 0):Float {
var index = getBeatsInChangeIndex(beatTime, from);
if (index == -1) return beatTime * (15000 / dummyChange.bpm);
else if (index == 0) return beatTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat;
else {
var change = bpmChangeMap[index];
var stepTime = change.stepTime + (beatTime - change.beatTime) * change.stepsPerBeat;
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
}
public static function getStepsInTime(stepTime:Float, from:Int = 0):Float {
var index = getStepsInChangeIndex(stepTime, from);
return index < 1 ? stepTime * (15000 / bpmChangeMap[index].bpm) : getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
@:noCompletion
@:haxe.warning("-WDeprecated")
public static inline function getTimeForStep(steps:Float):Float return getStepsInTime(steps);
public static inline function getMeasureLength()
return stepsPerBeat * beatsPerMeasure;
public static inline function getMeasuresLength() {
if (FlxG.sound.music == null) return 0.0;
return getStepForTime(FlxG.sound.music.length) / getMeasureLength();
var length = FlxG.sound.music.length;
var index = getTimeInChangeIndex(length, bpmChangeMap.length - 1);
var change = bpmChangeMap[index];
return change.measureTime + (getTimeInBeats(length, index) - change.beatTime) / change.beatsPerMeasure;
}
}
}
@@ -12,7 +12,7 @@ class ConductorInfo extends FramerateCategory {
_text += '\n - ${Conductor.curStep} steps';
_text += '\n - ${Conductor.curMeasure} measures';
_text += '\nCurrent BPM: ${Conductor.bpm}';
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}';
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
this.text.text = _text;
super.__enterFrame(t);
+40 -1
View File
@@ -443,7 +443,7 @@ class CoolUtil
}
var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat));
} else
Conductor.changeBPM(DefaultBPM);
}
@@ -781,6 +781,15 @@ class CoolUtil
@:noUsing public static inline function maxInt(p1:Int, p2:Int)
return p1 < p2 ? p2 : p1;
/**
* Equivalent of `Math.min`, except doesn't require a Int -> Float -> Int conversion.
* @param p1
* @param p2
* @return return p1 > p2 ? p2 : p1
*/
@:noUsing public static inline function minInt(p1:Int, p2:Int)
return p1 > p2 ? p2 : p1;
/**
* Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion.
* @param e Value to get the floor of.
@@ -880,6 +889,36 @@ class CoolUtil
return file.file;
}
public static inline function bound(Value:Float, Min:Float, Max:Float):Float {
#if cpp
var _hx_tmp1:Float = Value;
var _hx_tmp2:Float = Min;
var _hx_tmp3:Float = Max;
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
#else
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
#end
}
public static inline function boundInt(Value:Int, Min:Int, Max:Int):Int {
#if cpp
var _hx_tmp1:Int = Value;
var _hx_tmp2:Int = Min;
var _hx_tmp3:Int = Max;
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
#else
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
#end
}
public static inline function boolToInt(b:Bool):Int {
#if cpp
return untyped __cpp__("(({0}) ? 1 : 0)", b);
#else
return b ? 1 : 0;
#end
}
/**
* Converts a string of "1..3,5,7..9,8..5" into an array of numbers like [1,2,3,5,7,8,9,8,7,6,5]
* @param input String to parse
+28
View File
@@ -0,0 +1,28 @@
package funkin.backend.utils;
final class MathUtil {
//Remind me to add the descriptions for the Wiki later - sen
public static inline function lessThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal < bVal - diff;
}
public static inline function lessThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal <= bVal + diff;
}
public static inline function greaterThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal > bVal + diff;
}
public static inline function greaterThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal >= bVal - diff;
}
public static inline function equal(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return Math.abs(aVal - bVal) <= diff;
}
public static inline function notEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return Math.abs(aVal - bVal) > diff;
}
}