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
25 changes: 13 additions & 12 deletions packages/alphatab/src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3121,28 +3121,29 @@ export class AlphaTabApiBase<TSettings> {
if (this._selectionStart && this._tickCache) {
// get the start and stop ticks (which consider properly repeats)
const tickCache: MidiTickLookup = this._tickCache;
const realMasterBarStart: number = tickCache.getMasterBarStart(
const realStartMasterBarStart: number = tickCache.getMasterBarStart(
this._selectionStart.beat.voice.bar.masterBar
);
const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
// move to selection start
this._currentBeat = null; // reset current beat so it is updating the cursor
if (this._player.state === PlayerState.Paused) {
this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
}
this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
// set playback range
if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
const realMasterBarEnd: number = tickCache.getMasterBarStart(
const realEndMasterBarStart: number = tickCache.getMasterBarStart(
this._selectionEnd.beat.voice.bar.masterBar
);

const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
const endBeatPlaybackEnd =
endBeatPlaybackRange?.endTick ??
this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
const range = new PlaybackRange();
range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart;
range.endTick =
realMasterBarEnd +
this._selectionEnd.beat.playbackStart +
this._selectionEnd.beat.playbackDuration -
50;
range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
this.playbackRange = range;
} else {
this._selectionStart = undefined;
Expand Down Expand Up @@ -3622,7 +3623,7 @@ export class AlphaTabApiBase<TSettings> {

this._currentBeat = null;
this._cursorUpdateTick(this._previousTick, false, 1, true, true);
if(this._selectionStart) {
if (this._selectionStart) {
this.highlightPlaybackRange(this._selectionStart.beat, this._selectionEnd!.beat);
}

Expand Down
1 change: 0 additions & 1 deletion packages/alphatab/src/midi/MasterBarTickLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ export class MasterBarTickLookup {
if (this.firstBeat == null) {
const n1 = new BeatTickLookup(sliceStart, end);
n1.highlightBeat(beat, beatPlaybackStart);

this._insertAfter(this.firstBeat, n1);
}
// Variant B
Expand Down
32 changes: 29 additions & 3 deletions packages/alphatab/src/midi/MidiTickLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MasterBarTickLookup } from '@coderline/alphatab/midi/MasterBarTickLooku
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
import type { Beat } from '@coderline/alphatab/model/Beat';
import type { MasterBar } from '@coderline/alphatab/model/MasterBar';
import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';
import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';

/**
* Describes how a cursor should be moving.
Expand Down Expand Up @@ -188,6 +188,14 @@ export class MidiTickLookup {
*/
public readonly masterBarLookup: Map<number, MasterBarTickLookup> = new Map();

/**
* A dictionary of all beat played. The index is the id to {@link Beat.id}.
* The value is the bar relative tick time at which the beat was registered during midi generation.
* This lookup only contains the first time a Beat is played.
* @internal
*/
public readonly beatLookup: Map<number, PlaybackRange> = new Map();

/**
* A list of all {@link MasterBarTickLookup} sorted by time.
*/
Expand Down Expand Up @@ -671,11 +679,23 @@ export class MidiTickLookup {
* @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
*/
public getBeatStart(beat: Beat): number {
if (!this.masterBarLookup.has(beat.voice.bar.index)) {
if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
return 0;
}

return this.masterBarLookup.get(beat.voice.bar.index)!.start + beat.playbackStart;
const mb = this.masterBarLookup.get(beat.voice.bar.index)!;
return mb.start + this.beatLookup.get(beat.id)!.startTick;
}
/**
* Gets the playback range in midi ticks for a given beat.
* @param beat The beat to find the time period for.
* @returns The relative playback range within the parent masterbar at which the beat start and ends playing
*/
public getRelativeBeatPlaybackRange(beat: Beat): PlaybackRange | undefined{
if (!this.beatLookup.has(beat.id)) {
return undefined;
}
return this.beatLookup.get(beat.id)!;
}

/**
Expand All @@ -695,6 +715,12 @@ export class MidiTickLookup {
}

public addBeat(beat: Beat, start: number, duration: number): void {
if (!this.beatLookup.has(beat.id)) {
const playbackRange = new PlaybackRange();
playbackRange.startTick = start;
playbackRange.endTick = start + duration;
this.beatLookup.set(beat.id, playbackRange);
}
const currentMasterBar = this._currentMasterBar;
if (currentMasterBar) {
// pre-beat grace notes at the start of the bar we also add the beat to the previous bar
Expand Down
64 changes: 60 additions & 4 deletions packages/alphatab/test/audio/MidiTickLookup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import { Voice } from '@coderline/alphatab/model/Voice';
import { Settings } from '@coderline/alphatab/Settings';
import { TestPlatform } from 'test/TestPlatform';
import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';
import { FlatMidiEvent, FlatMidiEventGenerator, FlatNoteEvent } from 'test/audio/FlatMidiEventGenerator';
import { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase';
import { TestUiFacade } from 'test/visualTests/TestUiFacade';
import { PlayerMode } from '@coderline/alphatab/PlayerSettings';

describe('MidiTickLookupTest', () => {
function buildLookup(score: Score, settings: Settings): MidiTickLookup {
Expand Down Expand Up @@ -729,9 +733,10 @@ describe('MidiTickLookupTest', () => {
expect(actualIncrementalNextIds.join(','), 'nextBeatIds mismatch').toBe(nextBeatIds.join(','));
expect(actualIncrementalTickDurations.join(','), 'durations mismatch').toBe(durations.join(','));
if (expectedCursorModes) {
expect(expectedCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(','), 'cursorModes mismatch').toBe(
actualCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(',')
);
expect(
expectedCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(','),
'cursorModes mismatch'
).toBe(actualCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(','));
}

if (!skipClean) {
Expand Down Expand Up @@ -1472,4 +1477,55 @@ describe('MidiTickLookupTest', () => {
);
});
});
});

it('swing-click-lookup', async () => {
const settings = new Settings();
settings.core.engine = 'svg';

const score = ScoreLoader.loadAlphaTex(`\\tf triplet8th C4.8 * 8`, settings);
const handler = new FlatMidiEventGenerator();
const generator = new MidiFileGenerator(score, settings, handler);
generator.generate();

const noteEvents = handler.midiEvents.filter<FlatMidiEvent>(e => e instanceof FlatNoteEvent);
expect(noteEvents.length).toBe(8);

const beats = score.tracks[0].staves[0].bars[0].voices[0].beats;

const facade = new TestUiFacade();
facade.rootContainer.width = 1300;

settings.player.playerMode = PlayerMode.EnabledSynthesizer;
const api = new AlphaTabApiBase<unknown>(facade, settings);

const promise = Promise.withResolvers<Score>();
api.postRenderFinished.on(() => {
promise.resolve(score);
});
api.error.on(e => promise.reject(e));
api.renderScore(score, [0]);

await promise.promise;

for (let i = 0; i < beats.length; i++) {
const range = generator.tickLookup.getRelativeBeatPlaybackRange(beats[i]);
expect(range).not.toBeUndefined();

const noteStart = noteEvents[i].tick;
const noteEnd = noteEvents[i].tick + (noteEvents[i] as FlatNoteEvent).length;

expect(range!.startTick).toBe(noteStart);
expect(range!.endTick).toBe(noteEnd);

const playbackRangePadding = 50; // small offset to avoid overshoot, see applyPlaybackRangeFromHighlight
if (i < beats.length - 1) {
api.highlightPlaybackRange(beats[i], beats[i + 1]);
api.applyPlaybackRangeFromHighlight();

expect(api.playbackRange!.startTick).toBe(noteStart);
const nextNoteEnd = noteEvents[i + 1].tick + (noteEvents[i + 1] as FlatNoteEvent).length;
expect(api.playbackRange!.endTick).toBe(nextNoteEnd - playbackRangePadding);
}
}
});
});
17 changes: 13 additions & 4 deletions packages/alphatab/test/visualTests/TestUiFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ import {
import { Settings } from '@coderline/alphatab/Settings';
import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader';
import { Score } from '@coderline/alphatab/model/Score';
import type { Cursors } from '@coderline/alphatab/platform/Cursors';
import { Cursors } from '@coderline/alphatab/platform/Cursors';
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 type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer';
import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs';
import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth';
import type { IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth';
import type { IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter';
import { TestPlatform } from 'test/TestPlatform';
import { TestOutput } from 'test/audio/TestOutput';

/**
* @internal
Expand Down Expand Up @@ -222,11 +224,18 @@ export class TestUiFacade implements IUiFacade<unknown> {
}

public createWorkerPlayer(): IAlphaSynth | null {
throw new Error('Not supported');
return new AlphaSynth(new TestOutput(), 500);
}

private _cursors?: Cursors;
public createCursors(): Cursors | null {
return null;
this._cursors = this._cursors ?? new Cursors(
new TestUiContainer(),
new TestUiContainer(),
new TestUiContainer(),
new TestUiContainer()
);
return this._cursors;
}

public destroyCursors(): void {}
Expand All @@ -240,7 +249,7 @@ export class TestUiFacade implements IUiFacade<unknown> {
public highlightElements(_groupId: string, _masterBarIndex: number): void {}

public createSelectionElement(): IContainer | null {
return null;
return new TestUiContainer();
}

public getScrollContainer(): IContainer {
Expand Down
Loading