Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/exporter/GpifWriter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { VersionInfo } from '@src/generated/VersionInfo';
import { GeneralMidi } from '@src/midi/GeneralMidi';
import { MidiFileGenerator } from '@src/midi/MidiFileGenerator';
import { MidiUtils } from '@src/midi/MidiUtils';
import { AccentuationType } from '@src/model/AccentuationType';
import { AutomationType } from '@src/model/Automation';
import { type Automation, AutomationType } from '@src/model/Automation';
import { type Bar, SustainPedalMarkerType } from '@src/model/Bar';
import { BarreShape } from '@src/model/BarreShape';
import { type Beat, BeatBeamingMode } from '@src/model/Beat';
Expand Down Expand Up @@ -44,6 +45,8 @@ import type { Voice } from '@src/model/Voice';
import { WahPedal } from '@src/model/WahPedal';
import { TextBaseline } from '@src/platform/ICanvas';
import { BeamDirection } from '@src/rendering/utils/BeamDirection';
import type { BackingTrackSyncPoint } from '@src/synth/IAlphaSynth';
import { Lazy } from '@src/util/Lazy';
import { XmlDocument } from '@src/xml/XmlDocument';
import { XmlNode, XmlNodeType } from '@src/xml/XmlNode';

Expand Down Expand Up @@ -1150,6 +1153,10 @@ export class GpifWriter {

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

const modifiedTempoLookup = new Lazy<Map<Automation, BackingTrackSyncPoint>>(() =>
MidiFileGenerator.buildModifiedTempoLookup(score)
);

for (const mb of score.masterBars) {
for (const automation of mb.tempoAutomations) {
const tempoAutomation = automations.addElement('Automation');
Expand All @@ -1176,7 +1183,7 @@ export class GpifWriter {

value.addElement('BarIndex').innerText = mb.index.toString();
value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue!.barOccurence.toString();
value.addElement('ModifiedTempo').innerText = syncPoint.syncPointValue!.modifiedTempo.toString();
value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint)!.syncBpm.toString();
value.addElement('OriginalTempo').innerText = score.tempo.toString();
const frameOffset =
(((syncPoint.syncPointValue!.millisecondOffset - millisecondPadding) / 1000) *
Expand Down
1 change: 0 additions & 1 deletion src/generated/model/SyncPointDataCloner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export class SyncPointDataCloner {
public static clone(original: SyncPointData): SyncPointData {
const clone = new SyncPointData();
clone.barOccurence = original.barOccurence;
clone.modifiedTempo = original.modifiedTempo;
clone.millisecondOffset = original.millisecondOffset;
return clone;
}
Expand Down
4 changes: 0 additions & 4 deletions src/generated/model/SyncPointDataSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export class SyncPointDataSerializer {
}
const o = new Map<string, unknown>();
o.set("baroccurence", obj.barOccurence);
o.set("modifiedtempo", obj.modifiedTempo);
o.set("millisecondoffset", obj.millisecondOffset);
return o;
}
Expand All @@ -27,9 +26,6 @@ export class SyncPointDataSerializer {
case "baroccurence":
obj.barOccurence = v! as number;
return true;
case "modifiedtempo":
obj.modifiedTempo = v! as number;
return true;
case "millisecondoffset":
obj.millisecondOffset = v! as number;
return true;
Expand Down
3 changes: 0 additions & 3 deletions src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,6 @@ export class GpifParser {
case 'BarOccurrence':
syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
break;
case 'ModifiedTempo':
syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
break;
case 'FrameOffset':
const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
Expand Down
210 changes: 176 additions & 34 deletions src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MidiTickLookup } from '@src/midi/MidiTickLookup';

import { MidiUtils } from '@src/midi/MidiUtils';
import { AccentuationType } from '@src/model/AccentuationType';
import { type Automation, AutomationType, SyncPointData } from '@src/model/Automation';
import { type Automation, AutomationType } from '@src/model/Automation';
import type { Bar } from '@src/model/Bar';
import type { Beat } from '@src/model/Beat';
import { BendPoint } from '@src/model/BendPoint';
Expand Down Expand Up @@ -58,6 +58,14 @@ class RasgueadoInfo {
public brushInfos: Int32Array[] = [];
}

class PlayThroughContext {
public synthTick: number = 0;
public synthTime: number = 0;
public currentTempo: number = 0;
public automationToSyncPoint: Map<Automation, BackingTrackSyncPoint> = new Map<Automation, BackingTrackSyncPoint>();
public syncPoints!: BackingTrackSyncPoint[];
}

/**
* This generator creates a midi file using a score.
*/
Expand Down Expand Up @@ -233,6 +241,29 @@ export class MidiFileGenerator {
return syncPoints;
}

/**
* @internal
*/
public static buildModifiedTempoLookup(score: Score): Map<Automation, BackingTrackSyncPoint> {
const syncPoints: BackingTrackSyncPoint[] = [];

const context = MidiFileGenerator.playThroughSong(
score,
syncPoints,
(_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
// no generation
},
(_barIndex, _currentTick, _currentTempo) => {
// no generation
},
_endTick => {
// no generation
}
);

return context.automationToSyncPoint;
}

private static playThroughSong(
score: Score,
syncPoints: BackingTrackSyncPoint[],
Expand All @@ -248,7 +279,9 @@ export class MidiFileGenerator {
) {
const controller: MidiPlaybackController = new MidiPlaybackController(score);

let currentTempo = score.tempo;
const playContext = new PlayThroughContext();
playContext.currentTempo = score.tempo;
playContext.syncPoints = syncPoints;
let previousMasterBar: MasterBar | null = null;

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

generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);

const barSyncPoints = bar.syncPoints;
if (barSyncPoints) {
for (const syncPoint of barSyncPoints) {
if (syncPoint.syncPointValue!.barOccurence === occurence) {
const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue!));
}
}
}

if (bar.tempoAutomations.length > 0) {
currentTempo = bar.tempoAutomations[0].value;
}
generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);

generateTracks(index, currentTick, currentTempo);
const trackTempo =
bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
generateTracks(index, currentTick, trackTempo);

if (bar.tempoAutomations.length > 0) {
currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
}
playContext.synthTick = currentTick;
MidiFileGenerator.processBarTime(bar, occurence, playContext);
}

controller.moveNext();
Expand All @@ -297,24 +317,146 @@ export class MidiFileGenerator {
// but where it ends according to the BPM and the remaining ticks.
if (syncPoints.length > 0) {
const lastSyncPoint = syncPoints[syncPoints.length - 1];
const remainingTicks = controller.currentTick - lastSyncPoint.tick;
const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
if (remainingTicks > 0) {
const syncPointData = new SyncPointData();

// last occurence of the last bar
syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1)!;
// same tempo as last point
syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
// interpolated end from last syncPoint
syncPointData.millisecondOffset =
lastSyncPoint.data.millisecondOffset +
MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);

syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
const backingTrackSyncPoint = new BackingTrackSyncPoint();
backingTrackSyncPoint.masterBarIndex = previousMasterBar!.index;
backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar!.index)! - 1;
backingTrackSyncPoint.synthTick = controller.currentTick;
backingTrackSyncPoint.synthBpm = playContext.currentTempo;

// we need to assume some BPM for the last interpolated point.
// if we have more than just a start point, we keep the BPM before the last manual sync point
// otherwise we have no customized sync BPM known and keep the synthesizer one.
backingTrackSyncPoint.syncBpm =
syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;

backingTrackSyncPoint.synthTime =
lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
backingTrackSyncPoint.syncTime =
lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);

// update the previous sync point according to the new time
lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);

