Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.

Commit 2f6207b

Browse files
fixing beat and measure lines with time sig changes (#2)
1 parent 0e47728 commit 2f6207b

File tree

3 files changed

+203
-61
lines changed

3 files changed

+203
-61
lines changed

source/funkin/backend/system/Conductor.hx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ final class Conductor
192192
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
193193
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
194194
};
195+
curChangeIndex = 0;
195196
bpmChangeMap = [curChange];
196197
if (song.events == null) return;
197198

@@ -450,8 +451,19 @@ final class Conductor
450451
}
451452
}
452453

454+
public static function getMeasuresInTime(measureTime:Float, from:Int = 0):Float {
455+
var index = getMeasuresInChangeIndex(measureTime, from);
456+
if (index == -1) return measureTime * (60000 / 100 / 4);
457+
else if (index == 0) return measureTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat * bpmChangeMap[index].beatsPerMeasure;
458+
else {
459+
var change = bpmChangeMap[index];
460+
var stepTime = change.stepTime + (measureTime - change.measureTime) * change.stepsPerBeat * change.beatsPerMeasure;
461+
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
462+
}
463+
}
464+
453465
public static function getBeatsInTime(beatTime:Float, from:Int = 0):Float {
454-
var index = getStepsInChangeIndex(beatTime, from);
466+
var index = getBeatsInChangeIndex(beatTime, from);
455467
if (index == -1) return beatTime * (60000 / 100);
456468
else if (index == 0) return beatTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat;
457469
else {

source/funkin/editors/charter/Charter.hx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import flixel.input.keyboard.FlxKey;
1616
import flixel.sound.FlxSound;
1717
import flixel.math.FlxPoint;
1818
import funkin.editors.charter.CharterBackdropGroup.CharterBackdropDummy;
19+
import funkin.editors.charter.CharterBackdropGroup.CharterGridSeperatorBase;
1920
import funkin.backend.system.Conductor;
2021
import funkin.backend.chart.*;
2122
import funkin.backend.chart.ChartData;
@@ -622,6 +623,8 @@ class Charter extends UIState {
622623
gridBackdrops.bottomLimitY = __endStep * 40;
623624
eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2;
624625

626+
CharterGridSeperatorBase.lastConductorSprY = Math.NEGATIVE_INFINITY;
627+
625628
updateWaveforms();
626629
}
627630

@@ -1631,11 +1634,9 @@ class Charter extends UIState {
16311634
}
16321635
function _view_showeventSecSeparator(t) {
16331636
t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0;
1634-
eventsBackdrop.eventSecSeparator.visible = gridBackdrops.sectionsVisible = Options.charterShowSections;
16351637
}
16361638
function _view_showeventBeatSeparator(t) {
16371639
t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0;
1638-
eventsBackdrop.eventBeatSeparator.visible = gridBackdrops.beatsVisible = Options.charterShowBeats;
16391640
}
16401641
function _view_switchWaveformDetail(t) {
16411642
t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0;
@@ -1837,8 +1838,8 @@ class Charter extends UIState {
18371838
public function updateBPMEvents() {
18381839
buildEvents();
18391840

1840-
Conductor.mapBPMChanges(PlayState.SONG);
18411841
Conductor.changeBPM(PlayState.SONG.meta.bpm, cast PlayState.SONG.meta.beatsPerMeasure.getDefault(4), cast PlayState.SONG.meta.stepsPerBeat.getDefault(4));
1842+
Conductor.mapBPMChanges(PlayState.SONG);
18421843

18431844
refreshBPMSensitive();
18441845
}

source/funkin/editors/charter/CharterBackdropGroup.hx

Lines changed: 186 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
1212

1313
public var conductorSprY:Float = 0;
1414
public var bottomLimitY:Float = 0;
15-
public var sectionsVisible:Bool = true;
16-
public var beatsVisible:Bool = true;
1715

1816
// Just here so you can update display sprites all dat and above
1917
public var strumlinesAmount:Int = 0;
@@ -61,8 +59,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
6159

6260
grid.conductorFollowerSpr.y = conductorSprY;
6361
grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2;
64-
grid.sectionSeparator.visible = sectionsVisible;
65-
grid.beatSeparator.visible = beatsVisible;
6662

6763
grid.waveformSprite.shader = strumLine.waveformShader;
6864

@@ -119,8 +115,7 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
119115
public var waveformSprite:FlxSprite;
120116

121117
public var conductorFollowerSpr:FlxSprite;
122-
public var beatSeparator:FlxBackdrop;
123-
public var sectionSeparator:FlxBackdrop;
118+
public var beatSeparator:CharterGridSeperator;
124119

125120
public var notesGroup:FlxTypedGroup<CharterNote> = new FlxTypedGroup<CharterNote>();
126121
public var strumLine:CharterStrumline;
@@ -136,23 +131,14 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
136131
waveformSprite.updateHitbox();
137132
add(waveformSprite);
138133

139-
sectionSeparator = new FlxBackdrop(null, Y, 0, 0);
140-
sectionSeparator.y = -2;
141-
sectionSeparator.visible = Options.charterShowSections;
142-
143-
beatSeparator = new FlxBackdrop(null, Y, 0, 0);
144-
beatSeparator.y = -1;
145-
beatSeparator.visible = Options.charterShowBeats;
146-
147-
for(sep in [sectionSeparator, beatSeparator]) {
148-
sep.makeSolid(1, 1, -1);
149-
sep.alpha = 0.5;
150-
sep.scrollFactor.set(1, 1);
151-
sep.scale.set((4 * 40), sep == sectionSeparator ? 4 : 2);
152-
sep.updateHitbox();
153-
}
134+
beatSeparator = new CharterGridSeperator();
135+
beatSeparator.makeSolid(1, 1, -1);
136+
beatSeparator.alpha = 0.5;
137+
beatSeparator.scrollFactor.set(1, 1);
138+
beatSeparator.scale.set((4 * 40), 2);
139+
beatSeparator.updateHitbox();
154140
add(beatSeparator);
155-
add(sectionSeparator);
141+
156142
add(notesGroup);
157143

158144
bottomSeparator = new FlxSprite(0,-2);
@@ -200,15 +186,12 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
200186
alpha = strumLine.strumLine.visible ? 0.9 : 0.4;
201187
} else alpha = 0.9;
202188

203-
for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit,
189+
for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit,
204190
topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) {
205191
spr.x = x; if (spr != waveformSprite) spr.alpha = alpha;
206192
spr.cameras = this.cameras;
207193
}
208194

209-
sectionSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
210-
beatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
211-
212195
topLimit.scale.set(4 * 40, Math.ceil(FlxG.height / cameras[0].zoom));
213196
topLimit.updateHitbox();
214197
topLimit.y = -topLimit.height;
@@ -237,6 +220,159 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
237220
}
238221
}
239222

