diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts
index b5eb34d08f..a087482bd6 100644
--- a/packages/alphatab/src/exporter/GpifWriter.ts
+++ b/packages/alphatab/src/exporter/GpifWriter.ts
@@ -664,7 +664,7 @@ export class GpifWriter {
}
if (beat.showTimer) {
- beatNode.addElement('Timer').innerText = (beat.timer ?? 0).toString();
+ beatNode.addElement('Timer').innerText = Math.round(beat.timer ?? 0).toString();
}
this._writeBeatProperties(beatNode, beat);
diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts
index c2c23b33d8..be18148493 100644
--- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts
+++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts
@@ -472,7 +472,7 @@ export class AlphaTex1LanguageDefinitions {
[
[[106], 0],
[[106], 0],
- [[106], 0],
+ [[106], 2],
[[106], 3]
]
]
diff --git a/packages/alphatab/src/midi/MidiUtils.ts b/packages/alphatab/src/midi/MidiUtils.ts
index 7b27059cf5..773133a3a3 100644
--- a/packages/alphatab/src/midi/MidiUtils.ts
+++ b/packages/alphatab/src/midi/MidiUtils.ts
@@ -16,7 +16,7 @@ export class MidiUtils {
* @returns The converted duration in milliseconds.
*/
public static ticksToMillis(ticks: number, tempo: number): number {
- return (ticks * (60000.0 / (tempo * MidiUtils.QuarterTime))) | 0;
+ return (ticks * 60000.0) / (tempo * MidiUtils.QuarterTime);
}
/**
diff --git a/packages/alphatab/test-data/audio/export-sync-points.pcm b/packages/alphatab/test-data/audio/export-sync-points.pcm
index 39c6e11e24..148a4c94cf 100644
Binary files a/packages/alphatab/test-data/audio/export-sync-points.pcm and b/packages/alphatab/test-data/audio/export-sync-points.pcm differ
diff --git a/packages/alphatab/test/audio/MidiFileGenerator.test.ts b/packages/alphatab/test/audio/MidiFileGenerator.test.ts
index 8296ee72c3..15b67ca6d7 100644
--- a/packages/alphatab/test/audio/MidiFileGenerator.test.ts
+++ b/packages/alphatab/test/audio/MidiFileGenerator.test.ts
@@ -1933,6 +1933,32 @@ describe('MidiFileGeneratorTest', () => {
expect(actualTimers.join(',')).toBe(expectedTimers.join(','));
});
+ /**
+ * See #2952
+ */
+ it('beat-timer-non-integer-bar-duration', () => {
+ // a 4/4 bar at 148 BPM lasts 1621.6216...ms
+ const barCount = 200;
+ const barTex: string[] = [];
+ for (let i = 0; i < barCount; i++) {
+ barTex.push('C4.4 { timer } C4.4 * 3');
+ }
+ const score: Score = parseTex(`
+ \\tempo 148
+ .
+ ${barTex.join(' | ')}
+ `);
+
+ const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
+ const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
+ generator.generate();
+
+ const bars = score.tracks[0].staves[0].bars;
+ for (let i = 0; i < barCount; i++) {
+ expect(bars[i].voices[0].beats[0].timer, `bar ${i}`).toBeCloseTo((i * 4 * 60000) / 148, 6);
+ }
+ });
+
describe('metronome', () => {
function testMetronome(tex: string, expectedClicks: string) {
const score: Score = parseTex(tex);
diff --git a/packages/alphatab/test/audio/SyncPoint.test.ts b/packages/alphatab/test/audio/SyncPoint.test.ts
index 8526753d61..95cd902db1 100644
--- a/packages/alphatab/test/audio/SyncPoint.test.ts
+++ b/packages/alphatab/test/audio/SyncPoint.test.ts
@@ -6,6 +6,7 @@ import type { AlphaTabMetronomeEvent } from '@coderline/alphatab/midi/MidiEvent'
import { MidiFile } from '@coderline/alphatab/midi/MidiFile';
import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator';
import type { BackingTrack } from '@coderline/alphatab/model/BackingTrack';
+import type { Score } from '@coderline/alphatab/model/Score';
import { Settings } from '@coderline/alphatab/Settings';
import { BackingTrackPlayer, type IBackingTrackSynthOutput } from '@coderline/alphatab/synth/BackingTrackPlayer';
import {
@@ -13,6 +14,7 @@ import {
type IExternalMediaHandler,
type IExternalMediaSynthOutput
} from '@coderline/alphatab/synth/ExternalMediaPlayer';
+import type { IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth';
import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer';
import type { ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput';
import { MidiFileSequencer } from '@coderline/alphatab/synth/MidiFileSequencer';
@@ -357,6 +359,89 @@ describe('SyncPointTests', () => {
expect(events.map(e => `${e.currentTime},${e.originalTempo},${e.modifiedTempo}`)).toMatchSnapshot();
expect(testOutput.seekTimes).toMatchSnapshot();
});
+
+ /**
+ * See #2952
+ */
+ describe('non-integer-bar-duration', () => {
+ // a 4/4 bar at 148 BPM lasts 1621.6216...ms
+ const tempo = 148;
+ const barCount = 201;
+ const ticksPerBar = 3840;
+ const barMillis = (4 * 60000) / tempo;
+ const lastSyncTime = (barCount - 1) * barMillis;
+
+ function createScore(): Score {
+ const barTex: string[] = [];
+ for (let i = 0; i < barCount; i++) {
+ barTex.push('C4.4 * 4');
+ }
+ return ScoreLoader.loadAlphaTex(`
+ \\tempo ${tempo}
+ .
+ ${barTex.join(' | ')}
+ .
+ \\sync 0 0 0
+ \\sync ${barCount - 1} 0 ${lastSyncTime}
+ `);
+ }
+
+ function createGenerator(score: Score, midi: MidiFile): MidiFileGenerator {
+ const generator = new MidiFileGenerator(score, new Settings(), new AlphaSynthMidiFileHandler(midi));
+ generator.generate();
+ return generator;
+ }
+
+ function expectPlayerInSync(player: IAlphaSynth, seek: (time: number) => void) {
+ const score = createScore();
+ const midi = new MidiFile();
+ const generator = createGenerator(score, midi);
+
+ player.loadMidiFile(midi);
+ player.loadBackingTrack(score);
+ player.updateSyncPoints(generator.syncPoints);
+
+ let tick = -1;
+ player.positionChanged.on(e => {
+ tick = e.currentTick;
+ });
+ player.play();
+
+ for (let bar = 0; bar < barCount; bar++) {
+ seek(bar * barMillis);
+ const expectedTick = bar * ticksPerBar;
+ expect(Math.abs(tick - expectedTick), `bar ${bar}: tick ${tick}`).toBeLessThanOrEqual(1);
+ }
+ }
+
+ it('sync-point-times', () => {
+ const generator = createGenerator(createScore(), new MidiFile());
+
+ const syncPoints = generator.syncPoints;
+ expect(syncPoints[1].synthTick).toBe((barCount - 1) * ticksPerBar);
+ expect(syncPoints[1].syncTime).toBe(lastSyncTime);
+ expect(syncPoints[1].synthTime).toBeCloseTo(lastSyncTime, 6);
+ expect(syncPoints[0].syncBpm).toBeCloseTo(tempo, 6);
+ });
+
+ it('external-media-position', () => {
+ const player = new ExternalMediaPlayer(500);
+ const output = player.output as IExternalMediaSynthOutput;
+ const mediaHandler = new TestExternalMediaHandler(output);
+ mediaHandler.backingTrackDuration = lastSyncTime + barMillis;
+ output.handler = mediaHandler;
+
+ expectPlayerInSync(player, time => mediaHandler.simulateSeek(time));
+ });
+
+ it('backing-track-position', () => {
+ const output = new TestBackingTrackOutput();
+ output.backingTrackDuration = lastSyncTime + barMillis;
+ const player = new BackingTrackPlayer(output, 500);
+
+ expectPlayerInSync(player, time => output.simulateSeek(time));
+ });
+ });
});
describe('MidiFileSequencerCountInTests', () => {
diff --git a/packages/alphatab/test/audio/__snapshots__/SyncPoint.test.ts.snap b/packages/alphatab/test/audio/__snapshots__/SyncPoint.test.ts.snap
index 99ad5e9795..3e01356c2f 100644
--- a/packages/alphatab/test/audio/__snapshots__/SyncPoint.test.ts.snap
+++ b/packages/alphatab/test/audio/__snapshots__/SyncPoint.test.ts.snap
@@ -10,7 +10,7 @@ exports[`SyncPointTests > modified-tempo-lookup 1`] = `
"22010.34013605442 => 120.04899959166994",
"24009.52380952381 => 120.04899959167003",
"30007.074829931975 => 80.01088583480758",
- "33006.666666666664 => 80.01778172927317",
+ "33006.666666666664 => 80.01088583480758",
]
`;
@@ -50,15 +50,15 @@ exports[`SyncPointTests > playback-fast-backing-track 1`] = `
"18496.530140155122,80,160.02177166961516",
"18996.59817662267,80,160.02177166961516",
"19496.66621309022,80,160.02177166961516",
- "19996.77706156924,80,160.03556345854633",
- "20496.888197377197,80,160.03556345854633",
- "20996.999333185155,80,160.03556345854633",
- "21497.11046899311,80,160.03556345854633",
- "21997.221604801067,80,160.03556345854633",
- "22497.332740609025,80,160.03556345854633",
- "22997.443876416983,80,160.03556345854633",
- "23497.55501222494,80,160.03556345854633",
- "23997.6661480329,80,160.03556345854633",
+ "19996.734249557765,80,160.02177166961516",
+ "20496.80228602531,80,160.02177166961516",
+ "20996.87032249286,80,160.02177166961516",
+ "21496.938358960408,80,160.02177166961516",
+ "21997.006395427954,80,160.02177166961516",
+ "22497.0744318955,80,160.02177166961516",
+ "22997.14246836305,80,160.02177166961516",
+ "23497.210504830597,80,160.02177166961516",
+ "23997.278541298143,80,160.02177166961516",
]
`;
@@ -98,15 +98,15 @@ exports[`SyncPointTests > playback-fast-external-media 1`] = `
"18496.530140155122,80,160.02177166961516",
"18996.59817662267,80,160.02177166961516",
"19496.66621309022,80,160.02177166961516",
- "19996.77706156924,80,160.03556345854633",
- "20496.888197377197,80,160.03556345854633",
- "20996.999333185155,80,160.03556345854633",
- "21497.11046899311,80,160.03556345854633",
- "21997.221604801067,80,160.03556345854633",
- "22497.332740609025,80,160.03556345854633",
- "22997.443876416983,80,160.03556345854633",
- "23497.55501222494,80,160.03556345854633",
- "23997.6661480329,80,160.03556345854633",
+ "19996.734249557765,80,160.02177166961516",
+ "20496.80228602531,80,160.02177166961516",
+ "20996.87032249286,80,160.02177166961516",
+ "21496.938358960408,80,160.02177166961516",
+ "21997.006395427954,80,160.02177166961516",
+ "22497.0744318955,80,160.02177166961516",
+ "22997.14246836305,80,160.02177166961516",
+ "23497.210504830597,80,160.02177166961516",
+ "23997.278541298143,80,160.02177166961516",
]
`;
@@ -179,24 +179,24 @@ exports[`SyncPointTests > playback-normal-backing-track 1`] = `
"37993.19635324534,80,80.01088583480758",
"38493.26438971289,80,80.01088583480758",
"38993.33242618044,80,80.01088583480758",
- "39493.44298733052,80,80.01778172927317",
- "39993.55412313848,80,80.01778172927317",
- "40493.665258946436,80,80.01778172927317",
- "40993.77639475439,80,80.01778172927317",
- "41493.88753056235,80,80.01778172927317",
- "41993.99866637031,80,80.01778172927317",
- "42494.10980217827,80,80.01778172927317",
- "42994.22093798622,80,80.01778172927317",
- "43494.332073794176,80,80.01778172927317",
- "43994.443209602134,80,80.01778172927317",
- "44494.55434541009,80,80.01778172927317",
- "44994.66548121805,80,80.01778172927317",
- "45494.77661702601,80,80.01778172927317",
- "45994.887752833965,80,80.01778172927317",
- "46494.99888864192,80,80.01778172927317",
- "46995.11002444988,80,80.01778172927317",
- "47495.22116025784,80,80.01778172927317",
- "47995.3322960658,80,80.01778172927317",
+ "39493.40046264798,80,80.01088583480758",
+ "39993.46849911553,80,80.01088583480758",
+ "40493.53653558308,80,80.01088583480758",
+ "40993.60457205062,80,80.01088583480758",
+ "41493.67260851817,80,80.01088583480758",
+ "41993.74064498572,80,80.01088583480758",
+ "42493.808681453265,80,80.01088583480758",
+ "42993.876717920815,80,80.01088583480758",
+ "43493.94475438836,80,80.01088583480758",
+ "43994.01279085591,80,80.01088583480758",
+ "44494.08082732346,80,80.01088583480758",
+ "44994.148863791,80,80.01088583480758",
+ "45494.21690025855,80,80.01088583480758",
+ "45994.2849367261,80,80.01088583480758",
+ "46494.35297319364,80,80.01088583480758",
+ "46994.42100966119,80,80.01088583480758",
+ "47494.48904612874,80,80.01088583480758",
+ "47994.557082596286,80,80.01088583480758",
]
`;
@@ -269,24 +269,24 @@ exports[`SyncPointTests > playback-normal-external-media 1`] = `
"37993.19635324534,80,80.01088583480758",
"38493.26438971289,80,80.01088583480758",
"38993.33242618044,80,80.01088583480758",
- "39493.44298733052,80,80.01778172927317",
- "39993.55412313848,80,80.01778172927317",
- "40493.665258946436,80,80.01778172927317",
- "40993.77639475439,80,80.01778172927317",
- "41493.88753056235,80,80.01778172927317",
- "41993.99866637031,80,80.01778172927317",
- "42494.10980217827,80,80.01778172927317",
- "42994.22093798622,80,80.01778172927317",
- "43494.332073794176,80,80.01778172927317",
- "43994.443209602134,80,80.01778172927317",
- "44494.55434541009,80,80.01778172927317",
- "44994.66548121805,80,80.01778172927317",
- "45494.77661702601,80,80.01778172927317",
- "45994.887752833965,80,80.01778172927317",
- "46494.99888864192,80,80.01778172927317",
- "46995.11002444988,80,80.01778172927317",
- "47495.22116025784,80,80.01778172927317",
- "47995.3322960658,80,80.01778172927317",
+ "39493.40046264798,80,80.01088583480758",
+ "39993.46849911553,80,80.01088583480758",
+ "40493.53653558308,80,80.01088583480758",
+ "40993.60457205062,80,80.01088583480758",
+ "41493.67260851817,80,80.01088583480758",
+ "41993.74064498572,80,80.01088583480758",
+ "42493.808681453265,80,80.01088583480758",
+ "42993.876717920815,80,80.01088583480758",
+ "43493.94475438836,80,80.01088583480758",
+ "43994.01279085591,80,80.01088583480758",
+ "44494.08082732346,80,80.01088583480758",
+ "44994.148863791,80,80.01088583480758",
+ "45494.21690025855,80,80.01088583480758",
+ "45994.2849367261,80,80.01088583480758",
+ "46494.35297319364,80,80.01088583480758",
+ "46994.42100966119,80,80.01088583480758",
+ "47494.48904612874,80,80.01088583480758",
+ "47994.557082596286,80,80.01088583480758",
]
`;
@@ -311,7 +311,7 @@ exports[`SyncPointTests > seek-fast-backing-track 2`] = `
4000,
8001.292517006803,
23343.12925170068,
- 36006,
+ 36006.258503401354,
]
`;
@@ -336,7 +336,7 @@ exports[`SyncPointTests > seek-fast-external-media 2`] = `
4000,
8001.292517006803,
23343.12925170068,
- 36006,
+ 36006.258503401354,
]
`;
@@ -398,8 +398,8 @@ exports[`SyncPointTests > sync-point-generation 1`] = `
"8,0,80,120.04899959166994,24000,22010.34013605442",
"9,0,80,120.04899959167003,27000,24009.52380952381",
"8,1,80,80.01088583480758,36000,30007.074829931975",
- "9,1,80,80.01778172927317,39000,33006.666666666664",
- "11,0,80,80.01088583480758,48000,42004.666666666664",
+ "9,1,80,80.01088583480758,39000,33006.666666666664",
+ "11,0,80,80.01088583480758,48000,42005.44217687073",
]
`;
@@ -422,7 +422,7 @@ exports[`SyncPointTests > sync-point-update 1`] = `
"8,0,80,120.04899959166994,24000,22010.34013605442",
"9,0,80,120.04899959167003,27000,24009.52380952381",
"8,1,80,80.01088583480758,36000,30007.074829931975",
- "9,1,80,80.01778172927317,39000,33006.666666666664",
- "11,0,80,80.01088583480758,48000,42004.666666666664",
+ "9,1,80,80.01088583480758,39000,33006.666666666664",
+ "11,0,80,80.01088583480758,48000,42005.44217687073",
]
`;
diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts
index af7ded3912..eaa1e4459d 100644
--- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts
+++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts
@@ -184,6 +184,20 @@ describe('Gp7ExporterTest', () => {
expect(gpif).toContain('1>');
});
+ it('beat-timer-as-integer', () => {
+ const score = ScoreLoader.loadAlphaTex(`
+ C4.4 { timer } C4.4 { timer }
+ `);
+ const beats = score.tracks[0].staves[0].bars[0].voices[0].beats;
+ beats[0].timer = 1621.4;
+ beats[1].timer = 3243.6;
+
+ const gpif = readExportedGpif(exportGp7(score));
+
+ expect(gpif).toContain('1621');
+ expect(gpif).toContain('3244');
+ });
+
it('alphatex-drums-to-gp7', () => {
const tex = `\\track "Drums" { instrument percussion }
\\clef neutral
diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts
index c37fd57fa7..58880c5537 100644
--- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts
+++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts
@@ -2241,6 +2241,31 @@ describe('AlphaTexImporterTest', () => {
testExportRoundtrip(score);
});
+ it('sync-float-millisecond-offset', () => {
+ const score = parseTex(`
+ \\tempo 148
+ .
+ 3.3.4*4 | 3.3.4*4 | 3.3.4*4 | 3.3.4*4
+ .
+ \\sync 0 0 0
+ \\sync 1 0 1621.6216216216217
+ \\sync 2 0 3243.25 0.5
+ \\sync (3 0 4864.865)
+ `);
+
+ expect(score.masterBars.length).toBe(4);
+ expect(score.tracks[0].staves[0].bars[3].voices[0].beats.length).toBe(4);
+ const syncPoints = score.exportFlatSyncPoints();
+ expect(syncPoints.map(p => `${p.barIndex},${p.barOccurence},${p.barPosition},${p.millisecondOffset}`)).toEqual([
+ '0,0,0,0',
+ '1,0,0,1621.6216216216217',
+ '2,0,0.5,3243.25',
+ '3,0,0,4864.865'
+ ]);
+
+ testExportRoundtrip(score);
+ });
+
it('tuning-name', () => {
const score = parseTex(`
\\tuning E4 B3 G3 D3 A2 E2 "Default"
diff --git a/packages/alphatex/src/metadata/bar/sync.ts b/packages/alphatex/src/metadata/bar/sync.ts
index 03cf2eb864..cdb24edfbf 100644
--- a/packages/alphatex/src/metadata/bar/sync.ts
+++ b/packages/alphatex/src/metadata/bar/sync.ts
@@ -34,7 +34,7 @@ export const sync: MetadataTagDefinition = {
{
name: 'millisecondOffset',
shortDescription: 'The absolute millisecond offset within the external media',
- parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required,
+ parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.RequiredAsFloat,
type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number
},
{