Skip to content

Commit 7166db8

Browse files
authored
feat: Sync Points for alphaTex (#2131)
1 parent 98a4c2b commit 7166db8

File tree

10 files changed

+484
-12
lines changed

10 files changed

+484
-12
lines changed

src.compiler/csharp/CSharpAstTransformer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3647,7 +3647,16 @@ export default class CSharpAstTransformer {
36473647
newObject.arguments.push(assignment);
36483648
} else if (ts.isShorthandPropertyAssignment(p)) {
36493649
assignment.label = p.name.getText();
3650-
assignment.expression = this.visitExpression(assignment, p.objectAssignmentInitializer!)!;
3650+
if(p.objectAssignmentInitializer) {
3651+
assignment.expression = this.visitExpression(assignment, p.objectAssignmentInitializer)!;
3652+
} else {
3653+
assignment.expression = {
3654+
nodeType: cs.SyntaxKind.Identifier,
3655+
parent: assignment,
3656+
tsNode: p.name,
3657+
text: p.name.getText()
3658+
} as cs.Identifier
3659+
}
36513660
newObject.arguments.push(assignment);
36523661
} else if (ts.isSpreadAssignment(p)) {
36533662
this._context.addTsNodeDiagnostics(p, 'Spread operator not supported', ts.DiagnosticCategory.Error);

src/importer/AlphaTexImporter.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GeneralMidi } from '@src/midi/GeneralMidi';
22
import { ScoreImporter } from '@src/importer/ScoreImporter';
33
import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError';
44
import { AccentuationType } from '@src/model/AccentuationType';
5-
import { Automation, AutomationType } from '@src/model/Automation';
5+
import { Automation, AutomationType, type FlatSyncPoint } from '@src/model/Automation';
66
import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar';
77
import { Beat, BeatBeamingMode } from '@src/model/Beat';
88
import { BendPoint } from '@src/model/BendPoint';
@@ -190,6 +190,7 @@ export class AlphaTexImporter extends ScoreImporter {
190190
private _articulationValueToIndex = new Map<number, number>();
191191

192192
private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit;
193+
private _syncPoints: FlatSyncPoint[] = [];
193194

194195
public logErrors: boolean = false;
195196

@@ -235,11 +236,18 @@ export class AlphaTexImporter extends ScoreImporter {
235236
if (!anyMetaRead && !anyBarsRead) {
236237
throw new UnsupportedFormatError('No alphaTex data found');
237238
}
239+
240+
if (this._sy === AlphaTexSymbols.Dot) {
241+
this._sy = this.newSy();
242+
this.syncPoints();
243+
}
238244
}
239245

240246
ModelUtils.consolidate(this._score);
241247
this._score.finish(this.settings);
248+
ModelUtils.trimEmptyBarsAtEnd(this._score);
242249
this._score.rebuildRepeatGroups();
250+
this._score.applyFlatSyncPoints(this._syncPoints);
243251
for (const [track, lyrics] of this._lyrics) {
244252
this._score.tracks[track].applyLyrics(lyrics);
245253
}
@@ -256,6 +264,55 @@ export class AlphaTexImporter extends ScoreImporter {
256264
}
257265
}
258266

267+
private syncPoints() {
268+
while (this._sy !== AlphaTexSymbols.Eof) {
269+
this.syncPoint();
270+
}
271+
}
272+
273+
private syncPoint() {
274+
// \sync BarIndex Occurence MillisecondOffset
275+
// \sync BarIndex Occurence MillisecondOffset RatioPosition
276+
277+
if (this._sy !== AlphaTexSymbols.MetaCommand || (this._syData as string) !== 'sync') {
278+
this.error('syncPoint', AlphaTexSymbols.MetaCommand, true);
279+
}
280+
281+
this._sy = this.newSy();
282+
if (this._sy !== AlphaTexSymbols.Number) {
283+
this.error('syncPointBarIndex', AlphaTexSymbols.Number, true);
284+
}
285+
const barIndex = this._syData as number;
286+
287+
this._sy = this.newSy();
288+
if (this._sy !== AlphaTexSymbols.Number) {
289+
this.error('syncPointBarOccurence', AlphaTexSymbols.Number, true);
290+
}
291+
const barOccurence = this._syData as number;
292+
293+
this._sy = this.newSy();
294+
if (this._sy !== AlphaTexSymbols.Number) {
295+
this.error('syncPointBarMillis', AlphaTexSymbols.Number, true);
296+
}
297+
const millisecondOffset = this._syData as number;
298+
299+
this._allowFloat = true;
300+
this._sy = this.newSy();
301+
this._allowFloat = false;
302+
let barPosition = 0;
303+
if (this._sy === AlphaTexSymbols.Number) {
304+
barPosition = this._syData as number;
305+
this._sy = this.newSy();
306+
}
307+
308+
this._syncPoints.push({
309+
barIndex,
310+
barOccurence,
311+
barPosition,
312+
millisecondOffset
313+
});
314+
}
315+
259316
private error(nonterm: string, expected: AlphaTexSymbols, wrongSymbol: boolean = true): void {
260317
let receivedSymbol: AlphaTexSymbols;
261318
let showSyData = false;

src/importer/ScoreLoader.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ import type { Score } from '@src/model/Score';
99
import { Settings } from '@src/Settings';
1010

1111
import { Logger } from '@src/Logger';
12+
import { AlphaTexImporter } from '@src/importer/AlphaTexImporter';
1213

1314
/**
1415
* The ScoreLoader enables you easy loading of Scores using all
1516
* available importers
1617
*/
1718
export class ScoreLoader {
19+
/**
20+
* Loads the given alphaTex string.
21+
* @param tex The alphaTex string.
22+
* @param settings The settings to use for parsing.
23+
* @returns The parsed {@see Score}.
24+
*/
25+
public static loadAlphaTex(tex: string, settings?: Settings): Score {
26+
const parser = new AlphaTexImporter();
27+
parser.logErrors = true;
28+
parser.initFromString(tex, settings ?? new Settings());
29+
return parser.readScore();
30+
}
31+
1832
/**
1933
* Loads a score asynchronously from the given datasource
2034
* @param path the source path to load the binary file from

src/importer/_barrel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ScoreImporter } from '@src/importer/ScoreImporter';
22
export { ScoreLoader } from '@src/importer/ScoreLoader';
33
export { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError';
4+
export { AlphaTexImporter } from '@src/importer/AlphaTexImporter';

src/model/Bar.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,31 @@ export class Bar {
318318
return this._isEmpty;
319319
}
320320

321+
/**
322+
* Whether this bar has any changes applied which are not related to the voices in it.
323+
* (e.g. new key signatures)
324+
*/
325+
public get hasChanges(): boolean {
326+
if (this.index === 0) {
327+
return true;
328+
}
329+
const hasChangesToPrevious =
330+
this.keySignature !== this.previousBar!.keySignature ||
331+
this.keySignatureType !== this.previousBar!.keySignatureType ||
332+
this.clef !== this.previousBar!.clef ||
333+
this.clefOttava !== this.previousBar!.clefOttava;
334+
if (hasChangesToPrevious) {
335+
return true;
336+
}
337+
338+
return (
339+
this.simileMark !== SimileMark.None ||
340+
this.sustainPedals.length > 0 ||
341+
this.barLineLeft !== BarLineStyle.Automatic ||
342+
this.barLineRight !== BarLineStyle.Automatic
343+
);
344+
}
345+
321346
/**
322347
* Whether this bar is empty or has only rests.
323348
*/

src/model/MasterBar.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ export class MasterBar {
4444
*/
4545
public index: number = 0;
4646

47+
/**
48+
* Whether the masterbar is has any changes applied to it (e.g. tempo changes, time signature changes etc)
49+
* The first bar is always considered changed due to initial setup of values. It does not consider
50+
* elements like whether the tempo really changes to the previous bar.
51+
*/
52+
public get hasChanges() {
53+
if (this.index === 0) {
54+
return false;
55+
}
56+
57+
const hasChangesToPrevious =
58+
this.timeSignatureCommon !== this.previousMasterBar!.timeSignatureCommon ||
59+
this.timeSignatureNumerator !== this.previousMasterBar!.timeSignatureNumerator ||
60+
this.timeSignatureDenominator !== this.previousMasterBar!.timeSignatureDenominator ||
61+
this.tripletFeel !== this.previousMasterBar!.tripletFeel;
62+
if (hasChangesToPrevious) {
63+
return true;
64+
}
65+
66+
return (
67+
this.alternateEndings !== 0 ||
68+
this.isRepeatStart ||
69+
this.isRepeatEnd ||
70+
this.isFreeTime ||
71+
this.isSectionStart ||
72+
this.tempoAutomations.length > 0 ||
73+
this.syncPoints && this.syncPoints!.length > 0 ||
74+
(this.fermata !== null && this.fermata!.size > 0) ||
75+
(this.directions !== null && this.directions!.size > 0) ||
76+
this.isAnacrusis
77+
);
78+
}
79+
4780
/**
4881
* The key signature used on all bars.
4982
* @deprecated Use key signatures on bar level

src/model/ModelUtils.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,16 +411,15 @@ export class ModelUtils {
411411
const masterBar = score.masterBars[currentIndex];
412412

413413
let hasTempoChange = false;
414-
for(const a of masterBar.tempoAutomations) {
415-
if(a.value !== tempo) {
414+
for (const a of masterBar.tempoAutomations) {
415+
if (a.value !== tempo) {
416416
hasTempoChange = true;
417417
}
418418
tempo = a.value;
419419
}
420420

421421
// check if masterbar breaks multibar rests, it must be fully empty with no annotations
422-
if (
423-
masterBar.alternateEndings ||
422+
if (masterBar.alternateEndings ||
424423
(masterBar.isRepeatStart && masterBar.index !== currentGroupStartIndex) ||
425424
masterBar.isFreeTime ||
426425
masterBar.isAnacrusis ||
@@ -632,4 +631,46 @@ export class ModelUtils {
632631
}
633632
}
634633
}
634+
635+
/**
636+
* Trims any empty bars at the end of the song.
637+
* @param score
638+
*/
639+
public static trimEmptyBarsAtEnd(score: Score) {
640+
while (score.masterBars.length > 1) {
641+
const barIndex = score.masterBars.length - 1;
642+
const masterBar = score.masterBars[barIndex];
643+
644+
if (masterBar.hasChanges) {
645+
return;
646+
}
647+
648+
for (const track of score.tracks) {
649+
for (const staff of track.staves) {
650+
if (barIndex < staff.bars.length) {
651+
const bar = staff.bars[barIndex];
652+
if (!bar.isEmpty || bar.hasChanges) {
653+
// found a non-empty bar, stop whole cleanup
654+
return;
655+
}
656+
}
657+
}
658+
}
659+
660+
// if we reach here, all found bars are empty, remove the bar
661+
for (const track of score.tracks) {
662+
for (const staff of track.staves) {
663+
if (barIndex < staff.bars.length) {
664+
const bar = staff.bars[barIndex];
665+
staff.bars.pop();
666+
// unlink
667+
bar.previousBar!.nextBar = null;
668+
}
669+
}
670+
}
671+
672+
score.masterBars.pop();
673+
masterBar.previousMasterBar!.nextMasterBar = null;
674+
}
675+
}
635676
}

test/audio/MidiPlaybackController.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ describe('MidiPlaybackControllerTest', () => {
163163
const tex: string = `
164164
\\tempo 175
165165
.
166-
\\ro :1 r | \\ae 1 r | \\ae 2 \\rc 2 r | \\ro r | \\ae 1 r | \\ae (2 3 4) \\rc 4 r |
166+
\\ro :1 r | \\ae 1 r | \\ae 2 \\rc 2 r | \\ro r | \\ae 1 r | \\ae (2 3 4) \\rc 4 r | r
167167
`;
168168
const expectedBars: number[] = [0, 1, 0, 2, 3, 4, 3, 5, 3, 5, 3, 5, 6];
169169
testAlphaTexRepeat(tex, expectedBars, 50);

test/importer/AlphaTexImporter.test.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ describe('AlphaTexImporterTest', () => {
781781
[KeySignature.B, KeySignatureType.Major],
782782
[KeySignature.FSharp, KeySignatureType.Minor]
783783
];
784-
784+
785785
for (let i = 0; i < expected.length; i++) {
786786
expect(bars[i].keySignature).to.equal(expected[i][0]);
787787
expect(bars[i].keySignatureType).to.equal(expected[i][1]);
@@ -881,7 +881,7 @@ describe('AlphaTexImporterTest', () => {
881881
});
882882

883883
it('triplet-feel-multi-bar', () => {
884-
const tex: string = '\\tf t16 | | | \\tf t8 | | | \\tf no | | ';
884+
const tex: string = '\\tf t16 C4 | C4 | C4 | \\tf t8 C4 | C4 | C4 | \\tf no | C4 | C4 ';
885885
const score: Score = parseTex(tex);
886886
expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.Triplet16th);
887887
expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th);
@@ -1480,9 +1480,9 @@ describe('AlphaTexImporterTest', () => {
14801480

14811481
expect(score.masterBars).to.have.length(2);
14821482

1483-
expect(score.tracks[0].staves[0].bars).to.have.length(2);
1484-
expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2);
1485-
expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(2);
1483+
expect(score.tracks[0].staves[0].bars.length).to.equal(2);
1484+
expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2);
1485+
expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2);
14861486
});
14871487

14881488
it('multi-voice-simple-all-voices', () => {
@@ -1974,4 +1974,52 @@ describe('AlphaTexImporterTest', () => {
19741974
`);
19751975
expect(score).toMatchSnapshot();
19761976
});
1977+
1978+
it('sync', () => {
1979+
const score = parseTex(`
1980+
\\tempo 90
1981+
.
1982+
3.4.4*4 | 3.4.4*4 |
1983+
\\ro 3.4.4*4 | 3.4.4*4 | \\rc 2 3.4.4*4 |
1984+
3.4.4*4 | 3.4.4*4
1985+
.
1986+
\\sync 0 0 0
1987+
\\sync 0 0 1000 0.5
1988+
\\sync 1 0 2000
1989+
\\sync 3 0 3000
1990+
\\sync 3 1 4000
1991+
\\sync 6 1 5000
1992+
`);
1993+
1994+
// simplify snapshot
1995+
score.tracks = [];
1996+
1997+
expect(score).toMatchSnapshot();
1998+
});
1999+
2000+
it('sync-expect-dot', () => {
2001+
const score = parseTex(`
2002+
\\title "Prelude in D Minor"
2003+
\\artist "J.S. Bach (1685-1750)"
2004+
\\copyright "Public Domain"
2005+
\\tempo 80
2006+
.
2007+
\\ts 3 4
2008+
0.4.16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) |
2009+
(3.2 0.4).16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) |
2010+
(3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) |
2011+
(3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) |
2012+
.
2013+
\\sync 0 0 0
2014+
\\sync 0 0 1500 0.666
2015+
\\sync 1 0 4075 0.666
2016+
\\sync 2 0 6475 0.333
2017+
\\sync 3 0 10223 1
2018+
`);
2019+
2020+
// simplify snapshot
2021+
score.tracks = [];
2022+
2023+
expect(score).toMatchSnapshot();
2024+
});
19772025
});

0 commit comments

Comments
 (0)