Skip to content

Commit 001c98d

Browse files
authored
docs: Add SyncPoints for alphaTex (#136)
1 parent 827a549 commit 001c98d

8 files changed

Lines changed: 157 additions & 191 deletions

File tree

docs/alphatex/introduction.mdx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { AlphaTexSample } from '@site/src/components/AlphaTexSample';
66

77
In this section you find all details about how to write music notation using AlphaTex.
88
AlphaTex is a text format for writing music notation for AlphaTab. AlphaTex loading
9-
can be enabled by specifying `data-tex="true"` on the container element.
10-
AlphaTab will load the tex code from the element contents and parse it.
9+
can be enabled by setting the [`tex`](/docs/reference/settings/core/tex) option or loading it via [`tex()`](/docs/reference/api/tex) method on the API.
10+
AlphaTab will load the tex code from the element contents and parse it. You can also load it from a file like other formats.
1111
AlphaTex supports most of the features alphaTab supports overall.
1212
If you find anything missing you would like to see, feel free to
1313
[initiate a Discussion on GitHub](https://github.com/CoderLine/alphaTab/discussions/new) so we can find a good solution together.
@@ -30,3 +30,23 @@ Here is an example score fully rendered using alphaTex.
3030
15.1.8 :16 14.1{tu 3} 15.1{tu 3} 14.1{tu 3} :8 17.2 15.1 14.1 :16 12.1{tu 3} 14.1{tu 3} 12.1{tu 3} :8 15.2 14.2 |
3131
12.2 14.3 12.3 15.2 :32 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h} 14.2{h} 15.2{h}
3232
`}</AlphaTexSample>
33+
34+
35+
## General Song Structure
36+
37+
alphaTex has the following structure variations. Comments are supported in C-style comments via `// Single Line` and `/* Multi Line */`.
38+
39+
40+
```title=General File Structure
41+
/* Song Metadata */
42+
.
43+
/* Song Contents */
44+
.
45+
/* Sync Points */
46+
```
47+
48+
The Song Metadata and Sync Points are optional but the dots are mandatory to separate the sections in case there is any content filled.
49+
50+
* Song Metadata: This section contains all information generally about the song like title.
51+
* Song Contents: This section contains defines the whole song contents with all the tracks, staves, bars, beats, notes that alphaTab supports. Bars are separated by `|` symbols.
52+
* Sync Points: alphaTab can be synchronized with external media like audio backing tracks or videos. To have the correct cursor display and highlighting, songs have to be synchronized. This section defines such markers.

docs/alphatex/sync-points.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: Sync Points
3+
---
4+
5+
6+
alphaTex support specifying sync points for the [synchronization with external media](/docs/guides/audio-video-sync).
7+
8+
The related sync points are specified as flat list at the end of the song contents separated by a dot `.`.
9+
As we consider it unlikely that authors write this information manually, we separated the sync points from the other song.
10+
This way tools like our [Media Sync Editor](/docs/playground/) on the Playground can be used to synchronize songs and
11+
the sync info can be copy-pasted after the main song.
12+
13+
The supported formats of sync points are:
14+
15+
* `\sync BarIndex Occurence MillisecondOffset`
16+
* `\sync BarIndex Occurence MillisecondOffset RatioPosition`
17+
18+
Where:
19+
20+
* `BarIndex` is the numeric (0-based) index of the bar for which the sync point applies.
21+
* `Occurence` is the numeric (0-based) index of bar repetitions. e.g. on Repeats or Jumps bars might be played multiple times. This value allows specifying points on subsequent plays of a bar.
22+
* `MillisecondOffset` is the numeric timestamp in milliseconds in the external audio.
23+
* `RatioPosition` is the relative offset within the bar at which the sync point is placed (0 if not provided).
24+
25+
The `BarIndex`, `Occurence`, `RatioPosition` values define the absolute position within the music sheet.
26+
The `MillisecondOffset` defines the absolute position within the external media.
27+
28+
With this information known, alphaTab can synchronize the external media with the music sheet.
29+
30+
The sample below uses an audio backing track with inconsistent tempos. The sync points correct the tempo differences and the cursor is placed correctly.
31+
32+
import { AlphaTexSyncPointSample } from '@site/src/components/AlphaTexSyncPointSample';
33+
34+
<AlphaTexSyncPointSample />

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const sidebars: SidebarsConfig = {
140140
"alphatex/note-effects",
141141
"alphatex/percussion",
142142
"alphatex/lyrics",
143+
"alphatex/sync-points",
143144
],
144145
},
145146
showcase: [

src/components/AlphaTabPlayground/media-sync-editor.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
buildSyncPointInfoFromSynth,
2020
syncPointsToTypeScriptCode,
2121
syncPointsToCSharpCode,
22-
syncPointsToKotlinCode
22+
syncPointsToKotlinCode,
23+
syncPointsToAlphaTex
2324
} from './sync-point-info';
2425
import { WaveformCanvas } from './waveform-canvas';
2526
import { SyncPointMarkerPanel } from './sync-point-marker-panel';
@@ -374,7 +375,8 @@ export const MediaSyncEditor: React.FC<MediaSyncEditorProps> = ({
374375
values={[
375376
{ label: 'TypeScript', value: 'ts' },
376377
{ label: 'C#', value: 'cs' },
377-
{ label: 'Kotlin', value: 'kt' }
378+
{ label: 'Kotlin', value: 'kt' },
379+
{ label: 'alphaTex', value: 'at' }
378380
]}>
379381
<TabItem value="ts">
380382
<CodeBlock language="typescript" title="SyncPoints">
@@ -462,6 +464,15 @@ export const MediaSyncEditor: React.FC<MediaSyncEditorProps> = ({
462464
].join('\n')}
463465
</CodeBlock>
464466
</TabItem>
467+
<TabItem value="at">
468+
Place the following alphaTex at the end of your alphaTex song for applying the sync points on load.
469+
<CodeBlock title="alphaTex Sync Points">
470+
{[
471+
'.',
472+
syncPointsToAlphaTex(syncPointInfo),
473+
].join('\n')}
474+
</CodeBlock>
475+
</TabItem>
465476
</Tabs>
466477
<div className={styles['modal-actions']}>
467478
<button

src/components/AlphaTabPlayground/sync-point-info.ts

Lines changed: 20 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -303,124 +303,13 @@ function buildSyncPointMarkers(api: alphaTab.AlphaTabApi): SyncPointMarker[] {
303303
syncTime: syncTime,
304304
synthTime: synthTime,
305305
synthBpm: synthBpm,
306-
//syncBpm: syncBpm,
307306

308307
markerType: SyncPointMarkerType.EndMarker,
309308
});
310309
} else {
311-
//lastSyncPoint.syncBpm = syncBpm;
312310
lastSyncPoint.markerType = SyncPointMarkerType.EndMarker;
313311
}
314312

