diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 833709117..c754e0b28 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -44,6 +44,7 @@ import { MidiEventType } from '@src/midi/MidiEvent'; import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; import { ActiveBeatsChangedEventArgs } from '@src/synth/ActiveBeatsChangedEventArgs'; +import { BeatTickLookupItem } from './midi/BeatTickLookup'; class SelectionInfo { public beat: Beat; @@ -983,7 +984,7 @@ export class AlphaTabApiBase { nextBeat: Beat | null, duration: number, stop: boolean, - beatsToHighlight: Beat[], + beatsToHighlight: BeatTickLookupItem[], cache: BoundsLookup, beatBoundings: BeatBounds, shouldScroll: boolean @@ -1018,7 +1019,7 @@ export class AlphaTabApiBase { if (this._playerState === PlayerState.Playing && !stop) { if (this.settings.player.enableElementHighlighting) { for (let highlight of beatsToHighlight) { - let className: string = BeatContainerGlyph.getGroupId(highlight); + let className: string = BeatContainerGlyph.getGroupId(highlight.beat); this.uiFacade.highlightElements(className, beat.voice.bar.index); } } @@ -1058,7 +1059,7 @@ export class AlphaTabApiBase { // trigger an event for others to indicate which beat/bar is played if (shouldNotifyBeatChange) { this.onPlayedBeatChanged(beat); - this.onActiveBeatsChanged(new ActiveBeatsChangedEventArgs(beatsToHighlight)); + this.onActiveBeatsChanged(new ActiveBeatsChangedEventArgs(beatsToHighlight.map(i => i.beat))); } } diff --git a/src/midi/BeatTickLookup.ts b/src/midi/BeatTickLookup.ts index 90af0b53d..77e5763f8 100644 --- a/src/midi/BeatTickLookup.ts +++ b/src/midi/BeatTickLookup.ts @@ -1,5 +1,35 @@ import { Beat } from '@src/model/Beat'; +/** + * Represents a beat and when it is actually played according to the generated audio. + */ +export class BeatTickLookupItem { + /** + * Gets the beat represented by this item. + */ + public readonly beat: Beat; + + /** + * Gets the playback start of the beat according to the generated audio. + */ + public readonly playbackStart: number; + + /** + * Gets the playback start of the beat duration according to the generated audio. + */ + public readonly playbackDuration: number; + + public constructor( + beat: Beat, + playbackStart: number, + playbackDuration: number + ) { + this.beat = beat; + this.playbackStart = playbackStart; + this.playbackDuration = playbackDuration; + } +} + /** * Represents the time period, for which one or multiple {@link Beat}s are played */ @@ -21,7 +51,7 @@ export class BeatTickLookup { * the beat of this lookup starts playing. This might not mean * the beats start at this position. */ - public highlightedBeats: Beat[] = []; + public highlightedBeats: BeatTickLookupItem[] = []; /** * Gets the next BeatTickLookup which comes after this one and is in the same @@ -42,7 +72,7 @@ export class BeatTickLookup { return this.end - this.start; } - public constructor(start:number, end:number) { + public constructor(start: number, end: number) { this.start = start; this.end = end; } @@ -51,13 +81,13 @@ export class BeatTickLookup { * Marks the given beat as highlighed as part of this lookup. * @param beat The beat to add. */ - public highlightBeat(beat: Beat): void { - if(beat.isEmpty) { + public highlightBeat(beat: Beat, playbackStart: number, playbackDuration: number): void { + if (beat.isEmpty) { return; } if (!this._highlightedBeats.has(beat.id)) { this._highlightedBeats.set(beat.id, true); - this.highlightedBeats.push(beat); + this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart, playbackDuration)); } } @@ -68,8 +98,8 @@ export class BeatTickLookup { */ public getVisibleBeatAtStart(visibleTracks: Set): Beat | null { for (const b of this.highlightedBeats) { - if (b.playbackStart == this.start && visibleTracks.has(b.voice.bar.staff.track.index)) { - return b; + if (b.playbackStart == this.start && visibleTracks.has(b.beat.voice.bar.staff.track.index)) { + return b.beat; } } return null; diff --git a/src/midi/MasterBarTickLookup.ts b/src/midi/MasterBarTickLookup.ts index 5b21e67d7..08d820d26 100644 --- a/src/midi/MasterBarTickLookup.ts +++ b/src/midi/MasterBarTickLookup.ts @@ -89,6 +89,12 @@ export class MasterBarTickLookup { */ public nextMasterBar: MasterBarTickLookup | null = null; + + /** + * Gets or sets the {@link MasterBarTickLookup} of the previous masterbar in the {@link Score} + */ + public previousMasterBar: MasterBarTickLookup | null = null; + /** * Adds a new beat to this masterbar following the slicing logic required by the MidiTickLookup. * @returns The first item of the chain which was affected. @@ -203,7 +209,7 @@ export class MasterBarTickLookup { // Variant A if (this.firstBeat == null) { const n1 = new BeatTickLookup(start, end); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); this.insertAfter(this.firstBeat, n1); } @@ -212,7 +218,7 @@ export class MasterBarTickLookup { else if (start >= this.lastBeat!.end) { // using the end here allows merge of B & C const n1 = new BeatTickLookup(this.lastBeat!.end, end); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); this.insertAfter(this.lastBeat, n1); } @@ -245,21 +251,21 @@ export class MasterBarTickLookup { if (end == l1.start) { // using firstBeat.start here allows merge of D & E const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); this.insertBefore(this.firstBeat, n1); } // Variant F else if (end < l1.end) { const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); this.insertBefore(l1, n1); const n2 = new BeatTickLookup(l1.start, end); for (const b of l1.highlightedBeats) { - n2.highlightBeat(b); + n2.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); } - n2.highlightBeat(beat); + n2.highlightBeat(beat, start, duration); this.insertBefore(l1, n2); l1.start = end; @@ -267,9 +273,9 @@ export class MasterBarTickLookup { // Variant G else if (end == l1.end) { const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, duration); this.insertBefore(l1, n1); } @@ -277,9 +283,9 @@ export class MasterBarTickLookup { else /* end > this.firstBeat.end */ { const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, duration); this.insertBefore(l1, n1); @@ -291,11 +297,11 @@ export class MasterBarTickLookup { if (end == l1.end) { const n1 = new BeatTickLookup(l1.start, start); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b); + n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); } l1.start = start; - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, duration); this.insertBefore(l1, n1) } @@ -308,10 +314,10 @@ export class MasterBarTickLookup { this.insertBefore(l1, n2) for (const b of l1.highlightedBeats) { - n1.highlightBeat(b) - n2.highlightBeat(b) + n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration) + n2.highlightBeat(b.beat, b.playbackStart, b.playbackDuration) } - n2.highlightBeat(beat); + n2.highlightBeat(beat, start, duration); l1.start = end; } @@ -319,29 +325,29 @@ export class MasterBarTickLookup { else /* end > l1.end */ { const n1 = new BeatTickLookup(l1.start, start); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b); + n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); } l1.start = start; - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, duration); this.insertBefore(l1, n1); - + this.addBeat(beat, l1.end, end - l1.end); } } else /* start == l1.start */ { // Variant L if (end === l1.end) { - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, end); } // Variant M else if (end < l1.end) { const n1 = new BeatTickLookup(l1.start, end); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b); + n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); } - n1.highlightBeat(beat); + n1.highlightBeat(beat, start, duration); l1.start = end; @@ -349,7 +355,7 @@ export class MasterBarTickLookup { } // variant N else /* end > l1.end */ { - l1.highlightBeat(beat); + l1.highlightBeat(beat, start, duration); this.addBeat(beat, l1.end, end - l1.end); } } diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index a87001acc..ee41270a3 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -289,15 +289,12 @@ export class MidiFileGenerator { } } - const realTickOffset: number = !beat.nextBeat - ? audioDuration - : beat.nextBeat.absolutePlaybackStart - beat.absolutePlaybackStart; // in case of normal playback register playback if (realBar === beat.voice.bar) { - this.tickLookup.addBeat(beat, beatStart, realTickOffset > audioDuration ? realTickOffset : audioDuration); + this.tickLookup.addBeat(beat, beatStart, audioDuration); } else { // in case of simile marks where we repeat we also register - this.tickLookup.addBeat(beat, 0, realTickOffset > audioDuration ? realTickOffset : audioDuration); + this.tickLookup.addBeat(beat, 0, audioDuration); } const track: Track = beat.voice.bar.staff.track; diff --git a/src/midi/MidiTickLookup.ts b/src/midi/MidiTickLookup.ts index 9f64b37cc..b47468f17 100644 --- a/src/midi/MidiTickLookup.ts +++ b/src/midi/MidiTickLookup.ts @@ -155,6 +155,7 @@ export class MidiTickLookup { current.masterBar, current.beatLookup.nextBeat, current.end, trackLookup, false, true); + if (current.nextBeat == null) { current.nextBeat = this.findBeatSlow(trackLookup, current, current.end, true); } @@ -164,6 +165,12 @@ export class MidiTickLookup { current.tickDuration = current.nextBeat.start - current.start; current.duration = MidiUtils.ticksToMillis(current.tickDuration, current.masterBar.tempo); } + + // no next beat, animate to the end of the bar (could be an incomplete bar) + if (!current.nextBeat) { + current.tickDuration = current.masterBar.end - current.start; + current.duration = MidiUtils.ticksToMillis(current.tickDuration, current.masterBar.tempo); + } } private findBeatSlow(trackLookup: Set, currentBeatHint: MidiTickLookupFindBeatResult | null, tick: number, isNextSearch: boolean): MidiTickLookupFindBeatResult | null { @@ -252,29 +259,47 @@ export class MidiTickLookup { if (!startBeat) { if (isNextSeach) { - while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); - - if (startBeat) { - startBeatLookup = currentStartLookup; - break; + let currentMasterBar: MasterBarTickLookup | null = masterBar; + while (currentMasterBar != null && startBeat == null) { + while (currentStartLookup != null) { + startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + + if (startBeat) { + startBeatLookup = currentStartLookup; + masterBar = currentMasterBar; + break; + } + + currentStartLookup = currentStartLookup.nextBeat; } - currentStartLookup = currentStartLookup.nextBeat; + if (!startBeat || !startBeatLookup) { + currentMasterBar = currentMasterBar.nextMasterBar; + currentStartLookup = currentMasterBar?.firstBeat ?? null; + } } - } else { - while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); - if (startBeat) { - startBeatLookup = currentStartLookup; - break; + } else { + let currentMasterBar: MasterBarTickLookup | null = masterBar; + while (currentMasterBar != null && startBeat == null) { + while (currentStartLookup != null) { + startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + + if (startBeat) { + startBeatLookup = currentStartLookup; + masterBar = currentMasterBar; + break; + } + + currentStartLookup = currentStartLookup.previousBeat; } - currentStartLookup = currentStartLookup.previousBeat; + if (!startBeat || !startBeatLookup) { + currentMasterBar = currentMasterBar.previousMasterBar; + currentStartLookup = currentMasterBar?.firstBeat ?? null; + } } } - } } else if (currentStartLookup.end > relativeTick) { break; @@ -382,6 +407,7 @@ export class MidiTickLookup { public addMasterBar(masterBar: MasterBarTickLookup): void { this.masterBars.push(masterBar); if (this._currentMasterBar) { + masterBar.previousMasterBar = this._currentMasterBar; this._currentMasterBar.nextMasterBar = masterBar; } this._currentMasterBar = masterBar; diff --git a/test/audio/MidiTickLookup.test.ts b/test/audio/MidiTickLookup.test.ts index b85e3fc04..1647e322f 100644 --- a/test/audio/MidiTickLookup.test.ts +++ b/test/audio/MidiTickLookup.test.ts @@ -34,7 +34,7 @@ describe('MidiTickLookupTest', () => { expect(masterBarLookup.firstBeat!.start).to.equal(0); expect(masterBarLookup.firstBeat!.end).to.equal(MidiUtils.QuarterTime); expect(masterBarLookup.firstBeat!.highlightedBeats.length).to.equal(1); - expect(masterBarLookup.firstBeat!.highlightedBeats[0]).to.equal(nb); + expect(masterBarLookup.firstBeat!.highlightedBeats[0].beat).to.equal(nb); }) function prepareVariantTest(): MidiTickLookup { @@ -77,7 +77,7 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.lastBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(MidiUtils.QuarterTime * 2); expect(n1.end).to.equal(MidiUtils.QuarterTime * 3); @@ -98,7 +98,7 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.lastBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(MidiUtils.QuarterTime * 2); expect(n1.end).to.equal(MidiUtils.QuarterTime * 4); @@ -121,7 +121,7 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.firstBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime); expect(n1.end).to.equal(0); @@ -143,7 +143,7 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.firstBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime * 2); expect(n1.end).to.equal(0); @@ -166,13 +166,13 @@ describe('MidiTickLookupTest', () => { const n2 = n1.nextBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime * 0.5); expect(n1.end).to.equal(0); expect(n2.highlightedBeats.length).to.equal(2); - expect(n2.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); - expect(n2.highlightedBeats[1]).to.equal(nb); + expect(n2.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); + expect(n2.highlightedBeats[1].beat).to.equal(nb); expect(n2.start).to.equal(0); expect(n2.end).to.equal(MidiUtils.QuarterTime * 0.5); @@ -199,12 +199,12 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.firstBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime); expect(n1.end).to.equal(0); expect(l1.highlightedBeats.length).to.equal(2); - expect(l1.highlightedBeats[1]).to.equal(nb); + expect(l1.highlightedBeats[1].beat).to.equal(nb); expect(l1.start).to.equal(0); expect(l1.end).to.equal(MidiUtils.QuarterTime); @@ -227,18 +227,18 @@ describe('MidiTickLookupTest', () => { const n2 = l1.nextBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime); expect(n1.end).to.equal(0); expect(l1.highlightedBeats.length).to.equal(2); - expect(l1.highlightedBeats[1]).to.equal(nb); + expect(l1.highlightedBeats[1].beat).to.equal(nb); expect(l1.start).to.equal(0); expect(l1.end).to.equal(MidiUtils.QuarterTime); expect(n2.highlightedBeats.length).to.equal(2); - expect(n2.highlightedBeats[0]).to.equal(l2.highlightedBeats[0]); - expect(n2.highlightedBeats[1]).to.equal(nb); + expect(n2.highlightedBeats[0].beat).to.equal(l2.highlightedBeats[0].beat); + expect(n2.highlightedBeats[1].beat).to.equal(nb); expect(n2.start).to.equal(MidiUtils.QuarterTime); expect(n2.end).to.equal(MidiUtils.QuarterTime * 1.5); @@ -265,12 +265,12 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.firstBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); + expect(n1.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); expect(n1.start).to.equal(0); expect(n1.end).to.equal(MidiUtils.QuarterTime * 0.5); expect(l1.highlightedBeats.length).to.equal(2); - expect(l1.highlightedBeats[1]).to.equal(nb); + expect(l1.highlightedBeats[1].beat).to.equal(nb); expect(l1.start).to.equal(MidiUtils.QuarterTime * 0.5); expect(l1.end).to.equal(MidiUtils.QuarterTime); @@ -298,13 +298,13 @@ describe('MidiTickLookupTest', () => { const n2 = n1.nextBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); + expect(n1.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); expect(n1.start).to.equal(0); expect(n1.end).to.equal(MidiUtils.QuarterTime * 0.25); expect(n2.highlightedBeats.length).to.equal(2); - expect(n2.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); - expect(n2.highlightedBeats[1]).to.equal(nb); + expect(n2.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); + expect(n2.highlightedBeats[1].beat).to.equal(nb); expect(n2.start).to.equal(MidiUtils.QuarterTime * 0.25); expect(n2.end).to.equal(MidiUtils.QuarterTime * 0.75); @@ -336,13 +336,13 @@ describe('MidiTickLookupTest', () => { const n2 = n1.nextBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); + expect(n1.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); expect(n1.start).to.equal(0); expect(n1.end).to.equal(MidiUtils.QuarterTime * 0.25); expect(n2.highlightedBeats.length).to.equal(2); - expect(n2.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); - expect(n2.highlightedBeats[1]).to.equal(nb); + expect(n2.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); + expect(n2.highlightedBeats[1].beat).to.equal(nb); expect(n2.start).to.equal(MidiUtils.QuarterTime * 0.25); expect(n2.end).to.equal(MidiUtils.QuarterTime * 0.75); @@ -371,7 +371,7 @@ describe('MidiTickLookupTest', () => { lookup.addBeat(nb, l1.start, MidiUtils.QuarterTime); expect(l1.highlightedBeats.length).to.equal(2); - expect(l1.highlightedBeats[1]).to.equal(nb); + expect(l1.highlightedBeats[1].beat).to.equal(nb); expect(l1).to.equal(masterBar.firstBeat!); expect(l1.nextBeat).to.equal(l2); @@ -390,8 +390,8 @@ describe('MidiTickLookupTest', () => { const n1 = masterBar.firstBeat!; expect(n1.highlightedBeats.length).to.equal(2); - expect(n1.highlightedBeats[0]).to.equal(l1.highlightedBeats[0]); - expect(n1.highlightedBeats[1]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(l1.highlightedBeats[0].beat); + expect(n1.highlightedBeats[1].beat).to.equal(nb); expect(n1.start).to.equal(0); expect(n1.end).to.equal(MidiUtils.QuarterTime * 0.5); @@ -422,22 +422,22 @@ describe('MidiTickLookupTest', () => { const n2 = l2.nextBeat!; expect(n1.highlightedBeats.length).to.equal(1); - expect(n1.highlightedBeats[0]).to.equal(nb); + expect(n1.highlightedBeats[0].beat).to.equal(nb); expect(n1.start).to.equal(-MidiUtils.QuarterTime); expect(n1.end).to.equal(0); expect(l1.highlightedBeats.length).to.equal(2); - expect(l1.highlightedBeats[1]).to.equal(nb); + expect(l1.highlightedBeats[1].beat).to.equal(nb); expect(l1.start).to.equal(0); expect(l1.end).to.equal(MidiUtils.QuarterTime); expect(l2.highlightedBeats.length).to.equal(2); - expect(l2.highlightedBeats[1]).to.equal(nb); + expect(l2.highlightedBeats[1].beat).to.equal(nb); expect(l2.start).to.equal(MidiUtils.QuarterTime * 1); expect(l2.end).to.equal(MidiUtils.QuarterTime * 2); expect(n2.highlightedBeats.length).to.equal(1); - expect(n2.highlightedBeats[0]).to.equal(nb); + expect(n2.highlightedBeats[0].beat).to.equal(nb); expect(n2.start).to.equal(MidiUtils.QuarterTime * 2); expect(n2.end).to.equal(MidiUtils.QuarterTime * 3); @@ -488,49 +488,90 @@ describe('MidiTickLookupTest', () => { expect(secondBeat!.beatLookup.duration).to.equal(960); }); - function nextBeatSearchTest(trackIndexes: number[], + + function lookupTest( + tex: string, + ticks: number[], + trackIndexes: number[], durations: number[], currentBeatFrets: number[], - nextBeatFrets: (number | null)[] + nextBeatFrets: (number | null)[], + skipClean: boolean = false ) { - const buffer = ByteBuffer.fromString(` - \\tempo 67 - . - \\track "T01" - \\ts 1 4 1.1.8 2.1.8 | 6.1.8 7.1.8 | - \\track "T02" - 3.1.16 4.1.16 5.1.8 | 8.1.16 9.1.16 10.1.8 - `); + const buffer = ByteBuffer.fromString(tex); const settings = new Settings(); const score = ScoreLoader.loadScoreFromBytes(buffer.getBuffer(), settings); const lookup = buildLookup(score, settings); const tracks = new Set(trackIndexes); - const ticks = [ - 0, 120, 240, 360, 480, 600, 720, 840, 960, - 1080, 1200, 1320, 1440, 1560, 1680, 1800 - ]; - let currentLookup: MidiTickLookupFindBeatResult | null = null; + + const actualIncrementalFrets: number[] = []; + const actualIncrementalNextFrets: (number | null)[] = []; + const actualIncrementalTickDurations: number[] = []; + + const actualCleanFrets: number[] = []; + const actualCleanNextFrets: (number | null)[] = []; + const actualCleanTickDurations: number[] = []; + for (let i = 0; i < ticks.length; i++) { currentLookup = lookup.findBeat(tracks, ticks[i], currentLookup); Logger.debug("Test", `Checking index ${i} with tick ${ticks[i]}`) expect(currentLookup).to.be.ok; - expect(currentLookup!.beat.notes[0].fret).to.equal(currentBeatFrets[i]); - expect(currentLookup!.nextBeat?.beat?.notes?.[0]?.fret ?? null).to.equal(nextBeatFrets[i]); - expect(currentLookup!.tickDuration).to.equal(durations[i]); - - const cleanLookup = lookup.findBeat(tracks, ticks[i], null); - expect(cleanLookup).to.be.ok; - expect(cleanLookup!.beat.notes[0].fret).to.equal(currentBeatFrets[i]); - expect(cleanLookup!.nextBeat?.beat?.notes?.[0]?.fret ?? null).to.equal(nextBeatFrets[i]); - expect(cleanLookup!.tickDuration).to.equal(durations[i]); + actualIncrementalFrets.push(currentLookup!.beat.notes[0].fret); + actualIncrementalNextFrets.push(currentLookup!.nextBeat?.beat?.notes?.[0]?.fret ?? null) + actualIncrementalTickDurations.push(currentLookup!.tickDuration) + + if (!skipClean) { + const cleanLookup = lookup.findBeat(tracks, ticks[i], null); + + actualCleanFrets.push(cleanLookup!.beat.notes[0].fret); + actualCleanNextFrets.push(cleanLookup!.nextBeat?.beat?.notes?.[0]?.fret ?? null) + actualCleanTickDurations.push(cleanLookup!.tickDuration) + } + } + + expect(actualIncrementalFrets.join(',')).to.equal(currentBeatFrets.join(',')); + expect(actualIncrementalNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); + expect(actualIncrementalTickDurations.join(',')).to.equal(durations.join(',')); + + if(!skipClean) { + expect(actualCleanFrets.join(',')).to.equal(currentBeatFrets.join(',')); + expect(actualCleanNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); + expect(actualCleanTickDurations.join(',')).to.equal(durations.join(',')); } } + + function nextBeatSearchTest(trackIndexes: number[], + durations: number[], + currentBeatFrets: number[], + nextBeatFrets: (number | null)[] + ) { + lookupTest( + ` + \\tempo 67 + . + \\track "T01" + \\ts 1 4 1.1.8 2.1.8 | 6.1.8 7.1.8 | + \\track "T02" + 3.1.16 4.1.16 5.1.8 | 8.1.16 9.1.16 10.1.8 + `, + [ + 0, 120, 240, 360, 480, 600, 720, 840, 960, + 1080, 1200, 1320, 1440, 1560, 1680, 1800 + ], + trackIndexes, + durations, + currentBeatFrets, + nextBeatFrets + ) + } + + it('next-beat-search-multi-track', () => { nextBeatSearchTest( [0, 1], @@ -566,4 +607,98 @@ describe('MidiTickLookupTest', () => { ] ) }); + + it('lookup-triplet-feel-reference', () => { + lookupTest( + `\\ts 2 4 + 1.1.4{tu 3} 2.1.8{tu 3} 3.1.4{tu 3} 4.1.8{tu 3} | 5.1.4{tu 3} 6.1.8{tu 3} 7.1.4{tu 3} 8.1.8{tu 3}`, + [ + 0, 640, 960, 1600, + 1920, 2560, 2880, 3520 + ], + [0], + [ + 640, 320, 640, 320, + 640, 320, 640, 320 + ], + [ + 1, 2, 3, 4, + 5, 6, 7, 8 + ], + [ + 2, 3, 4, 5, + 6, 7, 8, null + ] + ) + }); + + it('lookup-triplet-feel-test', () => { + lookupTest( + `\\tf triplet-8th \\ts 2 4 + 1.1.8 2.1.8 3.1.8 4.1.8 | 5.1.8 6.1.8 7.1.8 8.1.8`, + [ + 0, 640, 960, 1600, + 1920, 2560, 2880, 3520 + ], + [0], + [ + 640, 320, 640, 320, + 640, 320, 640, 320 + ], + [ + 1, 2, 3, 4, + 5, 6, 7, 8 + ], + [ + 2, 3, 4, 5, + 6, 7, 8, null + ] + ) + }); + + it('incomplete', () => { + lookupTest( + ` + \\ts 4 4 + 1.1.4 2.1.4 | 3.1.4 4.1.4 + `, + [ + // first bar, real playback + 0, 480, 960, 1440, + // gap + 1920, 2400, 2880, 3360, + // second bar, real playback + 3840, 4320, 4800, 5280, + // second gap + 5760, 6240, 6720, 7200 + ], + [0], + [ + 960, 960, 2880, 2880, + 2880, 2880, 2880, 2880, + 960, 960, 2880, 2880, + 2880, 2880, 2880, 2880 + ], + [ + // first bar, real playback + 1, 1, 2, 2, + // gap + 2, 2, 2, 2, + // second bar, real playback + 3, 3, 4, 4, + // second gap + 4, 4, 4, 4 + ], + [ + 2, 2, null, null, + + null, null, null, null, + + 4, 4, null, null, + + null, null, null, null + ], + true + ) + }); });