223+
class CharterGridSeperatorBase extends FlxSprite {
224+
225+
private static var minStep:Float = 0;
226+
private static var maxStep:Float = 0;
227+
228+
private static var minBeat:Float = 0;
229+
private static var maxBeat:Float = 0;
230+
231+
private static var minMeasure:Float = 0;
232+
private static var maxMeasure:Float = 0;
233+
234+
private static var lastMinBeat:Float = -1;
235+
private static var lastMaxBeat:Float = -1;
236+
237+
private static var lastMinMeasure:Float = -1;
238+
private static var lastMaxMeasure:Float = -1;
239+
240+
public static var lastConductorSprY:Float = Math.NEGATIVE_INFINITY;
241+
242+
private static var beatStepTimes:Array<Float> = [];
243+
private static var measureStepTimes:Array<Float> = [];
244+
private static var timeSignatureChangeGaps:Array<Float> = [];
245+
246+
private function recalculateBeats() {
247+
var conductorSprY = Charter.instance.gridBackdrops.conductorSprY;
248+
if (conductorSprY == lastConductorSprY) return;
249+
250+
var zoomOffset = ((FlxG.height * (1/cameras[0].zoom)) * 0.5);
251+
252+
minStep = (conductorSprY - zoomOffset)/40;
253+
maxStep = (conductorSprY + zoomOffset)/40;
254+
255+
var minTime:Float = Conductor.getStepsInTime(minStep);
256+
var maxTime:Float = Conductor.getStepsInTime(maxStep);
257+
258+
var minBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(minTime)];
259+
var maxBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(maxTime)];
260+
261+
minBeat = Conductor.getTimeInBeats(minTime);
262+
maxBeat = Conductor.getTimeInBeats(maxTime);
263+
264+
minMeasure = minBpmChange.measureTime + (minBeat - minBpmChange.beatTime) / minBpmChange.beatsPerMeasure;
265+
maxMeasure = maxBpmChange.measureTime + (maxBeat - maxBpmChange.beatTime) / maxBpmChange.beatsPerMeasure;
266+
267+
//cap out the beats/measures at the end of the song
268+
var endTime = Conductor.getStepsInTime(Charter.instance.__endStep);
269+
var endBeat = Conductor.getTimeInBeats(endTime);
270+
var endBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(endTime)];
271+
var endMeasure = endBpmChange.measureTime + (endBeat - endBpmChange.beatTime) / endBpmChange.beatsPerMeasure;
272+
273+
if (maxBeat > endBeat) maxBeat = endBeat;
274+
if (maxMeasure > endMeasure) maxMeasure = endMeasure;
275+
if (minMeasure < 0) minMeasure = 0;
276+
if (minBeat < 0) minBeat = 0;
277+
278+
//only calculate if needed
279+
if ((minBeat != lastMinBeat) || (maxBeat != lastMaxBeat) || (minMeasure != lastMinMeasure) || (maxMeasure != lastMaxMeasure) || lastConductorSprY == Math.NEGATIVE_INFINITY) {
280+
calculateTimeSignatureGaps();
281+
calculateStepTimes();
282+
lastMinBeat = minBeat;
283+
lastMaxBeat = maxBeat;
284+
lastMinMeasure = minMeasure;
285+
lastMaxMeasure = maxMeasure;
286+
}
287+
288+
lastConductorSprY = conductorSprY;
289+
}
290+
291+
private inline function calculateTimeSignatureGaps() {
292+
//for time signatures that start mid step
293+
timeSignatureChangeGaps.splice(0, timeSignatureChangeGaps.length);
294+
for (i => change in Conductor.bpmChangeMap) {
295+
if (change.stepTime >= minStep && change.stepTime <= maxStep) {
296+
//get step while ignoring the current change
297+
var index = CoolUtil.boundInt(i-1, 0, Conductor.bpmChangeMap.length - 1);
298+
var step = Conductor.getTimeWithBPMInSteps(change.songTime, index, Conductor.getTimeWithIndexInBPM(change.songTime, index));
299+
300+
if (Math.ceil(step) - step > 0) { //mid step change
301+
timeSignatureChangeGaps.push(step);
302+
}
303+
}
304+
}
305+
}
306+
307+
private inline function calculateStepTimes() {
308+
beatStepTimes.splice(0, beatStepTimes.length);
309+
for (i in Math.floor(minBeat)...Math.ceil(maxBeat)) {
310+
beatStepTimes.push(Conductor.getTimeInSteps(Conductor.getBeatsInTime(i)));
311+
}
312+
measureStepTimes.splice(0, measureStepTimes.length);
313+
for (i in Math.floor(minMeasure)...Math.ceil(maxMeasure)) {
314+
measureStepTimes.push(Conductor.getTimeInSteps(Conductor.getMeasuresInTime(i)));
315+
}
316+
}
317+
318+
override public function draw() {
319+
320+
//should only need to recalculate once per frame and will be shared across each instance
321+
recalculateBeats();
322+
323+
drawTimeSignatureChangeGaps();
324+
325+
if (Options.charterShowBeats) drawBeats();
326+
if (Options.charterShowSections) drawMeasures();
327+
}
328+
329+
private function drawBeats(offset:Float = 0.0) {
330+
for (i in beatStepTimes) {
331+
y = (i*40)+offset;
332+
super.draw();
333+
}
334+
}
335+
private function drawMeasures(offset:Float = 0.0) {
336+
for (i in measureStepTimes) {
337+
y = (i*40)+offset;
338+
super.draw();
339+
}
340+
}
341+
private function drawTimeSignatureChangeGaps() {
342+
if (timeSignatureChangeGaps.length == 0) return;
343+
var prevColor = color;
344+
var prevBlend = blend;
345+
346+
color = 0xFF888888;
347+
blend = MULTIPLY;
348+
349+
for (step in timeSignatureChangeGaps) {
350+
y = step*40;
351+
var diff = Math.ceil(step) - step;
352+
scale.y = diff*40;
353+
updateHitbox();
354+
355+
super.draw();
356+
}
357+
358+
color = prevColor;
359+
blend = prevBlend;
360+
}
361+
}
362+
363+
class CharterGridSeperator extends CharterGridSeperatorBase {
364+
override private function drawBeats(offset:Float = 0.0) {
365+
scale.y = 2;
366+
updateHitbox();
367+
super.drawBeats(-2);
368+
}
369+
override private function drawMeasures(offset:Float = 0.0) {
370+
scale.y = 4;
371+
updateHitbox();
372+
super.drawMeasures(-3);
373+
}
374+
}
375+
240376
class CharterBackdropDummy extends UISprite {
241377
var parent:CharterBackdropGroup;
242378
public function new(parent:CharterBackdropGroup) {
@@ -258,8 +394,7 @@ class CharterBackdropDummy extends UISprite {
258394
}
259395

260396
class EventBackdrop extends FlxBackdrop {
261-
public var eventBeatSeparator:FlxBackdrop;
262-
public var eventSecSeparator:FlxBackdrop;
397+
public var eventBeatSeparator:CharterEventGridSeperator;
263398

264399
public var topSeparator:FlxSprite;
265400
public var bottomSeparator:FlxSprite;
@@ -269,25 +404,10 @@ class EventBackdrop extends FlxBackdrop {
269404
alpha = 0.9;
270405

271406
// Separators
272-
eventSecSeparator = new FlxBackdrop(null, Y, 0, 0);
273-
eventSecSeparator.y = -2;
274-
eventSecSeparator.visible = Options.charterShowSections;
275-
276-
eventBeatSeparator = new FlxBackdrop(null, Y, 0, 0);
277-
eventBeatSeparator.y = -1;
278-
eventBeatSeparator.visible = Options.charterShowBeats;
279-
280-
for(sep in [eventSecSeparator, eventBeatSeparator]) {
281-
sep.makeSolid(1, 1, -1);
282-
sep.alpha = 0.5;
283-
sep.scrollFactor.set(1, 1);
284-
}
285-
286-
eventSecSeparator.scale.set(20, 4);
287-
eventSecSeparator.updateHitbox();
288-
289-
eventBeatSeparator.scale.set(10, 2);
290-
eventBeatSeparator.updateHitbox();
407+
eventBeatSeparator = new CharterEventGridSeperator();
408+
eventBeatSeparator.makeSolid(1, 1, -1);
409+
eventBeatSeparator.alpha = 0.5;
410+
eventBeatSeparator.scrollFactor.set(1, 1);
291411

292412
bottomSeparator = new FlxSprite(0,-2);
293413
bottomSeparator.makeSolid(1, 1, -1);
@@ -308,23 +428,32 @@ class EventBackdrop extends FlxBackdrop {
308428
public override function draw() {
309429
super.draw();
310430

311-
eventSecSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
312-
eventBeatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
313-
314-
eventSecSeparator.cameras = cameras;
315-
eventSecSeparator.x = (x+width) - 20;
316-
if (eventSecSeparator.visible) eventSecSeparator.draw();
317-
318431
eventBeatSeparator.cameras = cameras;
319-
eventBeatSeparator.x = (x+width) - 10;
320-
if (eventBeatSeparator.visible) eventBeatSeparator.draw();
432+
eventBeatSeparator.xPos = x+width;
433+
eventBeatSeparator.draw();
321434

322435
topSeparator.x = (x+width) - 20;
323436
topSeparator.cameras = this.cameras;
324-
if (!eventSecSeparator.visible) topSeparator.draw();
437+
if (!Options.charterShowSections) topSeparator.draw();
325438

326439
bottomSeparator.x = (x+width) - 20;
327440
bottomSeparator.cameras = this.cameras;
328441
bottomSeparator.draw();
329442
}
443+
}
444+
class CharterEventGridSeperator extends CharterGridSeperatorBase {
445+
public var xPos:Float = 0.0;
446+
override private function drawBeats(offset:Float = 0.0) {
447+
scale.set(10, 2);
448+
updateHitbox();
449+
x = xPos-10;
450+
super.drawBeats(-2);
451+
}
452+
override private function drawMeasures(offset:Float = 0.0) {
453+
scale.set(20, 4);
454+
updateHitbox();
455+
x = xPos-20;
456+
super.drawMeasures(-3);
457+
}
458+
override private function drawTimeSignatureChangeGaps() {}
330459
}

0 commit comments

Comments
 (0)