diff --git a/assets/fonts/timeSig.ttf b/assets/fonts/timeSig.ttf new file mode 100644 index 00000000..9729308f Binary files /dev/null and b/assets/fonts/timeSig.ttf differ diff --git a/assets/images/editors/charter/event-icons/Time Signature Change.png b/assets/images/editors/charter/event-icons/Time Signature Change.png new file mode 100644 index 00000000..f7cc46c7 Binary files /dev/null and b/assets/images/editors/charter/event-icons/Time Signature Change.png differ diff --git a/source/funkin/backend/chart/Chart.hx b/source/funkin/backend/chart/Chart.hx index bd13fd50..e2a22d40 100644 --- a/source/funkin/backend/chart/Chart.hx +++ b/source/funkin/backend/chart/Chart.hx @@ -129,6 +129,7 @@ class Chart { 1 => "Camera Movement", 2 => "BPM Change", 3 => "Alt Animation Toggle", + 4 => "Time Signature Change", ]; if (data.events == null) data.events = []; diff --git a/source/funkin/backend/chart/EventsData.hx b/source/funkin/backend/chart/EventsData.hx index d443218a..e0d17331 100644 --- a/source/funkin/backend/chart/EventsData.hx +++ b/source/funkin/backend/chart/EventsData.hx @@ -10,7 +10,7 @@ import funkin.backend.assets.Paths; using StringTools; class EventsData { - public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"]; + public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation", "Time Signature Change"]; public static var defaultEventsParams:Map> = [ "HScript Call" => [ {name: "Function Name", type: TString, defValue: "myFunc"}, @@ -49,6 +49,7 @@ class EventsData { ], "Alt Animation Toggle" => [{name: "Enable On Sing Poses", type: TBool, defValue: true}, {name: "Enable On Idle", type: TBool, defValue: true}, {name: "Strumline", type: TStrumLine, defValue: 0}], "Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}], + "Time Signature Change" => [{name: "Target Beat Count", type: TFloat(1), defValue: 4}, {name: "Target Step Count", type: TFloat(1), defValue: 4}], ]; public static var eventsList:Array = defaultEventsList.copy(); diff --git a/source/funkin/backend/chart/FNFLegacyParser.hx b/source/funkin/backend/chart/FNFLegacyParser.hx index ede54e18..d7560228 100644 --- a/source/funkin/backend/chart/FNFLegacyParser.hx +++ b/source/funkin/backend/chart/FNFLegacyParser.hx @@ -109,6 +109,16 @@ class FNFLegacyParser { }); } + if (section.sectionBeats != beatsPerMeasure) { + beatsPerMeasure = section.sectionBeats != null ? section.sectionBeats : data.beatsPerMeasure.getDefault(4); + + result.events.push({ + time: curTime, + name: "Time Signature Change", + params: [section.sectionBeats, 4] + }); + } + curTime += curCrochet * beatsPerMeasure; } } @@ -179,7 +189,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)); @@ -193,6 +204,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; diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx index bd5fca4c..4af8ab8a 100644 --- a/source/funkin/backend/system/Conductor.hx +++ b/source/funkin/backend/system/Conductor.hx @@ -10,6 +10,8 @@ typedef BPMChangeEvent = var stepTime:Float; var songTime:Float; var bpm:Float; + var beatsPerMeasure:Float; + var stepsPerBeat:Float; } class Conductor @@ -21,6 +23,8 @@ class Conductor public static var onBeatHit:FlxTypedSignalVoid> = new FlxTypedSignal(); public static var onStepHit:FlxTypedSignalVoid> = new FlxTypedSignal(); public static var onBPMChange:FlxTypedSignalVoid> = new FlxTypedSignal(); + public static var onBeatsPerMeasureChange:FlxTypedSignalVoid> = new FlxTypedSignal(); + public static var onStepsPerBeatChange:FlxTypedSignalVoid> = new FlxTypedSignal(); /** * Current BPM @@ -108,45 +112,76 @@ class Conductor songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0; bpmChangeMap = []; changeBPM(0); + changeTimeSignature(4, 4); } 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)); + changeBPM(SONG.meta.bpm); + changeTimeSignature(cast SONG.meta.beatsPerMeasure.getDefault(4), cast SONG.meta.stepsPerBeat.getDefault(4)); } /** * Maps BPM changes from a song. * @param song Song to map BPM changes from. */ - public static function mapBPMChanges(song:ChartData) - { + public static function mapBPMChanges(song:ChartData) { bpmChangeMap = [ { stepTime: 0, songTime: 0, - bpm: song.meta.bpm + bpm: song.meta.bpm, + beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4), + stepsPerBeat: song.meta.stepsPerBeat.getDefault(4) } ]; - + if (song.events == null) return; - + var curBPM:Float = song.meta.bpm; + var curBeatsPerMeasure:Float = song.meta.beatsPerMeasure.getDefault(4); + var curStepsPerBeat:Float = song.meta.stepsPerBeat.getDefault(4); var songTime:Float = 0; var stepTime:Float = 0; - - 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]; - - bpmChangeMap.push({ - stepTime: stepTime, - songTime: songTime, - bpm: curBPM - }); + + 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 / stepsPerBeat); + stepTime += steps; + songTime = e.time; + curBPM = e.params[0]; + + bpmChangeMap.push({ + stepTime: stepTime, + songTime: songTime, + bpm: curBPM, + beatsPerMeasure: curBeatsPerMeasure, // keep old beatsPerMeasure and stepsPerMeasure so shit doesnt break + stepsPerBeat: curStepsPerBeat + }); + } + + if (e.name == "Time Signature Change" && e.params != null) { + var newBeatsPerMeasure = e.params[0]; + var newStepsPerBeat = e.params[1]; + + if (newBeatsPerMeasure == curBeatsPerMeasure && newStepsPerBeat == curStepsPerBeat) continue; + + var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / stepsPerBeat); + stepTime += steps; + songTime = e.time; + + curBeatsPerMeasure = newBeatsPerMeasure; + curStepsPerBeat = newStepsPerBeat; + + bpmChangeMap.push({ + stepTime: stepTime, + songTime: songTime, + bpm: curBPM, // keep old bpm so shit doesnt break + beatsPerMeasure: curBeatsPerMeasure, + stepsPerBeat: curStepsPerBeat + }); + } } } @@ -184,22 +219,32 @@ class Conductor if (FlxG.state != null && FlxG.state is MusicBeatState && cast(FlxG.state, MusicBeatState).cancelConductorUpdate) return; __updateSongPos(FlxG.elapsed); - + if (bpm > 0) { - // updates curbeat and stuff + // Check for BPM change __lastChange = { stepTime: 0, songTime: 0, - bpm: 0 + bpm: 0, + beatsPerMeasure: beatsPerMeasure, + stepsPerBeat: stepsPerBeat }; - for (change in Conductor.bpmChangeMap) - { + + for (change in Conductor.bpmChangeMap) { if (Conductor.songPosition >= change.songTime) __lastChange = change; } + + // Change BPM if necessary + if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) { + changeBPM(__lastChange.bpm); + } - if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) changeBPM(__lastChange.bpm); - + // Check for time signature change + if (__lastChange.beatsPerMeasure != beatsPerMeasure || __lastChange.stepsPerBeat != stepsPerBeat) { + changeTimeSignature(__lastChange.beatsPerMeasure, __lastChange.stepsPerBeat); + } + curStepFloat = __lastChange.stepTime + ((Conductor.songPosition - __lastChange.songTime) / Conductor.stepCrochet); curBeatFloat = curStepFloat / stepsPerBeat; curMeasureFloat = curBeatFloat / beatsPerMeasure; @@ -270,34 +315,50 @@ class Conductor onBPMChange.dispatch(bpm); } + + public static function changeTimeSignature(newBeatsPerMeasure:Float, newStepsPerBeat:Float) { + beatsPerMeasure = newBeatsPerMeasure; + stepsPerBeat = newStepsPerBeat; + + crochet = ((60 / bpm) * 1000); + stepCrochet = crochet / stepsPerBeat; + + onBeatsPerMeasureChange.dispatch(beatsPerMeasure); + onStepsPerBeatChange.dispatch(stepsPerBeat); + } public static function getTimeForStep(step:Float) { var bpmChange:BPMChangeEvent = { stepTime: 0, songTime: 0, - bpm: bpm + bpm: bpm, + beatsPerMeasure: beatsPerMeasure, + stepsPerBeat: stepsPerBeat }; 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))); + return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000 / bpmChange.stepsPerBeat))); } public static function getStepForTime(time:Float) { var bpmChange:BPMChangeEvent = { stepTime: 0, songTime: 0, - bpm: bpm + bpm: bpm, + beatsPerMeasure: beatsPerMeasure, + stepsPerBeat: stepsPerBeat }; - + 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))); + + return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000 / bpmChange.stepsPerBeat))); } + public static inline function getMeasureLength() return stepsPerBeat * beatsPerMeasure; diff --git a/source/funkin/editors/charter/CharterEvent.hx b/source/funkin/editors/charter/CharterEvent.hx index 89a5d0d9..b624ee24 100644 --- a/source/funkin/editors/charter/CharterEvent.hx +++ b/source/funkin/editors/charter/CharterEvent.hx @@ -109,7 +109,7 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable { draggable = true; for (event in events) - if (event.name == "BPM Change") { + if (event.name == "BPM Change" || event.name == "Time Signature Change") { draggable = false; break; } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 4e125724..1806fee2 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -1388,6 +1388,7 @@ class PlayState extends MusicBeatState if (strumLines.members[event.params[0]] != null && strumLines.members[event.params[0]].characters != null) for (char in strumLines.members[event.params[0]].characters) if (char != null) char.playAnim(event.params[1], event.params[2], null); + case "Time Signature Change": // automatically handled by conductor case "Unknown": // nothing } }