syncPoints.push(backingTrackSyncPoint);
}
}

finish(controller.currentTick);

return playContext;
}

private static processBarTime(bar: MasterBar, occurence: number, context: PlayThroughContext) {
const duration = bar.calculateDuration();
const barSyncPoints = bar.syncPoints;
const barStartTick = context.synthTick;
if (barSyncPoints) {
MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
} else {
MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
}

// don't forget the part after the last tempo change
const endTick = barStartTick + duration;
const tickOffset = endTick - context.synthTick;
if (tickOffset > 0) {
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
context.synthTick = endTick;
}
}

private static processBarTimeWithSyncPoints(bar: MasterBar, occurence: number, context: PlayThroughContext) {
const barStartTick = context.synthTick;
const duration = bar.calculateDuration();

let tempoChangeIndex = 0;
let tickOffset: number;

for (const syncPoint of bar.syncPoints!) {
if (syncPoint.syncPointValue!.barOccurence !== occurence) {
continue;
}

const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;

// first process all tempo changes until this sync point
while (
tempoChangeIndex < bar.tempoAutomations.length &&
bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition
) {
const tempoChange = bar.tempoAutomations[tempoChangeIndex];
const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
tickOffset = absoluteTick - context.synthTick;

if (tickOffset > 0) {
context.synthTick = absoluteTick;
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
}

context.currentTempo = tempoChange.value;
tempoChangeIndex++;
}

// process time until sync point
tickOffset = syncPointTick - context.synthTick;
if (tickOffset > 0) {
context.synthTick = syncPointTick;
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
}

// update the previous sync point according to the new time
if (context.syncPoints.length > 0) {
context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(
context.synthTime,
syncPoint.syncPointValue!.millisecondOffset
);
}

// create the new sync point
const backingTrackSyncPoint = new BackingTrackSyncPoint();
backingTrackSyncPoint.masterBarIndex = bar.index;
backingTrackSyncPoint.masterBarOccurence = occurence;
backingTrackSyncPoint.synthTick = syncPointTick;
backingTrackSyncPoint.synthBpm = context.currentTempo;
backingTrackSyncPoint.synthTime = context.synthTime;
backingTrackSyncPoint.syncTime = syncPoint.syncPointValue!.millisecondOffset;
backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;

context.syncPoints.push(backingTrackSyncPoint);
context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
}

// process remaining tempo changes after all sync points
while (tempoChangeIndex < bar.tempoAutomations.length) {
const tempoChange = bar.tempoAutomations[tempoChangeIndex];
const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
tickOffset = absoluteTick - context.synthTick;
if (tickOffset > 0) {
context.synthTick = absoluteTick;
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
}

context.currentTempo = tempoChange.value;
tempoChangeIndex++;
}
}

