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
33 changes: 27 additions & 6 deletions packages/alphatab/src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
import type { CoreSettings } from '@coderline/alphatab/CoreSettings';
import { Environment } from '@coderline/alphatab/Environment';
import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter';
import {
EventEmitter,
EventEmitterOfT,
type IEventEmitter,
type IEventEmitterOfT
} from '@coderline/alphatab/EventEmitter';
import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter';
import { Logger } from '@coderline/alphatab/Logger';
import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler';
Expand Down Expand Up @@ -42,11 +47,10 @@ import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
import type { Note } from '@coderline/alphatab/model/Note';
import type { Score } from '@coderline/alphatab/model/Score';
import type { Track } from '@coderline/alphatab/model/Track';
import { PlayerMode, ScrollMode } from '@coderline/alphatab/PlayerSettings';
import type { IContainer } from '@coderline/alphatab/platform/IContainer';
import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventArgs';
import type { IUiFacade } from '@coderline/alphatab/platform/IUiFacade';
import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs';
import { PlayerMode, ScrollMode } from '@coderline/alphatab/PlayerSettings';
import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph';
import type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer';
import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs';
Expand All @@ -57,12 +61,17 @@ import type { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup';
import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds';
import type { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds';
import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs';
import type { Settings } from '@coderline/alphatab/Settings';
import { ActiveBeatsChangedEventArgs } from '@coderline/alphatab/synth/ActiveBeatsChangedEventArgs';
import { AlphaSynthWrapper } from '@coderline/alphatab/synth/AlphaSynthWrapper';
import { ExternalMediaPlayer } from '@coderline/alphatab/synth/ExternalMediaPlayer';
import type { IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth';
import { AudioExportOptions, type IAudioExporter, type IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter';
import {
AudioExportOptions,
type IAudioExporter,
type IAudioExporterWorker
} from '@coderline/alphatab/synth/IAudioExporter';
import type { ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput';
import type { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs';
import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';
Expand Down Expand Up @@ -100,6 +109,15 @@ export class AlphaTabApiBase<TSettings> {
private _player!: AlphaSynthWrapper;
private _renderer: ScoreRendererWrapper;

/**
* An indicator by how many midi-ticks the song contents are shifted.
* Grace beats at start might require a shift for the first beat to start at 0.
* This information can be used to translate back the player time axis to the music notation.
*/
public get midiTickShift() {
return this._player.midiTickShift;
}

/**
* The actual player mode which is currently active.
* @remarks
Expand Down Expand Up @@ -1537,6 +1555,7 @@ export class AlphaTabApiBase<TSettings> {
this._onMidiLoad(midiFile);

const player = this._player;
player.midiTickShift = handler.tickShift;
player.loadMidiFile(midiFile);
player.loadBackingTrack(score);
player.updateSyncPoints(generator.syncPoints);
Expand Down Expand Up @@ -3578,10 +3597,12 @@ export class AlphaTabApiBase<TSettings> {
return;
}

this._previousTick = e.currentTick;
const currentTick = e.currentTick;

this._previousTick = currentTick;
this.uiFacade.beginInvoke(() => {
const cursorSpeed = e.modifiedTempo / e.originalTempo;
this._cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
this._cursorUpdateTick(currentTick, false, cursorSpeed, false, e.isSeek);
});

this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
Expand Down
22 changes: 22 additions & 0 deletions packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
private _midiFile: MidiFile;
private _smf1Mode: boolean;

/**
* An indicator by how many midi-ticks the song contents are shifted.
* Grace beats at start might require a shift for the first beat to start at 0.
* This information can be used to translate back the player time axis to the music notation.
*/
public tickShift: number = 0;

/**
* Initializes a new instance of the {@link AlphaSynthMidiFileHandler} class.
* @param midiFile The midi file.
Expand All @@ -34,7 +41,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
this._smf1Mode = smf1Mode;
}

public addTickShift(tickShift: number) {
this._midiFile.tickShift = tickShift;
this.tickShift = tickShift;
}

public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void {
tick += this.tickShift;

let denominatorIndex: number = 0;
let denominator = timeSignatureDenominator;
while (true) {
Expand All @@ -50,12 +64,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
}

public addRest(track: number, tick: number, channel: number): void {
tick += this.tickShift;
if (!this._smf1Mode) {
this._midiFile.addEvent(new AlphaTabRestEvent(track, tick, channel));
}
}

public addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void {
start += this.tickShift;
this._midiFile.addEvent(
new NoteOnEvent(
track,
Expand Down Expand Up @@ -94,23 +110,27 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
controller: ControllerType,
value: number
): void {
tick += this.tickShift;
this._midiFile.addEvent(
new ControlChangeEvent(track, tick, channel, controller, AlphaSynthMidiFileHandler._fixValue(value))
);
}

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

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

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

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

public finishTrack(track: number, tick: number): void {
tick += this.tickShift;
if (this._midiFile.format === MidiFileFormat.MultiTrack || track === 0) {
this._midiFile.addEvent(new EndOfTrackEvent(track, tick));
}
Expand Down
9 changes: 9 additions & 0 deletions packages/alphatab/src/midi/IMidiFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,13 @@ export interface IMidiFileHandler {
* @param tick The end tick for this track.
*/
finishTrack(track: number, tick: number): void;


/**
* Registers a general shift of the time-axis for the generate midi file.
* @param tickShift The shift in midi ticks by which all midi events beside the initial channel setups are shifted.
* This shift is applied in case grace beats
*/
addTickShift(tickShift: number): void;

}
7 changes: 7 additions & 0 deletions packages/alphatab/src/midi/MidiFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ export class MidiFile {
* Gets or sets the division per quarter notes.
*/
public division: number = MidiUtils.QuarterTime;

/**
* An indicator by how many midi-ticks the song contents are shifted.
* Grace beats at start might require a shift for the first beat to start at 0.
* This information can be used to translate back the player time axis to the music notation.
*/
public tickShift: number = 0;

/**
* Gets a list of midi events sorted by time.
Expand Down
31 changes: 30 additions & 1 deletion packages/alphatab/src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,16 @@ export class MidiFileGenerator {
this._calculatedBeatTimers.clear();
this._currentTime = 0;


// initialize tracks
for (const track of this._score.tracks) {
this._generateTrack(track);
}

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


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

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

private _detectTickShift() {
let tickShift = 0;
for (const track of this._score.tracks) {
for (const staff of track.staves) {
for (const voice of staff.bars[0].voices) {
if (!voice.isEmpty) {
const beat = voice.beats[0];
if (beat.playbackStart < tickShift) {
tickShift = beat.playbackStart;
}
}
}
}
}
tickShift = Math.abs(tickShift);
this._handler.addTickShift(tickShift);
}

private _generateTrack(track: Track): void {
// channel
this._generateChannel(track, track.playbackInfo.primaryChannel, track.playbackInfo);
Expand Down Expand Up @@ -1313,7 +1336,13 @@ export class MidiFileGenerator {
}
}

private _generateFadeSteps(track: Track, start: number, duration: number, startVolume: number, endVolume: number): void {
private _generateFadeSteps(
track: Track,
start: number,
duration: number,
startVolume: number,
endVolume: number
): void {
const tickStep: number = 120;
// we want to reach the target volume a bit earlier than the end of the note
duration = (duration * 0.8) | 0;
Expand Down
3 changes: 3 additions & 0 deletions packages/alphatab/src/midi/MidiTickLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,9 @@ export class MidiTickLookup {
}

private _findMasterBar(tick: number): MasterBarTickLookup | null {
if (tick <= 0 && this.masterBars.length > 0) {
return this.masterBars[0];
}
const bars: MasterBarTickLookup[] = this.masterBars;
let bottom: number = 0;
let top: number = bars.length - 1;
Expand Down
14 changes: 9 additions & 5 deletions packages/alphatab/src/model/JsonConverter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer';
import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer';
import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';
import type { ControllerType } from '@coderline/alphatab/midi/ControllerType';
import {
AlphaTabMetronomeEvent,
AlphaTabRestEvent,
Expand All @@ -17,11 +22,6 @@ import {
import { MidiFile, MidiTrack } from '@coderline/alphatab/midi/MidiFile';
import { Score } from '@coderline/alphatab/model/Score';
import { Settings } from '@coderline/alphatab/Settings';
import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer';
import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer';
import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError';
import type { ControllerType } from '@coderline/alphatab/midi/ControllerType';
import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';

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

JsonHelper.forEach(jsObject, (v, k) => {
switch (k) {
case 'tickShift':
midi2.tickShift = v as number;
break;
case 'division':
midi2.division = v as number;
break;
Expand Down Expand Up @@ -271,6 +274,7 @@ export class JsonConverter {
public static midiFileToJsObject(midi: MidiFile): Map<string, unknown> {
const o = new Map<string, unknown>();
o.set('division', midi.division);
o.set('tickShift', midi.tickShift);

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