Skip to content

Commit 98a4c2b

Browse files
authored
refactor: Eliminate ModifiedTempo (#2130)
1 parent fa81a14 commit 98a4c2b

File tree

10 files changed

+277
-143
lines changed

10 files changed

+277
-143
lines changed

src/exporter/GpifWriter.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { VersionInfo } from '@src/generated/VersionInfo';
22
import { GeneralMidi } from '@src/midi/GeneralMidi';
3+
import { MidiFileGenerator } from '@src/midi/MidiFileGenerator';
34
import { MidiUtils } from '@src/midi/MidiUtils';
45
import { AccentuationType } from '@src/model/AccentuationType';
5-
import { AutomationType } from '@src/model/Automation';
6+
import { type Automation, AutomationType } from '@src/model/Automation';
67
import { type Bar, SustainPedalMarkerType } from '@src/model/Bar';
78
import { BarreShape } from '@src/model/BarreShape';
89
import { type Beat, BeatBeamingMode } from '@src/model/Beat';
@@ -44,6 +45,8 @@ import type { Voice } from '@src/model/Voice';
4445
import { WahPedal } from '@src/model/WahPedal';
4546
import { TextBaseline } from '@src/platform/ICanvas';
4647
import { BeamDirection } from '@src/rendering/utils/BeamDirection';
48+
import type { BackingTrackSyncPoint } from '@src/synth/IAlphaSynth';
49+
import { Lazy } from '@src/util/Lazy';
4750
import { XmlDocument } from '@src/xml/XmlDocument';
4851
import { XmlNode, XmlNodeType } from '@src/xml/XmlNode';
4952

@@ -1150,6 +1153,10 @@ export class GpifWriter {
11501153

11511154
this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
11521155

1156+
const modifiedTempoLookup = new Lazy<Map<Automation, BackingTrackSyncPoint>>(() =>
1157+
MidiFileGenerator.buildModifiedTempoLookup(score)
1158+
);
1159+
11531160
for (const mb of score.masterBars) {
11541161
for (const automation of mb.tempoAutomations) {
11551162
const tempoAutomation = automations.addElement('Automation');
@@ -1176,7 +1183,7 @@ export class GpifWriter {
11761183

11771184
value.addElement('BarIndex').innerText = mb.index.toString();
11781185
value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue!.barOccurence.toString();
1179-
value.addElement('ModifiedTempo').innerText = syncPoint.syncPointValue!.modifiedTempo.toString();
1186+
value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint)!.syncBpm.toString();
11801187
value.addElement('OriginalTempo').innerText = score.tempo.toString();
11811188
const frameOffset =
11821189
(((syncPoint.syncPointValue!.millisecondOffset - millisecondPadding) / 1000) *

src/generated/model/SyncPointDataCloner.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export class SyncPointDataCloner {
88
public static clone(original: SyncPointData): SyncPointData {
99
const clone = new SyncPointData();
1010
clone.barOccurence = original.barOccurence;
11-
clone.modifiedTempo = original.modifiedTempo;
1211
clone.millisecondOffset = original.millisecondOffset;
1312
return clone;
1413
}

src/generated/model/SyncPointDataSerializer.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class SyncPointDataSerializer {
1818
}
1919
const o = new Map<string, unknown>();
2020
o.set("baroccurence", obj.barOccurence);
21-
o.set("modifiedtempo", obj.modifiedTempo);
2221
o.set("millisecondoffset", obj.millisecondOffset);
2322
return o;
2423
}
@@ -27,9 +26,6 @@ export class SyncPointDataSerializer {
2726
case "baroccurence":
2827
obj.barOccurence = v! as number;
2928
return true;
30-
case "modifiedtempo":
31-
obj.modifiedTempo = v! as number;
32-
return true;
3329
case "millisecondoffset":
3430
obj.millisecondOffset = v! as number;
3531
return true;

src/importer/GpifParser.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,6 @@ export class GpifParser {
472472
case 'BarOccurrence':
473473
syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
474474
break;
475-
case 'ModifiedTempo':
476-
syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
477-
break;
478475
case 'FrameOffset':
479476
const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
480477
syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;

src/midi/MidiFileGenerator.ts

Lines changed: 176 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { MidiTickLookup } from '@src/midi/MidiTickLookup';
77

88
import { MidiUtils } from '@src/midi/MidiUtils';
99
import { AccentuationType } from '@src/model/AccentuationType';
10-
import { type Automation, AutomationType, SyncPointData } from '@src/model/Automation';
10+
import { type Automation, AutomationType } from '@src/model/Automation';
1111
import type { Bar } from '@src/model/Bar';
1212
import type { Beat } from '@src/model/Beat';
1313
import { BendPoint } from '@src/model/BendPoint';
@@ -58,6 +58,14 @@ class RasgueadoInfo {
5858
public brushInfos: Int32Array[] = [];
5959
}
6060

61+
class PlayThroughContext {
62+
public synthTick: number = 0;
63+
public synthTime: number = 0;
64+
public currentTempo: number = 0;
65+
public automationToSyncPoint: Map<Automation, BackingTrackSyncPoint> = new Map<Automation, BackingTrackSyncPoint>();
66+
public syncPoints!: BackingTrackSyncPoint[];
67+
}
68+
6169
/**
6270
* This generator creates a midi file using a score.
6371
*/
@@ -233,6 +241,29 @@ export class MidiFileGenerator {
233241
return syncPoints;
234242
}
235243

244+
/**
245+
* @internal
246+
*/
247+
public static buildModifiedTempoLookup(score: Score): Map<Automation, BackingTrackSyncPoint> {
248+
const syncPoints: BackingTrackSyncPoint[] = [];
249+
250+
const context = MidiFileGenerator.playThroughSong(
251+
score,
252+
syncPoints,
253+
(_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
254+
// no generation
255+
},
256+
(_barIndex, _currentTick, _currentTempo) => {
257+
// no generation
258+
},
259+
_endTick => {
260+
// no generation
261+
}
262+
);
263+
264+
return context.automationToSyncPoint;
265+
}
266+
236267
private static playThroughSong(
237268
score: Score,
238269
syncPoints: BackingTrackSyncPoint[],
@@ -248,7 +279,9 @@ export class MidiFileGenerator {
248279
) {
249280
const controller: MidiPlaybackController = new MidiPlaybackController(score);
250281

251-
let currentTempo = score.tempo;
282+
const playContext = new PlayThroughContext();
283+
playContext.currentTempo = score.tempo;
284+
playContext.syncPoints = syncPoints;
252285
let previousMasterBar: MasterBar | null = null;
253286

254287
// store the previous played bar for repeats
@@ -264,27 +297,14 @@ export class MidiFileGenerator {
264297
occurence++;
265298
barOccurence.set(index, occurence);
266299

267-
generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
268-
269-
const barSyncPoints = bar.syncPoints;
270-
if (barSyncPoints) {
271-
for (const syncPoint of barSyncPoints) {
272-
if (syncPoint.syncPointValue!.barOccurence === occurence) {
273-
const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
274-
syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue!));
275-
}
276-
}
277-
}
278-
279-
if (bar.tempoAutomations.length > 0) {
280-
currentTempo = bar.tempoAutomations[0].value;
281-
}
300+
generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);
282301

283-
generateTracks(index, currentTick, currentTempo);
302+
const trackTempo =
303+
bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
304+
generateTracks(index, currentTick, trackTempo);
284305

285-
if (bar.tempoAutomations.length > 0) {
286-
currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
287-
}
306+
playContext.synthTick = currentTick;
307+
MidiFileGenerator.processBarTime(bar, occurence, playContext);
288308
}
289309

290310
controller.moveNext();
@@ -297,24 +317,146 @@ export class MidiFileGenerator {
297317
// but where it ends according to the BPM and the remaining ticks.
298318
if (syncPoints.length > 0) {
299319
const lastSyncPoint = syncPoints[syncPoints.length - 1];
300-
const remainingTicks = controller.currentTick - lastSyncPoint.tick;
320+
const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
301321
if (remainingTicks > 0) {
302-
const syncPointData = new SyncPointData();
303-
304-
// last occurence of the last bar
305-
syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1)!;
306-
// same tempo as last point
307-
syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
308-
// interpolated end from last syncPoint
309-
syncPointData.millisecondOffset =
310-
lastSyncPoint.data.millisecondOffset +
311-
MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
312-
313-
syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
322+
const backingTrackSyncPoint = new BackingTrackSyncPoint();
323+
backingTrackSyncPoint.masterBarIndex = previousMasterBar!.index;
324+
backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar!.index)! - 1;
325+
backingTrackSyncPoint.synthTick = controller.currentTick;
326+
backingTrackSyncPoint.synthBpm = playContext.currentTempo;
327+
328+
// we need to assume some BPM for the last interpolated point.
329+
// if we have more than just a start point, we keep the BPM before the last manual sync point
330+
// otherwise we have no customized sync BPM known and keep the synthesizer one.
331+
backingTrackSyncPoint.syncBpm =
332+
syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;
333+
334+
backingTrackSyncPoint.synthTime =
335+
lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
336+
backingTrackSyncPoint.syncTime =
337+
lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);
338+
339+
// update the previous sync point according to the new time
340+
lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);
341+
342+
syncPoints.push(backingTrackSyncPoint);
314343
}
315344
}
316345

317346
finish(controller.currentTick);
347+
348+
return playContext;
349+
}
350+
351+
private static processBarTime(bar: MasterBar, occurence: number, context: PlayThroughContext) {
352+
const duration = bar.calculateDuration();
353+
const barSyncPoints = bar.syncPoints;
354+
const barStartTick = context.synthTick;
355+
if (barSyncPoints) {
356+
MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
357+
} else {
358+
MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
359+
}
360+
361+
// don't forget the part after the last tempo change
362+
const endTick = barStartTick + duration;
363+
const tickOffset = endTick - context.synthTick;
364+
if (tickOffset > 0) {
365+
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
366+
context.synthTick = endTick;
367+
}
368+
}
369+
370+
private static processBarTimeWithSyncPoints(bar: MasterBar, occurence: number, context: PlayThroughContext) {
371+
const barStartTick = context.synthTick;
372+
const duration = bar.calculateDuration();
373+
374+
let tempoChangeIndex = 0;
375+
let tickOffset: number;
376+
377+
for (const syncPoint of bar.syncPoints!) {
378+
if (syncPoint.syncPointValue!.barOccurence !== occurence) {
379+
continue;
380+
}
381+
382+
const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;
383+
384+
// first process all tempo changes until this sync point
385+
while (
386+
tempoChangeIndex < bar.tempoAutomations.length &&
387+
bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition
388+
) {
389+
const tempoChange = bar.tempoAutomations[tempoChangeIndex];
390+
const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
391+
tickOffset = absoluteTick - context.synthTick;
392+
393+
if (tickOffset > 0) {
394+
context.synthTick = absoluteTick;
395+
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
396+
}
397+
398+
context.currentTempo = tempoChange.value;
399+
tempoChangeIndex++;
400+
}
401+
402+
// process time until sync point
403+
tickOffset = syncPointTick - context.synthTick;
404+
if (tickOffset > 0) {
405+
context.synthTick = syncPointTick;
406+
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
407+
}
408+
409+
// update the previous sync point according to the new time
410+
if (context.syncPoints.length > 0) {
411+
context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(
412+
context.synthTime,
413+
syncPoint.syncPointValue!.millisecondOffset
414+
);
415+
}
416+
417+
// create the new sync point
418+
const backingTrackSyncPoint = new BackingTrackSyncPoint();
419+
backingTrackSyncPoint.masterBarIndex = bar.index;
420+
backingTrackSyncPoint.masterBarOccurence = occurence;
421+
backingTrackSyncPoint.synthTick = syncPointTick;
422+
backingTrackSyncPoint.synthBpm = context.currentTempo;
423+
backingTrackSyncPoint.synthTime = context.synthTime;
424+
backingTrackSyncPoint.syncTime = syncPoint.syncPointValue!.millisecondOffset;
425+
backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;
426+
427+
context.syncPoints.push(backingTrackSyncPoint);
428+
context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
429+
}
430+
431+
// process remaining tempo changes after all sync points
432+
while (tempoChangeIndex < bar.tempoAutomations.length) {
433+
const tempoChange = bar.tempoAutomations[tempoChangeIndex];
434+
const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
435+
tickOffset = absoluteTick - context.synthTick;
436+
if (tickOffset > 0) {
437+
context.synthTick = absoluteTick;
438+
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
439+
}
440+
441+
context.currentTempo = tempoChange.value;
442+
tempoChangeIndex++;
443+
}
444+
}
445+
446+
private static processBarTimeNoSyncPoints(bar: MasterBar, context: PlayThroughContext) {
447+
// walk through the tempo changes
448+
const barStartTick = context.synthTick;
449+
const duration = bar.calculateDuration();
450+
for (const changes of bar.tempoAutomations) {
451+
const absoluteTick = barStartTick + changes.ratioPosition * duration;
452+
const tickOffset = absoluteTick - context.synthTick;
453+
if (tickOffset > 0) {
454+
context.synthTick = absoluteTick;
455+
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
456+
}
457+
458+
context.currentTempo = changes.value;
459+
}
318460
}
319461

320462
private static toChannelShort(data: number): number {

src/model/Automation.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,7 @@ export interface FlatSyncPoint {
4242
*/
4343
barOccurence: number;
4444
/**
45-
* The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
46-
* This information is used together with normal tempo changes to calculate how much faster/slower the
47-
* cursor playback is performed to align with the audio track.
48-
*/
49-
modifiedTempo: number;
50-
/**
51-
* The uadio offset marking the position within the audio track in milliseconds.
45+
* The audio offset marking the position within the audio track in milliseconds.
5246
* This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
5347
*/
5448
millisecondOffset: number;
@@ -66,12 +60,6 @@ export class SyncPointData {
6660
* Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
6761
*/
6862
public barOccurence: number = 0;
69-
/**
70-
* The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
71-
* This information is used together with normal tempo changes to calculate how much faster/slower the
72-
* cursor playback is performed to align with the audio track.
73-
*/
74-
public modifiedTempo: number = 0;
7563
/**
7664
* The audio offset marking the position within the audio track in milliseconds.
7765
* This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.

src/model/Score.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,6 @@ export class Score {
422422
automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
423423
automation.type = AutomationType.SyncPoint;
424424
automation.syncPointValue = new SyncPointData();
425-
automation.syncPointValue.modifiedTempo = syncPoint.modifiedTempo;
426425
automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
427426
automation.syncPointValue.barOccurence = syncPoint.barOccurence;
428427
if (syncPoint.barIndex < this.masterBars.length) {
@@ -458,8 +457,7 @@ export class Score {
458457
barIndex: masterBar.index,
459458
barOccurence: syncPoint.syncPointValue!.barOccurence,
460459
barPosition: syncPoint.ratioPosition,
461-
millisecondOffset: syncPoint.syncPointValue!.millisecondOffset,
462-
modifiedTempo: syncPoint.syncPointValue!.modifiedTempo
460+
millisecondOffset: syncPoint.syncPointValue!.millisecondOffset
463461
});
464462
}
465463
}

0 commit comments

Comments
 (0)