private static processBarTimeNoSyncPoints(bar: MasterBar, context: PlayThroughContext) {
// walk through the tempo changes
const barStartTick = context.synthTick;
const duration = bar.calculateDuration();
for (const changes of bar.tempoAutomations) {
const absoluteTick = barStartTick + changes.ratioPosition * duration;
const tickOffset = absoluteTick - context.synthTick;
if (tickOffset > 0) {
context.synthTick = absoluteTick;
context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
}

context.currentTempo = changes.value;
}
}

private static toChannelShort(data: number): number {
Expand Down
14 changes: 1 addition & 13 deletions src/model/Automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,7 @@ export interface FlatSyncPoint {
*/
barOccurence: number;
/**
* The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
* This information is used together with normal tempo changes to calculate how much faster/slower the
* cursor playback is performed to align with the audio track.
*/
modifiedTempo: number;
/**
* The uadio offset marking the position within the audio track in milliseconds.
* The audio offset marking the position within the audio track in milliseconds.
* This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
*/
millisecondOffset: number;
Expand All @@ -66,12 +60,6 @@ export class SyncPointData {
* Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
*/
public barOccurence: number = 0;
/**
* The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
* This information is used together with normal tempo changes to calculate how much faster/slower the
* cursor playback is performed to align with the audio track.
*/
public modifiedTempo: number = 0;
/**
* The audio offset marking the position within the audio track in milliseconds.
* This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
Expand Down
4 changes: 1 addition & 3 deletions src/model/Score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,6 @@ export class Score {
automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
automation.type = AutomationType.SyncPoint;
automation.syncPointValue = new SyncPointData();
automation.syncPointValue.modifiedTempo = syncPoint.modifiedTempo;
automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
automation.syncPointValue.barOccurence = syncPoint.barOccurence;
if (syncPoint.barIndex < this.masterBars.length) {
Expand Down Expand Up @@ -458,8 +457,7 @@ export class Score {
barIndex: masterBar.index,
barOccurence: syncPoint.syncPointValue!.barOccurence,
barPosition: syncPoint.ratioPosition,
millisecondOffset: syncPoint.syncPointValue!.millisecondOffset,
modifiedTempo: syncPoint.syncPointValue!.modifiedTempo
millisecondOffset: syncPoint.syncPointValue!.millisecondOffset
});
}
}
Expand Down
Loading