315-
316-
317-
// for (const masterBar of api.tickCache!.masterBars) {
318-
// const occurence = occurences.get(masterBar.masterBar.index) ?? 0;
319-
// occurences.set(masterBar.masterBar.index, occurence + 1);
320-
321-
// const duration = masterBar.end - masterBar.start;
322-
323-
// if (masterBar.masterBar.syncPoints) {
324-
// // if we have sync points we have to correctly walk through the points and tempo changes
325-
// // and place the markers accordingly
326-
327-
// let tempoChangeIndex = 0;
328-
// for (const syncPoint of masterBar.masterBar.syncPoints) {
329-
// if (syncPoint.syncPointValue!.barOccurence !== occurence) {
330-
// continue;
331-
// }
332-
333-
// const syncPointTick = masterBar.start + syncPoint.ratioPosition * duration;
334-
335-
// // first process all tempo change until this sync point
336-
// while (
337-
// tempoChangeIndex < masterBar.tempoChanges.length &&
338-
// masterBar.tempoChanges[tempoChangeIndex].tick <= syncPointTick
339-
// ) {
340-
// const tempoChange = masterBar.tempoChanges[tempoChangeIndex];
341-
// const absoluteTick = tempoChange.tick;
342-
// const tickOffset = absoluteTick - synthTickPosition;
343-
// if (tickOffset > 0) {
344-
// const timeOffset = ticksToMilliseconds(tickOffset, synthBpm);
345-
// synthTickPosition = absoluteTick;
346-
// synthTimePosition += timeOffset;
347-
// }
348-
349-
// synthBpm = tempoChange.tempo;
350-
// tempoChangeIndex++;
351-
// }
352-
353-
// // process time until sync point
354-
// const tickOffset = syncPointTick - synthTickPosition;
355-
// if (tickOffset > 0) {
356-
// synthTickPosition = syncPointTick;
357-
// const timeOffset = ticksToMilliseconds(tickOffset, synthBpm);
358-
// synthTimePosition += timeOffset;
359-
// }
360-
361-
// // create sync point marker
362-
// const newMarker: SyncPointMarker = {
363-
// masterBarIndex: masterBar.masterBar.index,
364-
// occurence: occurence,
365-
// syncTime: syncPoint.syncPointValue!.millisecondOffset,
366-
// synthTime: synthTimePosition,
367-
// synthBpm: masterBar.tempoChanges[0].tempo,
368-
// modifiedTempo: 0 /* calculated by next marker */,
369-
// markerType:
370-
// syncPoint.ratioPosition === 0
371-
// ? SyncPointMarkerType.MasterBar
372-
// : SyncPointMarkerType.Intermediate,
373-
// ratioPosition: syncPoint.ratioPosition,
374-
// synthTick: synthTickPosition
375-
// };
376-
// if (syncPointTick === 0) {
377-
// newMarker.markerType = SyncPointMarkerType.StartMarker;
378-
// }
379-
// markers.push(newMarker);
380-
381-
// if (markers.length > 0) {
382-
// updateModifiedTempo(markers.at(-1)!, newMarker.synthTime, newMarker.syncTime);
383-
// }
384-
// }
385-
386-
// // process remaining tempo changes after all sync points
387-
// while (tempoChangeIndex < masterBar.tempoChanges.length) {
388-
// const tempoChange = masterBar.tempoChanges[tempoChangeIndex];
389-
// const absoluteTick = tempoChange.tick;
390-
// const tickOffset = absoluteTick - synthTickPosition;
391-
// if (tickOffset > 0) {
392-
// const timeOffset = ticksToMilliseconds(tickOffset, synthBpm);
393-
// synthTickPosition = absoluteTick;
394-
// synthTimePosition += timeOffset;
395-
// }
396-
397-
// synthBpm = tempoChange.tempo;
398-
// tempoChangeIndex++;
399-
// }
400-
// }
401-
// }
402-
403-
// // at the very end we create the end marker
404-
// const lastMasterBar = api.tickCache!.masterBars.at(-1)!;
405-
// const endSyncPoint = lastMasterBar.masterBar.syncPoints?.find(m => m.ratioPosition === 1);
406-
407-
// const tickOffset = lastMasterBar.end - syncLastTick;
408-
// const endSyncPointTime = endSyncPoint
409-
// ? endSyncPoint.syncPointValue!.millisecondOffset
410-
// : syncLastMillisecondOffset + ticksToMilliseconds(tickOffset, syncBpm);
411-
412-
// markers.push({
413-
// masterBarIndex: lastMasterBar.masterBar.index,
414-
// occurence: occurences.get(lastMasterBar.masterBar.index)! - 1,
415-
// syncTime: endSyncPointTime,
416-
// synthTime: synthTimePosition,
417-
// synthBpm,
418-
// modifiedTempo: endSyncPoint?.syncPointValue?.modifiedTempo ?? synthBpm,
419-
// markerType: SyncPointMarkerType.EndMarker,
420-
// ratioPosition: 1,
421-
// synthTick: synthTickPosition
422-
// });
423-
424313
return markers;
425314
}
426315

