Skip to content

Commit 2e8601e

Browse files
committed
fix: ensure correct note durations for combined duration annotations
1 parent 0fa3828 commit 2e8601e

File tree

5 files changed

+542
-16
lines changed

5 files changed

+542
-16
lines changed

packages/alphatab/src/midi/MidiFileGenerator.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class MidiNoteDuration {
4949
public noteOnly: number = 0;
5050
public untilTieOrSlideEnd: number = 0;
5151
public letRingEnd: number = 0;
52+
/**
53+
* A factor indicating how much longer/shorter the beat is in its playback respecting
54+
* effects like tuplets, triplet feels, dots, grace beats stealing parts etc.
55+
*
56+
* This factor can be used to relatively adjust durations in effects like trills or tremolos.
57+
*/
58+
public beatDurationFactor: number = 1;
5259
}
5360

5461
/**
@@ -1158,15 +1165,19 @@ export class MidiFileGenerator {
11581165
this._handler.addNote(track.index, noteStart, remaining, noteKey, velocity, channel);
11591166
}
11601167

1161-
private _getNoteDuration(note: Note, duration: number, tempoOnBeatStart: number): MidiNoteDuration {
1168+
private _getNoteDuration(note: Note, beatPlayDuration: number, tempoOnBeatStart: number): MidiNoteDuration {
11621169
const durationWithEffects: MidiNoteDuration = new MidiNoteDuration();
1163-
durationWithEffects.noteOnly = duration;
1164-
durationWithEffects.untilTieOrSlideEnd = duration;
1165-
durationWithEffects.letRingEnd = duration;
1170+
1171+
const defaultBeatDuration = MidiUtils.toTicks(note.beat.duration);
1172+
durationWithEffects.beatDurationFactor = beatPlayDuration / defaultBeatDuration;
1173+
1174+
durationWithEffects.noteOnly = beatPlayDuration;
1175+
durationWithEffects.untilTieOrSlideEnd = beatPlayDuration;
1176+
durationWithEffects.letRingEnd = beatPlayDuration;
11661177
if (note.isDead) {
11671178
durationWithEffects.noteOnly = this._applyStaticDuration(
11681179
MidiFileGenerator._defaultDurationDead,
1169-
duration,
1180+
beatPlayDuration,
11701181
tempoOnBeatStart
11711182
);
11721183
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
@@ -1176,15 +1187,15 @@ export class MidiFileGenerator {
11761187
if (note.isPalmMute) {
11771188
durationWithEffects.noteOnly = this._applyStaticDuration(
11781189
MidiFileGenerator._defaultDurationPalmMute,
1179-
duration,
1190+
beatPlayDuration,
11801191
tempoOnBeatStart
11811192
);
11821193
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
11831194
durationWithEffects.letRingEnd = durationWithEffects.noteOnly;
11841195
return durationWithEffects;
11851196
}
11861197
if (note.isStaccato) {
1187-
durationWithEffects.noteOnly = (duration / 2) | 0;
1198+
durationWithEffects.noteOnly = (beatPlayDuration / 2) | 0;
11881199
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
11891200
durationWithEffects.letRingEnd = durationWithEffects.noteOnly;
11901201
return durationWithEffects;
@@ -1211,7 +1222,8 @@ export class MidiFileGenerator {
12111222
endNote.beat.playbackDuration,
12121223
tempoOnBeatStart
12131224
);
1214-
durationWithEffects.untilTieOrSlideEnd = duration + tieDestinationDuration.untilTieOrSlideEnd;
1225+
durationWithEffects.untilTieOrSlideEnd =
1226+
beatPlayDuration + tieDestinationDuration.untilTieOrSlideEnd;
12151227
}
12161228
}
12171229
} else if (note.slideOutType === SlideOutType.Legato) {
@@ -1254,7 +1266,7 @@ export class MidiFileGenerator {
12541266
}
12551267
}
12561268
if (lastLetRingBeat === note.beat) {
1257-
durationWithEffects.letRingEnd = duration;
1269+
durationWithEffects.letRingEnd = beatPlayDuration;
12581270
} else {
12591271
durationWithEffects.letRingEnd = letRingEnd;
12601272
}
@@ -1966,7 +1978,10 @@ export class MidiFileGenerator {
19661978
): void {
19671979
const track: Track = note.beat.voice.bar.staff.track;
19681980
const trillKey: number = note.stringTuning + note.trillFret;
1981+
// NOTE: no noteDuration.beatDurationFactor, the trill speed is absolute and not dependent on the
1982+
// beat effects
19691983
let trillLength: number = MidiUtils.toTicks(note.trillSpeed);
1984+
19701985
let realKey: boolean = true;
19711986
let tick: number = noteStart;
19721987
const end: number = noteStart + noteDuration.untilTieOrSlideEnd;
@@ -1975,7 +1990,7 @@ export class MidiFileGenerator {
19751990
if (tick + trillLength >= end) {
19761991
trillLength = end - tick;
19771992
}
1978-
this._handler.addNote(track.index, tick, trillLength, realKey ? trillKey : noteKey, dynamicValue, channel);
1993+
this._handler.addNote(track.index, tick, trillLength, realKey ? noteKey : trillKey, dynamicValue, channel);
19791994
realKey = !realKey;
19801995
tick += trillLength;
19811996
}
@@ -1994,9 +2009,10 @@ export class MidiFileGenerator {
19942009
if (marks === 0) {
19952010
return;
19962011
}
1997-
2012+
19982013
// the marks represent the duration
1999-
let tpLength = MidiUtils.toTicks(note.beat.tremoloPicking!.duration);
2014+
let tpLength = MidiUtils.toTicks(note.beat.tremoloPicking!.duration) * noteDuration.beatDurationFactor;
2015+
20002016
let tick: number = noteStart;
20012017
const end: number = noteStart + noteDuration.untilTieOrSlideEnd;
20022018

packages/alphatab/src/model/TremoloPickingEffect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class TremoloPickingEffect {
5454
marks = 1;
5555
}
5656
const baseDuration = Duration.Eighth as number;
57-
const actualDuration = baseDuration * Math.pow(2, marks);
57+
const actualDuration = baseDuration * Math.pow(2, marks - 1);
5858
return actualDuration as Duration;
5959
}
6060
}

packages/alphatab/test/audio/MidiFileGenerator.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { GraceType } from '@coderline/alphatab/model/GraceType';
2323
import type { Note } from '@coderline/alphatab/model/Note';
2424
import type { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation';
2525
import type { Score } from '@coderline/alphatab/model/Score';
26+
import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect';
27+
import { Tuning } from '@coderline/alphatab/model/Tuning';
2628
import { VibratoType } from '@coderline/alphatab/model/VibratoType';
2729
import { Settings } from '@coderline/alphatab/Settings';
2830
import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth';
@@ -1934,4 +1936,155 @@ describe('MidiFileGeneratorTest', () => {
19341936
expect(lastArgs!.currentTick).to.equal(tickImprecision);
19351937
expect(synth.tickPosition).to.equal(handler.tickShift + tickImprecision);
19361938
});
1939+
1940+
describe('effect-note-durations', () => {
1941+
function test(tex: string, applyEffect: (beat: Beat) => void) {
1942+
const score = ScoreLoader.loadAlphaTex(tex);
1943+
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
1944+
while (beat) {
1945+
applyEffect(beat);
1946+
beat = beat.nextBeat;
1947+
}
1948+
1949+
const settings = new Settings();
1950+
settings.player.playTripletFeel = true;
1951+
1952+
score.finish(settings);
1953+
1954+
const flat = new FlatMidiEventGenerator();
1955+
const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, flat);
1956+
generator.generate();
1957+
1958+
const noteEvents = flat.midiEvents
1959+
.filter(e => e instanceof FlatNoteEvent)
1960+
.map(e => `Note: ${Tuning.getTextForTuning(e.key, true)} ${e.length}`);
1961+
1962+
expect(noteEvents).toMatchSnapshot();
1963+
}
1964+
1965+
function addTrill(b: Beat) {
1966+
if (b.graceType !== GraceType.None) {
1967+
return;
1968+
}
1969+
b.notes[0].trillValue = b.notes[0].realValue + 12;
1970+
b.notes[0].trillSpeed = Duration.ThirtySecond;
1971+
}
1972+
1973+
function addTremolo(b: Beat, marks: number) {
1974+
if (b.graceType !== GraceType.None) {
1975+
return;
1976+
}
1977+
b.tremoloPicking = new TremoloPickingEffect();
1978+
b.tremoloPicking.marks = marks;
1979+
b.tremoloPicking.style = TremoloPickingStyle.Default;
1980+
}
1981+
1982+
// as reference to check snapshots
1983+
describe('plain', () => {
1984+
const tex = `
1985+
:8
1986+
5.3
1987+
7.3
1988+
9.3
1989+
10.3
1990+
`;
1991+
1992+
it('tripletfeel', () => test(`\\tf triplet8th ${tex}`, b => b));
1993+
it('tuplet', () =>
1994+
test(tex, b => {
1995+
b.tupletNumerator = 3;
1996+
b.tupletDenominator = 2;
1997+
}));
1998+
it('dot', () =>
1999+
test(tex, b => {
2000+
b.dots = 1;
2001+
}));
2002+
2003+
it('trill', () => test(tex, b => addTrill(b)));
2004+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2005+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2006+
});
2007+
2008+
describe('tuplet', () => {
2009+
const tex = `
2010+
:8
2011+
5.3 {tu 3}
2012+
7.3 {tu 3}
2013+
9.3 {tu 3}
2014+
`;
2015+
2016+
it('trill', () => test(tex, b => addTrill(b)));
2017+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2018+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2019+
});
2020+
2021+
describe('dots', () => {
2022+
const tex = `
2023+
:8
2024+
5.3 {d}
2025+
7.3 {d}
2026+
9.3 {d}
2027+
`;
2028+
2029+
it('trill', () => test(tex, b => addTrill(b)));
2030+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2031+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2032+
});
2033+
2034+
describe('triplet-feel', () => {
2035+
const tex = `
2036+
\\tf triplet8th
2037+
:8
2038+
5.3
2039+
7.3
2040+
9.3
2041+
10.3
2042+
`;
2043+
2044+
it('trill', () => test(tex, b => addTrill(b)));
2045+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2046+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2047+
});
2048+
2049+
describe('grace-notes-on-beat', () => {
2050+
const tex = `
2051+
:8
2052+
3.5 {gr ob}
2053+
5.3
2054+
5.5 {gr ob}
2055+
7.3
2056+
7.5 {gr ob}
2057+
9.3
2058+
9.5 {gr ob}
2059+
10.3
2060+
`;
2061+
2062+
it('trill', () => test(tex, b => addTrill(b)));
2063+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2064+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2065+
});
2066+
2067+
describe('grace-notes-before-beat', () => {
2068+
const tex = `
2069+
:8
2070+
5.3 {gr bb}
2071+
5.3
2072+
7.3 {gr bb}
2073+
7.3
2074+
9.3 {gr bb}
2075+
9.3
2076+
10.3 {gr bb}
2077+
10.3
2078+
`;
2079+
2080+
it('trill', () => test(tex, b => addTrill(b)));
2081+
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
2082+
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
2083+
});
2084+
2085+
// NOTE: there might be more affected effects which we assume are "good enough" with the
2086+
// current behavior. combining these effects are rather unlikely like:
2087+
// * brush-strokes combined with trills or tremolos
2088+
// * rasgueados combined with trills or tremolos
2089+
});
19372090
});

0 commit comments

Comments
 (0)