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
2 changes: 1 addition & 1 deletion packages/alphatab/src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
13 changes: 9 additions & 4 deletions packages/alphatab/src/synth/MidiFileSequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -436,7 +436,7 @@ export class MidiFileSequencer {
syncPoint.syncBpm = previousModifiedTempo;
}

bpm = state.tempoChanges[tempoChangeIndex].bpm;
bpm = MidiFileSequencer._sanitizeBpm(state.tempoChanges[tempoChangeIndex].bpm);
tempoChangeIndex++;
}

Expand All @@ -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);
}
Expand Down
67 changes: 67 additions & 0 deletions packages/alphatab/test-data/audio/small-tempo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
<work>
<work-title></work-title>
</work>
<identification>
<creator type="composer"></creator>
</identification>
<part-list>
<score-part id="P1">
<part-name>Drums</part-name>
<score-instrument id="P1-I1">
<instrument-name>Drum Set</instrument-name>
</score-instrument>
<midi-instrument id="P1-I1">
<midi-channel>10</midi-channel>
<midi-program>1</midi-program>
</midi-instrument>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>4</divisions>
<key>
<fifths>0</fifths>
</key>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>percussion</sign>
</clef>
</attributes>
<direction placement="above">
<direction-type>
<metronome>
<beat-unit>quarter</beat-unit>
<per-minute>0.111</per-minute>
</metronome>
</direction-type>
<sound tempo="0.111" />
</direction>
<note>
<rest measure="yes" />
<duration>16</duration>
<voice>1</voice>
</note>
</measure>
<measure number="2">
<note>
<rest measure="yes" />
<duration>16</duration>
<voice>1</voice>
</note>
</measure>
<measure number="3">
<note>
<rest measure="yes" />
<duration>16</duration>
<voice>1</voice>
</note>
</measure>
</part>
</score-partwise>
67 changes: 65 additions & 2 deletions packages/alphatab/test/audio/AlphaSynth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
16 changes: 14 additions & 2 deletions packages/alphatab/test/audio/TestOutput.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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();
Expand All @@ -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<number>).trigger(f.length / SynthConstants.AudioChannels);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/alphatab/test/importer/MusicXmlImporter.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Loading