@@ -524,99 +413,31 @@ export function autoSync(oldState: SyncPointInfo, api: alphaTab.AlphaTabApi, pad
524413

525414
// create initial sync points for all tempo changes to ensure the song and the
526415
// backing track roughly align
527-
let synthBpm = api.tickCache!.masterBars[0].tempoChanges[0].tempo;
528-
let synthTimePosition = 0;
529-
let synthTickPosition = 0;
530-
531-
const syncPoints: SyncPointMarker[] = [];
532-
533-
// first create all changes not respecting the song start and end
534-
const occurences = new Map<number, number>();
535-
for (const masterBar of api.tickCache!.masterBars) {
536-
const occurence = occurences.get(masterBar.masterBar.index) ?? 0;
537-
occurences.set(masterBar.masterBar.index, occurence + 1);
538-
539-
// we are guaranteed to have a tempo change per master bar indicating its own tempo
540-
// (even though its not a change)
541-
for (const changes of masterBar.tempoChanges) {
542-
const absoluteTick = changes.tick;
543-
const tickOffset = absoluteTick - synthTickPosition;
544-
if (tickOffset > 0) {
545-
const timeOffset = ticksToMilliseconds(tickOffset, synthBpm);
546-
synthTickPosition = absoluteTick;
547-
synthTimePosition += timeOffset;
548-
}
549-
550-
const marker: SyncPointMarker = {
551-
uniqueId: uid(),
552-
markerType: SyncPointMarkerType.MasterBar,
553-
masterBarIndex: masterBar.masterBar.index,
554-
masterBarStart: masterBar.start,
555-
masterBarEnd: masterBar.end,
556-
occurence,
557-
syncTime: synthTimePosition,
558-
synthBpm,
559-
synthTime: synthTimePosition,
560-
syncBpm: undefined,
561-
synthTick: synthTickPosition
562-
};
563-
564-
if (masterBar.start === 0) {
565-
marker.markerType = SyncPointMarkerType.StartMarker;
566-
} else if (changes.tick > masterBar.start) {
567-
marker.markerType = SyncPointMarkerType.Intermediate;
568-
}
569-
570-
if (changes.tempo !== synthBpm || marker.markerType === SyncPointMarkerType.StartMarker) {
571-
syncPoints.push(marker);
572-
marker.syncBpm = changes.tempo;
573-
}
574-
575-
synthBpm = changes.tempo;
576-
577-
state.syncPointMarkers.push(marker);
578-
}
579-
580-
const tickOffset = masterBar.end - synthTickPosition;
581-
const timeOffset = ticksToMilliseconds(tickOffset, synthBpm);
582-
synthTickPosition += tickOffset;
583-
synthTimePosition += timeOffset;
584-
}
585-
586-
// end marker
587-
const lastMasterBar = api.tickCache!.masterBars.at(-1)!;
588-
state.syncPointMarkers.push({
589-
uniqueId: uid(),
590-
masterBarIndex: lastMasterBar.masterBar.index,
591-
masterBarStart: lastMasterBar.start,
592-
masterBarEnd: lastMasterBar.end,
593-
occurence: occurences.get(lastMasterBar.masterBar.index)! - 1,
594-
syncTime: synthTimePosition,
595-
synthTime: synthTimePosition,
596-
synthBpm,
597-
syncBpm: synthBpm,
598-
markerType: SyncPointMarkerType.EndMarker,
599-
synthTick: synthTickPosition
600-
});
416+
417+
state.syncPointMarkers = buildSyncPointMarkers(api);
601418

602419
// with the final durations known, we can "squeeze" together the song
603420
// from start and end (keeping the relative positions)
604421
// and the other bars will be adjusted accordingly
605422
if (padToAudio) {
606423
const [songStart, songEnd] = findAudioStartAndEnd(state);
607424

608-
const synthDuration = synthTimePosition;
425+
const synthDuration = state.syncPointMarkers.at(-1)!.synthTime;
609426
const realDuration = songEnd - songStart;
610427
const scaleFactor = realDuration / synthDuration;
428+
429+
state.syncPointMarkers.at(0)!.syncBpm = state.syncPointMarkers.at(0)!.synthBpm;
430+
state.syncPointMarkers.at(-1)!.syncBpm = state.syncPointMarkers.at(-1)!.synthBpm;
611431

612432
// 1st Pass: shift all tempo change markers relatively and calculate BPM
433+
const syncPoints = state.syncPointMarkers.filter(m => m.syncBpm !== undefined);
613434
let syncTime = songStart;
614435
for (let i = 0; i < syncPoints.length; i++) {
615436
const syncPoint = syncPoints[i];
616437

617438
syncPoint.syncTime = syncTime;
618439

619-
if (i < 0) {
440+
if (i > 0) {
620441
const previousMarker = syncPoints[i - 1];
621442
const synthDuration = syncPoint.synthTime - previousMarker.synthTime;
622443
const syncedDuration = syncPoint.syncTime - previousMarker.syncTime;
@@ -898,3 +719,15 @@ export function syncPointsToKotlinCode(info: SyncPointInfo, indent: string): str
898719

899720
return lines.join(',\n');
900721
}
722+
723+
export function syncPointsToAlphaTex(info: SyncPointInfo): string {
724+
const lines: string[] = [];
725+
726+
const flat = toFlatSyncPoints(info);
727+
for (const m of flat) {
728+
const barPosition = m.barPosition > 0 ? ` ${Number(m.barPosition.toFixed(3))}` : '';
729+
lines.push(`\\sync ${m.barIndex} ${m.barOccurence} ${m.millisecondOffset}${barPosition}`)
730+
}
731+
732+
return lines.join('\n');
733+
}

0 commit comments

Comments
 (0)