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
48 changes: 33 additions & 15 deletions packages/alphatab/src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ class MidiNoteDuration {
public noteOnly: number = 0;
public untilTieOrSlideEnd: number = 0;
public letRingEnd: number = 0;
/**
* A factor indicating how much longer/shorter the beat is in its playback respecting
* effects like tuplets, triplet feels, dots, grace beats stealing parts etc.
*
* This factor can be used to relatively adjust durations in effects like trills or tremolos.
*/
public beatDurationFactor: number = 1;
}

/**
Expand Down Expand Up @@ -142,9 +149,6 @@ export class MidiFileGenerator {
this._generateTrack(track);
}

// tickshift is added after initial track channel details
this._detectTickShift();

Logger.debug('Midi', 'Begin midi generation');

this.syncPoints = [];
Expand All @@ -154,6 +158,10 @@ export class MidiFileGenerator {
false,
(bar, previousMasterBar, currentTick, currentTempo, occurence) => {
this._generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
if (bar.index === 0 && occurence === 0) {
// tickshift is added after initial track channel details
this._detectTickShift();
}
},
(index, currentTick, currentTempo) => {
for (const track of this._score.tracks) {
Expand Down Expand Up @@ -1158,15 +1166,19 @@ export class MidiFileGenerator {
this._handler.addNote(track.index, noteStart, remaining, noteKey, velocity, channel);
}

private _getNoteDuration(note: Note, duration: number, tempoOnBeatStart: number): MidiNoteDuration {
private _getNoteDuration(note: Note, beatPlayDuration: number, tempoOnBeatStart: number): MidiNoteDuration {
const durationWithEffects: MidiNoteDuration = new MidiNoteDuration();
durationWithEffects.noteOnly = duration;
durationWithEffects.untilTieOrSlideEnd = duration;
durationWithEffects.letRingEnd = duration;

const defaultBeatDuration = MidiUtils.toTicks(note.beat.duration);
durationWithEffects.beatDurationFactor = beatPlayDuration / defaultBeatDuration;

durationWithEffects.noteOnly = beatPlayDuration;
durationWithEffects.untilTieOrSlideEnd = beatPlayDuration;
durationWithEffects.letRingEnd = beatPlayDuration;
if (note.isDead) {
durationWithEffects.noteOnly = this._applyStaticDuration(
MidiFileGenerator._defaultDurationDead,
duration,
beatPlayDuration,
tempoOnBeatStart
);
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
Expand All @@ -1176,15 +1188,15 @@ export class MidiFileGenerator {
if (note.isPalmMute) {
durationWithEffects.noteOnly = this._applyStaticDuration(
MidiFileGenerator._defaultDurationPalmMute,
duration,
beatPlayDuration,
tempoOnBeatStart
);
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
durationWithEffects.letRingEnd = durationWithEffects.noteOnly;
return durationWithEffects;
}
if (note.isStaccato) {
durationWithEffects.noteOnly = (duration / 2) | 0;
durationWithEffects.noteOnly = (beatPlayDuration / 2) | 0;
durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly;
durationWithEffects.letRingEnd = durationWithEffects.noteOnly;
return durationWithEffects;
Expand All @@ -1211,7 +1223,8 @@ export class MidiFileGenerator {
endNote.beat.playbackDuration,
tempoOnBeatStart
);
durationWithEffects.untilTieOrSlideEnd = duration + tieDestinationDuration.untilTieOrSlideEnd;
durationWithEffects.untilTieOrSlideEnd =
beatPlayDuration + tieDestinationDuration.untilTieOrSlideEnd;
}
}
} else if (note.slideOutType === SlideOutType.Legato) {
Expand Down Expand Up @@ -1254,7 +1267,7 @@ export class MidiFileGenerator {
}
}
if (lastLetRingBeat === note.beat) {
durationWithEffects.letRingEnd = duration;
durationWithEffects.letRingEnd = beatPlayDuration;
} else {
durationWithEffects.letRingEnd = letRingEnd;
}
Expand Down Expand Up @@ -1966,7 +1979,10 @@ export class MidiFileGenerator {
): void {
const track: Track = note.beat.voice.bar.staff.track;
const trillKey: number = note.stringTuning + note.trillFret;
// NOTE: no noteDuration.beatDurationFactor, the trill speed is absolute and not dependent on the
// beat effects
let trillLength: number = MidiUtils.toTicks(note.trillSpeed);

let realKey: boolean = true;
let tick: number = noteStart;
const end: number = noteStart + noteDuration.untilTieOrSlideEnd;
Expand All @@ -1975,7 +1991,7 @@ export class MidiFileGenerator {
if (tick + trillLength >= end) {
trillLength = end - tick;
}
this._handler.addNote(track.index, tick, trillLength, realKey ? trillKey : noteKey, dynamicValue, channel);
this._handler.addNote(track.index, tick, trillLength, realKey ? noteKey : trillKey, dynamicValue, channel);
realKey = !realKey;
tick += trillLength;
}
Expand All @@ -1994,9 +2010,11 @@ export class MidiFileGenerator {
if (marks === 0) {
return;
}

// the marks represent the duration
let tpLength = MidiUtils.toTicks(note.beat.tremoloPicking!.duration);
let tpLength =
note.beat.tremoloPicking!.getDurationAsTicks(note.beat.duration) * noteDuration.beatDurationFactor;

let tick: number = noteStart;
const end: number = noteStart + noteDuration.untilTieOrSlideEnd;

Expand Down
2 changes: 1 addition & 1 deletion packages/alphatab/src/model/Beat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export class Beat {
public get tremoloSpeed(): Duration | null {
const tremolo = this.tremoloPicking;
if (tremolo) {
return tremolo.duration;
return tremolo.getDuration(this.duration);
}
return null;
}
Expand Down
29 changes: 24 additions & 5 deletions packages/alphatab/src/model/TremoloPickingEffect.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
import { Duration } from '@coderline/alphatab/model/Duration';

/**
Expand Down Expand Up @@ -45,16 +46,34 @@ export class TremoloPickingEffect {
public style: TremoloPickingStyle = TremoloPickingStyle.Default;

/**
* The number of marks define the note value of the note repetition.
* e.g. a single mark is an 8th note.
* @internal
* @deprecated use {@link getDurationAsTicks} to handle tremolo durations shorter than typical durations.
*/
public get duration(): Duration {
public getDuration(beatDuration: Duration): Duration {
let marks = this.marks;
if (marks < 1) {
marks = 1;
}
const baseDuration = Duration.Eighth as number;
const baseDuration = beatDuration as number;
const actualDuration = baseDuration * Math.pow(2, marks);
return actualDuration as Duration;
if (actualDuration <= Duration.TwoHundredFiftySixth) {
return actualDuration as Duration;
} else {
return Duration.TwoHundredFiftySixth;
}
}

/**
* Gets the duration of a single tremolo note played in a beat of the given duration
* based on the configured marks.
*/
public getDurationAsTicks(beatDuration: Duration): number {
let marks = this.marks;
if (marks < 1) {
marks = 1;
}
const baseDuration = beatDuration as number;
const actualDuration = baseDuration * Math.pow(2, marks);
return MidiUtils.valueToTicks(actualDuration);
}
}
5 changes: 3 additions & 2 deletions packages/alphatab/src/model/TupletGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ export class TupletGroup {
// by checking all potential note durations.
// this logic is very likely not 100% correct but for most cases the tuplets
// appeared correct.
if (beat.playbackDuration !== this.beats[0].playbackDuration) {

if (beat.displayDuration !== this.beats[0].displayDuration) {
this._isEqualLengthTuplet = false;
}
this.beats.push(beat);
this.totalDuration += beat.playbackDuration;
this.totalDuration += beat.displayDuration;
if (this._isEqualLengthTuplet) {
if (this.beats.length === this.beats[0].tupletNumerator) {
this.isFull = true;
Expand Down
Binary file modified packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 157 additions & 1 deletion packages/alphatab/test/audio/MidiFileGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { GraceType } from '@coderline/alphatab/model/GraceType';
import type { Note } from '@coderline/alphatab/model/Note';
import type { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation';
import type { Score } from '@coderline/alphatab/model/Score';
import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect';
import { Tuning } from '@coderline/alphatab/model/Tuning';
import { VibratoType } from '@coderline/alphatab/model/VibratoType';
import { Settings } from '@coderline/alphatab/Settings';
import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth';
Expand All @@ -32,7 +34,7 @@ import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/Positio
import { expect } from 'chai';
import {
FlatControlChangeEvent,
type FlatMidiEvent,
FlatMidiEvent,
FlatMidiEventGenerator,
FlatNoteBendEvent,
FlatNoteEvent,
Expand Down Expand Up @@ -1934,4 +1936,158 @@ describe('MidiFileGeneratorTest', () => {
expect(lastArgs!.currentTick).to.equal(tickImprecision);
expect(synth.tickPosition).to.equal(handler.tickShift + tickImprecision);
});

describe('effect-note-durations', () => {
function test(tex: string, applyEffect: (beat: Beat) => void) {
const score = ScoreLoader.loadAlphaTex(tex);
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
while (beat) {
applyEffect(beat);
beat = beat.nextBeat;
}

const settings = new Settings();
settings.player.playTripletFeel = true;

score.finish(settings);

const flat = new FlatMidiEventGenerator();
const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, flat);
generator.generate();

const noteEvents = flat.midiEvents
.filter<FlatMidiEvent>(e => e instanceof FlatNoteEvent)
.map(
e =>
`Note: ${Tuning.getTextForTuning((e as FlatNoteEvent).key, true)} ${(e as FlatNoteEvent).length}`
);

expect(noteEvents).toMatchSnapshot();
}

function addTrill(b: Beat) {
if (b.graceType !== GraceType.None) {
return;
}
b.notes[0].trillValue = b.notes[0].realValue + 12;
b.notes[0].trillSpeed = Duration.ThirtySecond;
}

function addTremolo(b: Beat, marks: number) {
if (b.graceType !== GraceType.None) {
return;
}
b.tremoloPicking = new TremoloPickingEffect();
b.tremoloPicking.marks = marks;
b.tremoloPicking.style = TremoloPickingStyle.Default;
}

// as reference to check snapshots
describe('plain', () => {
const tex = `
:8
5.3
7.3
9.3
10.3
`;

it('tripletfeel', () => test(`\\tf triplet8th ${tex}`, _b => {}));
it('tuplet', () =>
test(tex, b => {
b.tupletNumerator = 3;
b.tupletDenominator = 2;
}));
it('dot', () =>
test(tex, b => {
b.dots = 1;
}));

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

describe('tuplet', () => {
const tex = `
:8
5.3 {tu 3}
7.3 {tu 3}
9.3 {tu 3}
`;

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

describe('dots', () => {
const tex = `
:8
5.3 {d}
7.3 {d}
9.3 {d}
`;

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

describe('triplet-feel', () => {
const tex = `
\\tf triplet8th
:8
5.3
7.3
9.3
10.3
`;

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

describe('grace-notes-on-beat', () => {
const tex = `
:8
3.5 {gr ob}
5.3
5.5 {gr ob}
7.3
7.5 {gr ob}
9.3
9.5 {gr ob}
10.3
`;

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

describe('grace-notes-before-beat', () => {
const tex = `
:8
5.3 {gr bb}
5.3
7.3 {gr bb}
7.3
9.3 {gr bb}
9.3
10.3 {gr bb}
10.3
`;

it('trill', () => test(tex, b => addTrill(b)));
it('tremolo-2', () => test(tex, b => addTremolo(b, 2)));
it('tremolo-3', () => test(tex, b => addTremolo(b, 3)));
});

// NOTE: there might be more affected effects which we assume are "good enough" with the
// current behavior. combining these effects are rather unlikely like:
// * brush-strokes combined with trills or tremolos
// * rasgueados combined with trills or tremolos
});
});
Loading
Loading