Skip to content

Commit 3d31c01

Browse files
authored
fix(player): grace beats on song start (#2415)
1 parent 09bb0ab commit 3d31c01

File tree

11 files changed

+305
-30
lines changed

11 files changed

+305
-30
lines changed

packages/alphatab/src/AlphaTabApiBase.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
22
import type { CoreSettings } from '@coderline/alphatab/CoreSettings';
33
import { Environment } from '@coderline/alphatab/Environment';
4-
import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter';
4+
import {
5+
EventEmitter,
6+
EventEmitterOfT,
7+
type IEventEmitter,
8+
type IEventEmitterOfT
9+
} from '@coderline/alphatab/EventEmitter';
510
import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter';
611
import { Logger } from '@coderline/alphatab/Logger';
712
import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler';
@@ -42,11 +47,10 @@ import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4247
import type { Note } from '@coderline/alphatab/model/Note';
4348
import type { Score } from '@coderline/alphatab/model/Score';
4449
import type { Track } from '@coderline/alphatab/model/Track';
45-
import { PlayerMode, ScrollMode } from '@coderline/alphatab/PlayerSettings';
4650
import type { IContainer } from '@coderline/alphatab/platform/IContainer';
4751
import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventArgs';
4852
import type { IUiFacade } from '@coderline/alphatab/platform/IUiFacade';
49-
import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs';
53+
import { PlayerMode, ScrollMode } from '@coderline/alphatab/PlayerSettings';
5054
import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph';
5155
import type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer';
5256
import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs';
@@ -57,12 +61,17 @@ import type { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
5761
import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup';
5862
import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds';
5963
import type { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds';
64+
import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs';
6065
import type { Settings } from '@coderline/alphatab/Settings';
6166
import { ActiveBeatsChangedEventArgs } from '@coderline/alphatab/synth/ActiveBeatsChangedEventArgs';
6267
import { AlphaSynthWrapper } from '@coderline/alphatab/synth/AlphaSynthWrapper';
6368
import { ExternalMediaPlayer } from '@coderline/alphatab/synth/ExternalMediaPlayer';
6469
import type { IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth';
65-
import { AudioExportOptions, type IAudioExporter, type IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter';
70+
import {
71+
AudioExportOptions,
72+
type IAudioExporter,
73+
type IAudioExporterWorker
74+
} from '@coderline/alphatab/synth/IAudioExporter';
6675
import type { ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput';
6776
import type { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs';
6877
import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';
@@ -100,6 +109,15 @@ export class AlphaTabApiBase<TSettings> {
100109
private _player!: AlphaSynthWrapper;
101110
private _renderer: ScoreRendererWrapper;
102111

112+
/**
113+
* An indicator by how many midi-ticks the song contents are shifted.
114+
* Grace beats at start might require a shift for the first beat to start at 0.
115+
* This information can be used to translate back the player time axis to the music notation.
116+
*/
117+
public get midiTickShift() {
118+
return this._player.midiTickShift;
119+
}
120+
103121
/**
104122
* The actual player mode which is currently active.
105123
* @remarks
@@ -1537,6 +1555,7 @@ export class AlphaTabApiBase<TSettings> {
15371555
this._onMidiLoad(midiFile);
15381556

15391557
const player = this._player;
1558+
player.midiTickShift = handler.tickShift;
15401559
player.loadMidiFile(midiFile);
15411560
player.loadBackingTrack(score);
15421561
player.updateSyncPoints(generator.syncPoints);
@@ -3578,10 +3597,12 @@ export class AlphaTabApiBase<TSettings> {
35783597
return;
35793598
}
35803599

3581-
this._previousTick = e.currentTick;
3600+
const currentTick = e.currentTick;
3601+
3602+
this._previousTick = currentTick;
35823603
this.uiFacade.beginInvoke(() => {
35833604
const cursorSpeed = e.modifiedTempo / e.originalTempo;
3584-
this._cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
3605+
this._cursorUpdateTick(currentTick, false, cursorSpeed, false, e.isSeek);
35853606
});
35863607

35873608
this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);

packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
2424
private _midiFile: MidiFile;
2525
private _smf1Mode: boolean;
2626

27+
/**
28+
* An indicator by how many midi-ticks the song contents are shifted.
29+
* Grace beats at start might require a shift for the first beat to start at 0.
30+
* This information can be used to translate back the player time axis to the music notation.
31+
*/
32+
public tickShift: number = 0;
33+
2734
/**
2835
* Initializes a new instance of the {@link AlphaSynthMidiFileHandler} class.
2936
* @param midiFile The midi file.
@@ -34,7 +41,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
3441
this._smf1Mode = smf1Mode;
3542
}
3643

44+
public addTickShift(tickShift: number) {
45+
this._midiFile.tickShift = tickShift;
46+
this.tickShift = tickShift;
47+
}
48+
3749
public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void {
50+
tick += this.tickShift;
51+
3852
let denominatorIndex: number = 0;
3953
let denominator = timeSignatureDenominator;
4054
while (true) {
@@ -50,12 +64,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
5064
}
5165

5266
public addRest(track: number, tick: number, channel: number): void {
67+
tick += this.tickShift;
5368
if (!this._smf1Mode) {
5469
this._midiFile.addEvent(new AlphaTabRestEvent(track, tick, channel));
5570
}
5671
}
5772

5873
public addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void {
74+
start += this.tickShift;
5975
this._midiFile.addEvent(
6076
new NoteOnEvent(
6177
track,
@@ -94,23 +110,27 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
94110
controller: ControllerType,
95111
value: number
96112
): void {
113+
tick += this.tickShift;
97114
this._midiFile.addEvent(
98115
new ControlChangeEvent(track, tick, channel, controller, AlphaSynthMidiFileHandler._fixValue(value))
99116
);
100117
}
101118

102119
public addProgramChange(track: number, tick: number, channel: number, program: number): void {
120+
tick += this.tickShift;
103121
this._midiFile.addEvent(new ProgramChangeEvent(track, tick, channel, program));
104122
}
105123

106124
public addTempo(tick: number, tempo: number): void {
125+
tick += this.tickShift;
107126
// bpm -> microsecond per quarter note
108127
const tempoEvent = new TempoChangeEvent(tick, 0);
109128
tempoEvent.beatsPerMinute = tempo;
110129
this._midiFile.addEvent(tempoEvent);
111130
}
112131

113132
public addBend(track: number, tick: number, channel: number, value: number): void {
133+
tick += this.tickShift;
114134
if (value >= SynthConstants.MaxPitchWheel) {
115135
value = SynthConstants.MaxPitchWheel;
116136
} else {
@@ -120,6 +140,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
120140
}
121141

122142
public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void {
143+
tick += this.tickShift;
123144
if (this._smf1Mode) {
124145
this.addBend(track, tick, channel, value);
125146
} else {
@@ -132,6 +153,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
132153
}
133154

134155
public finishTrack(track: number, tick: number): void {
156+
tick += this.tickShift;
135157
if (this._midiFile.format === MidiFileFormat.MultiTrack || track === 0) {
136158
this._midiFile.addEvent(new EndOfTrackEvent(track, tick));
137159
}

packages/alphatab/src/midi/IMidiFileHandler.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,13 @@ export interface IMidiFileHandler {
8585
* @param tick The end tick for this track.
8686
*/
8787
finishTrack(track: number, tick: number): void;
88+
89+
90+
/**
91+
* Registers a general shift of the time-axis for the generate midi file.
92+
* @param tickShift The shift in midi ticks by which all midi events beside the initial channel setups are shifted.
93+
* This shift is applied in case grace beats
94+
*/
95+
addTickShift(tickShift: number): void;
96+
8897
}

packages/alphatab/src/midi/MidiFile.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ export class MidiFile {
8787
* Gets or sets the division per quarter notes.
8888
*/
8989
public division: number = MidiUtils.QuarterTime;
90+
91+
/**
92+
* An indicator by how many midi-ticks the song contents are shifted.
93+
* Grace beats at start might require a shift for the first beat to start at 0.
94+
* This information can be used to translate back the player time axis to the music notation.
95+
*/
96+
public tickShift: number = 0;
9097

9198
/**
9299
* Gets a list of midi events sorted by time.

packages/alphatab/src/midi/MidiFileGenerator.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,16 @@ export class MidiFileGenerator {
137137
this._calculatedBeatTimers.clear();
138138
this._currentTime = 0;
139139

140+
140141
// initialize tracks
141142
for (const track of this._score.tracks) {
142143
this._generateTrack(track);
143144
}
144145

146+
// tickshift is added after initial track channel details
147+
this._detectTickShift();
148+
149+
145150
Logger.debug('Midi', 'Begin midi generation');
146151

147152
this.syncPoints = [];
@@ -171,6 +176,24 @@ export class MidiFileGenerator {
171176
Logger.debug('Midi', 'Midi generation done');
172177
}
173178

179+
private _detectTickShift() {
180+
let tickShift = 0;
181+
for (const track of this._score.tracks) {
182+
for (const staff of track.staves) {
183+
for (const voice of staff.bars[0].voices) {
184+
if (!voice.isEmpty) {
185+
const beat = voice.beats[0];
186+
if (beat.playbackStart < tickShift) {
187+
tickShift = beat.playbackStart;
188+
}
189+
}
190+
}
191+
}
192+
}
193+
tickShift = Math.abs(tickShift);
194+
this._handler.addTickShift(tickShift);
195+
}
196+
174197
private _generateTrack(track: Track): void {
175198
// channel
176199
this._generateChannel(track, track.playbackInfo.primaryChannel, track.playbackInfo);
@@ -1313,7 +1336,13 @@ export class MidiFileGenerator {
13131336
}
13141337
}
13151338

1316-
private _generateFadeSteps(track: Track, start: number, duration: number, startVolume: number, endVolume: number): void {
1339+
private _generateFadeSteps(
1340+
track: Track,
1341+
start: number,
1342+
duration: number,
1343+
startVolume: number,
1344+
endVolume: number
1345+
): void {
13171346
const tickStep: number = 120;
13181347
// we want to reach the target volume a bit earlier than the end of the note
13191348
duration = (duration * 0.8) | 0;

packages/alphatab/src/midi/MidiTickLookup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,9 @@ export class MidiTickLookup {
553553
}
554554

555555
private _findMasterBar(tick: number): MasterBarTickLookup | null {
556+
if (tick <= 0 && this.masterBars.length > 0) {
557+
return this.masterBars[0];
558+
}
556559
const bars: MasterBarTickLookup[] = this.masterBars;
557560
let bottom: number = 0;
558561
let top: number = bars.length - 1;

packages/alphatab/src/model/JsonConverter.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
2+
import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer';
3+
import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer';
4+
import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';
5+
import type { ControllerType } from '@coderline/alphatab/midi/ControllerType';
16
import {
27
AlphaTabMetronomeEvent,
38
AlphaTabRestEvent,
@@ -17,11 +22,6 @@ import {
1722
import { MidiFile, MidiTrack } from '@coderline/alphatab/midi/MidiFile';
1823
import { Score } from '@coderline/alphatab/model/Score';
1924
import { Settings } from '@coderline/alphatab/Settings';
20-
import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer';
21-
import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer';
22-
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
23-
import type { ControllerType } from '@coderline/alphatab/midi/ControllerType';
24-
import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';
2525

2626
/**
2727
* This class can convert a full {@link Score} instance to a simple JavaScript object and back for further
@@ -144,6 +144,9 @@ export class JsonConverter {
144144

145145
JsonHelper.forEach(jsObject, (v, k) => {
146146
switch (k) {
147+
case 'tickShift':
148+
midi2.tickShift = v as number;
149+
break;
147150
case 'division':
148151
midi2.division = v as number;
149152
break;
@@ -271,6 +274,7 @@ export class JsonConverter {
271274
public static midiFileToJsObject(midi: MidiFile): Map<string, unknown> {
272275
const o = new Map<string, unknown>();
273276
o.set('division', midi.division);
277+
o.set('tickShift', midi.tickShift);
274278

275279
const tracks: Map<string, unknown>[] = [];
276280
for (const track of midi.tracks) {

0 commit comments

Comments
 (0)