diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts
index 400647a18..549da5689 100644
--- a/packages/alphatab/src/importer/MusicXmlImporter.ts
+++ b/packages/alphatab/src/importer/MusicXmlImporter.ts
@@ -2232,7 +2232,7 @@ export class MusicXmlImporter extends ScoreImporter {
if (unit !== null && perMinute > 0) {
const tempoAutomation: Automation = new Automation();
tempoAutomation.type = AutomationType.Tempo;
- tempoAutomation.value = (perMinute * (unit / 4)) | 0;
+ tempoAutomation.value = perMinute * (unit / 4);
tempoAutomation.ratioPosition = ratioPosition;
if (!this._hasSameTempo(masterBar, tempoAutomation)) {
diff --git a/packages/alphatab/src/synth/MidiFileSequencer.ts b/packages/alphatab/src/synth/MidiFileSequencer.ts
index 4cf255579..1feac5b3e 100644
--- a/packages/alphatab/src/synth/MidiFileSequencer.ts
+++ b/packages/alphatab/src/synth/MidiFileSequencer.ts
@@ -268,7 +268,7 @@ export class MidiFileSequencer {
if (mEvent.type === MidiEventType.TempoChange) {
const meta: TempoChangeEvent = mEvent as TempoChangeEvent;
- bpm = meta.beatsPerMinute;
+ bpm = MidiFileSequencer._sanitizeBpm(meta.beatsPerMinute);
state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime));
metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division));
} else if (mEvent.type === MidiEventType.TimeSignature) {
@@ -405,7 +405,7 @@ export class MidiFileSequencer {
previousTick = 0;
} else {
const previousSyncPoint = syncPoints[i - 1];
- previousModifiedTempo = previousSyncPoint.syncBpm;
+ previousModifiedTempo = MidiFileSequencer._sanitizeBpm(previousSyncPoint.syncBpm);
previousMillisecondOffset = previousSyncPoint.syncTime;
previousTick = previousSyncPoint.synthTick;
}
@@ -436,7 +436,7 @@ export class MidiFileSequencer {
syncPoint.syncBpm = previousModifiedTempo;
}
- bpm = state.tempoChanges[tempoChangeIndex].bpm;
+ bpm = MidiFileSequencer._sanitizeBpm(state.tempoChanges[tempoChangeIndex].bpm);
tempoChangeIndex++;
}
@@ -462,11 +462,16 @@ export class MidiFileSequencer {
this._updateCurrentTempo(state, timePosition);
const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
const timeDiff = timePosition - lastTempoChange.time;
- const ticks = (timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0;
+ const ticks =
+ (timeDiff / (60000.0 / (MidiFileSequencer._sanitizeBpm(lastTempoChange.bpm) * state.division))) | 0;
// we add 1 for possible rounding errors.(floating point issuses)
return lastTempoChange.ticks + ticks + 1;
}
+ private static _sanitizeBpm(bpm: number) {
+ return Math.max(bpm, 1); // prevent <0 bpms. Doesn't make sense and can cause endless loops
+ }
+
public currentUpdateCurrentTempo(timePosition: number) {
this._updateCurrentTempo(this._mainState, timePosition * this.playbackSpeed);
}
diff --git a/packages/alphatab/test-data/audio/small-tempo.xml b/packages/alphatab/test-data/audio/small-tempo.xml
new file mode 100644
index 000000000..0b0ef609f
--- /dev/null
+++ b/packages/alphatab/test-data/audio/small-tempo.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+ Drums
+
+ Drum Set
+
+
+ 10
+ 1
+
+
+
+
+
+
+ 4
+
+ 0
+
+
+
+ percussion
+
+
+
+
+
+ quarter
+ 0.111
+
+
+
+
+
+
+ 16
+ 1
+
+
+
+
+
+ 16
+ 1
+
+
+
+
+
+ 16
+ 1
+
+
+
+
\ No newline at end of file
diff --git a/packages/alphatab/test/audio/AlphaSynth.test.ts b/packages/alphatab/test/audio/AlphaSynth.test.ts
index 36e3a9ef2..a75abe4de 100644
--- a/packages/alphatab/test/audio/AlphaSynth.test.ts
+++ b/packages/alphatab/test/audio/AlphaSynth.test.ts
@@ -2,7 +2,12 @@ import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader';
import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer';
import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler';
import { ControllerType } from '@coderline/alphatab/midi/ControllerType';
-import { type ControlChangeEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent';
+import {
+ type ControlChangeEvent,
+ type MidiEvent,
+ MidiEventType,
+ TempoChangeEvent
+} from '@coderline/alphatab/midi/MidiEvent';
import { MidiFile } from '@coderline/alphatab/midi/MidiFile';
import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator';
import type { Score } from '@coderline/alphatab/model/Score';
@@ -12,9 +17,9 @@ import { AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter';
import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';
import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont';
import { VorbisFile } from '@coderline/alphatab/synth/vorbis/VorbisFile';
+import { assert, expect } from 'chai';
import { TestOutput } from 'test/audio/TestOutput';
import { TestPlatform } from 'test/TestPlatform';
-import { expect } from 'chai';
describe('AlphaSynthTests', () => {
it('pcm-generation', async () => {
@@ -332,4 +337,62 @@ describe('AlphaSynthTests', () => {
expect(synth.channelGetPresetBank(2)).to.equal(4000);
expect(synth.channelGetPresetBank(3)).to.equal(4000);
});
+
+ async function testPlaythrough(midi: MidiFile) {
+ const testOutput = new TestOutput(false);
+ const synth = new AlphaSynth(testOutput, 500);
+ const soundFont = await TestPlatform.loadFile('test-data/audio/default.sf2');
+ synth.loadSoundFont(soundFont, false);
+ synth.loadMidiFile(midi);
+ synth.play();
+ let finished = false;
+ synth.finished.on(() => {
+ finished = true;
+ });
+
+ const start = Date.now();
+
+ while (!finished) {
+ const now = Date.now();
+ if (now - start > 2000) {
+ assert.fail(`play did not complete after ${2000}ms`);
+ }
+ testOutput.next();
+ }
+ }
+
+ it('small-tempos', async () => {
+ const score = ScoreLoader.loadScoreFromBytes(await TestPlatform.loadFile('test-data/audio/small-tempo.xml'));
+
+ expect(score.masterBars[0].tempoAutomations[0].value).to.equal(0.111);
+
+ const midi = new MidiFile();
+ const handler = new AlphaSynthMidiFileHandler(midi);
+ const generator = new MidiFileGenerator(score, null, handler);
+ generator.generate();
+
+ const tempoChange: MidiEvent[] = midi.events.filter(e => e instanceof TempoChangeEvent);
+ expect(tempoChange.length).to.equal(1);
+ expect((tempoChange[0] as TempoChangeEvent).beatsPerMinute).to.equal(0.111);
+
+ await testPlaythrough(midi);
+ });
+
+ it('zero-tempo', async () => {
+ const score = ScoreLoader.loadScoreFromBytes(await TestPlatform.loadFile('test-data/audio/small-tempo.xml'));
+
+ expect(score.masterBars[0].tempoAutomations[0].value).to.equal(0.111);
+ score.masterBars[0].tempoAutomations[0].value = 0;
+
+ const midi = new MidiFile();
+ const handler = new AlphaSynthMidiFileHandler(midi);
+ const generator = new MidiFileGenerator(score, null, handler);
+ generator.generate();
+
+ const tempoChange: MidiEvent[] = midi.events.filter(e => e instanceof TempoChangeEvent);
+ expect(tempoChange.length).to.equal(1);
+ expect((tempoChange[0] as TempoChangeEvent).beatsPerMinute).to.equal(0);
+
+ await testPlaythrough(midi);
+ });
});
diff --git a/packages/alphatab/test/audio/TestOutput.ts b/packages/alphatab/test/audio/TestOutput.ts
index b227c78f0..cddc79b58 100644
--- a/packages/alphatab/test/audio/TestOutput.ts
+++ b/packages/alphatab/test/audio/TestOutput.ts
@@ -1,5 +1,10 @@
import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput';
-import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter';
+import {
+ EventEmitter,
+ type IEventEmitter,
+ type IEventEmitterOfT,
+ EventEmitterOfT
+} from '@coderline/alphatab/EventEmitter';
import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';
/**
@@ -8,11 +13,16 @@ import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';
export class TestOutput implements ISynthOutput {
public samples: Float32Array[] = [];
public sampleCount: number = 0;
+ private _storeSamples: boolean;
public get sampleRate(): number {
return 44100;
}
+ public constructor(storeSamples: boolean = true) {
+ this._storeSamples = storeSamples;
+ }
+
public open(_bufferTimeInMilliseconds: number): void {
this.samples = [];
(this.ready as EventEmitter).trigger();
@@ -35,7 +45,9 @@ export class TestOutput implements ISynthOutput {
}
public addSamples(f: Float32Array): void {
- this.samples.push(f);
+ if (this._storeSamples) {
+ this.samples.push(f);
+ }
this.sampleCount += f.length;
(this.samplesPlayed as EventEmitterOfT).trigger(f.length / SynthConstants.AudioChannels);
}
diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts
index b007e246d..d5fb108f7 100644
--- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts
+++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts
@@ -1,9 +1,9 @@
-import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper';
-import type { Score } from '@coderline/alphatab/model/Score';
import { BendType } from '@coderline/alphatab/model/BendType';
-import { expect } from 'chai';
import { JsonConverter } from '@coderline/alphatab/model/JsonConverter';
import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet';
+import type { Score } from '@coderline/alphatab/model/Score';
+import { expect } from 'chai';
+import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper';
describe('MusicXmlImporterTests', () => {
it('track-volume', async () => {