diff --git a/src/midi/BeatTickLookup.ts b/src/midi/BeatTickLookup.ts index 77e5763f8..47753f8c8 100644 --- a/src/midi/BeatTickLookup.ts +++ b/src/midi/BeatTickLookup.ts @@ -14,19 +14,12 @@ export class BeatTickLookupItem { */ 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 + playbackStart: number ) { this.beat = beat; this.playbackStart = playbackStart; - this.playbackDuration = playbackDuration; } } @@ -81,13 +74,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, playbackStart: number, playbackDuration: number): void { - if (beat.isEmpty) { + public highlightBeat(beat: Beat, playbackStart: number): void { + if (beat.isEmpty && !beat.voice.isEmpty) { return; } if (!this._highlightedBeats.has(beat.id)) { this._highlightedBeats.set(beat.id, true); - this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart, playbackDuration)); + this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart)); } } diff --git a/src/midi/MasterBarTickLookup.ts b/src/midi/MasterBarTickLookup.ts index 08d820d26..e28c78123 100644 --- a/src/midi/MasterBarTickLookup.ts +++ b/src/midi/MasterBarTickLookup.ts @@ -97,10 +97,14 @@ export class MasterBarTickLookup { /** * Adds a new beat to this masterbar following the slicing logic required by the MidiTickLookup. + * @param beat The beat to add to this masterbat + * @param beatPlaybackStart The original start of this beat. This time is relevant for highlighting. + * @param sliceStart The slice start to which this beat should be added. This time is relevant for creating new slices. + * @param sliceDuration The slice duration to which this beat should be added. This time is relevant for creating new slices. * @returns The first item of the chain which was affected. */ - public addBeat(beat: Beat, start: number, duration: number) { - const end = start + duration; + public addBeat(beat: Beat, beatPlaybackStart: number, sliceStart: number, sliceDuration: number) { + const end = sliceStart + sliceDuration; // We have following scenarios we cover overall on inserts // Technically it would be possible to merge some code paths and work with loops @@ -208,29 +212,29 @@ export class MasterBarTickLookup { // Variant A if (this.firstBeat == null) { - const n1 = new BeatTickLookup(start, end); - n1.highlightBeat(beat, start, duration); + const n1 = new BeatTickLookup(sliceStart, end); + n1.highlightBeat(beat, beatPlaybackStart); this.insertAfter(this.firstBeat, n1); } // Variant B // Variant C - else if (start >= this.lastBeat!.end) { + else if (sliceStart >= this.lastBeat!.end) { // using the end here allows merge of B & C const n1 = new BeatTickLookup(this.lastBeat!.end, end); - n1.highlightBeat(beat, start, duration); + n1.highlightBeat(beat, beatPlaybackStart); this.insertAfter(this.lastBeat, n1); } else { let l1: BeatTickLookup | null = null; - if (start < this.firstBeat.start) { + if (sliceStart < this.firstBeat.start) { l1 = this.firstBeat!; } else { let current: BeatTickLookup | null = this.firstBeat; while (current != null) { // find item where we fall into - if (start >= current.start && start < current.end) { + if (sliceStart >= current.start && sliceStart < current.end) { l1 = current; break; } @@ -245,109 +249,109 @@ export class MasterBarTickLookup { // those scenarios should only happen if we insert before the // first item (e.g. for grace notes starting < 0) - if (start < l1.start) { + if (sliceStart < l1.start) { // Variant D // Variant E if (end == l1.start) { // using firstBeat.start here allows merge of D & E - const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat, start, duration); + const n1 = new BeatTickLookup(sliceStart, l1.start); + n1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(this.firstBeat, n1); } // Variant F else if (end < l1.end) { - const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat, start, duration); + const n1 = new BeatTickLookup(sliceStart, l1.start); + n1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n1); const n2 = new BeatTickLookup(l1.start, end); for (const b of l1.highlightedBeats) { - n2.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); + n2.highlightBeat(b.beat, b.playbackStart); } - n2.highlightBeat(beat, start, duration); + n2.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n2); l1.start = end; } // Variant G else if (end == l1.end) { - const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat, start, duration); + const n1 = new BeatTickLookup(sliceStart, l1.start); + n1.highlightBeat(beat, beatPlaybackStart); - l1.highlightBeat(beat, start, duration); + l1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n1); } // Variant H else /* end > this.firstBeat.end */ { - const n1 = new BeatTickLookup(start, l1.start); - n1.highlightBeat(beat, start, duration); + const n1 = new BeatTickLookup(sliceStart, l1.start); + n1.highlightBeat(beat, beatPlaybackStart); - l1.highlightBeat(beat, start, duration); + l1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n1); - this.addBeat(beat, l1.end, end - l1.end); + this.addBeat(beat, beatPlaybackStart, l1.end, end - l1.end); } } - else if (start > l1.start) { + else if (sliceStart > l1.start) { // variant I if (end == l1.end) { - const n1 = new BeatTickLookup(l1.start, start); + const n1 = new BeatTickLookup(l1.start, sliceStart); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); + n1.highlightBeat(b.beat, b.playbackStart); } - l1.start = start; - l1.highlightBeat(beat, start, duration); + l1.start = sliceStart; + l1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n1) } // Variant J else if (end < l1.end) { - const n1 = new BeatTickLookup(l1.start, start); + const n1 = new BeatTickLookup(l1.start, sliceStart); this.insertBefore(l1, n1) - const n2 = new BeatTickLookup(start, end); + const n2 = new BeatTickLookup(sliceStart, end); this.insertBefore(l1, n2) for (const b of l1.highlightedBeats) { - n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration) - n2.highlightBeat(b.beat, b.playbackStart, b.playbackDuration) + n1.highlightBeat(b.beat, b.playbackStart) + n2.highlightBeat(b.beat, b.playbackStart) } - n2.highlightBeat(beat, start, duration); + n2.highlightBeat(beat, beatPlaybackStart); l1.start = end; } // Variant K else /* end > l1.end */ { - const n1 = new BeatTickLookup(l1.start, start); + const n1 = new BeatTickLookup(l1.start, sliceStart); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); + n1.highlightBeat(b.beat, b.playbackStart); } - l1.start = start; - l1.highlightBeat(beat, start, duration); + l1.start = sliceStart; + l1.highlightBeat(beat, beatPlaybackStart); this.insertBefore(l1, n1); - this.addBeat(beat, l1.end, end - l1.end); + this.addBeat(beat, beatPlaybackStart, l1.end, end - l1.end); } } else /* start == l1.start */ { // Variant L if (end === l1.end) { - l1.highlightBeat(beat, start, end); + l1.highlightBeat(beat, beatPlaybackStart); } // Variant M else if (end < l1.end) { const n1 = new BeatTickLookup(l1.start, end); for (const b of l1.highlightedBeats) { - n1.highlightBeat(b.beat, b.playbackStart, b.playbackDuration); + n1.highlightBeat(b.beat, b.playbackStart); } - n1.highlightBeat(beat, start, duration); + n1.highlightBeat(beat, beatPlaybackStart); l1.start = end; @@ -355,8 +359,8 @@ export class MasterBarTickLookup { } // variant N else /* end > l1.end */ { - l1.highlightBeat(beat, start, duration); - this.addBeat(beat, l1.end, end - l1.end); + l1.highlightBeat(beat, beatPlaybackStart); + this.addBeat(beat, beatPlaybackStart, l1.end, end - l1.end); } } } diff --git a/src/midi/MidiTickLookup.ts b/src/midi/MidiTickLookup.ts index b47468f17..05c4ab0ae 100644 --- a/src/midi/MidiTickLookup.ts +++ b/src/midi/MidiTickLookup.ts @@ -417,6 +417,25 @@ export class MidiTickLookup { } public addBeat(beat: Beat, start: number, duration: number): void { - this._currentMasterBar?.addBeat(beat, start, duration); + 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 + if (start < 0 && currentMasterBar.previousMasterBar) { + const previousStart = currentMasterBar.previousMasterBar!.end + start; + const previousEnd = previousStart + duration; + + // add to previous bar + currentMasterBar.previousMasterBar!.addBeat(beat, previousStart, previousStart, currentMasterBar.previousMasterBar!.end - previousStart); + + // overlap to current bar? + if(previousEnd > currentMasterBar.previousMasterBar!.end) { + // the start is negative and representing the overlap to the previous bar. + const overlapDuration = duration + start; + currentMasterBar.addBeat(beat, start, 0, overlapDuration); + } + } else { + currentMasterBar.addBeat(beat, start, start, duration); + } + } } } diff --git a/test/audio/MidiTickLookup.test.ts b/test/audio/MidiTickLookup.test.ts index 1647e322f..454d23a16 100644 --- a/test/audio/MidiTickLookup.test.ts +++ b/test/audio/MidiTickLookup.test.ts @@ -3,7 +3,7 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; import { Logger } from '@src/Logger'; import { AlphaSynthMidiFileHandler, MasterBarTickLookup, MidiFile, MidiFileGenerator, MidiTickLookup, MidiTickLookupFindBeatResult } from '@src/midi'; import { MidiUtils } from '@src/midi/MidiUtils'; -import { Beat, Duration, MasterBar, Score } from '@src/model'; +import { Beat, Duration, MasterBar, Note, Score } from '@src/model'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; @@ -448,6 +448,134 @@ describe('MidiTickLookupTest', () => { expect(n2).to.equal(masterBar.lastBeat!); }) + + + function beatWithFret(fret: number) { + const b = new Beat(); + b.notes.push(new Note()); + b.notes[0].fret = fret; + return b; + } + + function fretOfBeat(beat: Beat | null) { + return beat && beat.notes.length > 0 ? beat.notes[0].fret : -1; + } + + function prepareGraceMultiVoice(graceNoteOverlap: number, graceNoteDuration: number): MidiTickLookup { + const lookup = new MidiTickLookup(); + + const masterBar1Lookup = new MasterBarTickLookup(); + masterBar1Lookup.masterBar = new MasterBar(); + masterBar1Lookup.masterBar!.timeSignatureNumerator = 3; + masterBar1Lookup.masterBar!.timeSignatureDenominator = 4; + masterBar1Lookup.start = 0; + masterBar1Lookup.tempo = 120; + masterBar1Lookup.end = masterBar1Lookup.start + masterBar1Lookup.masterBar.calculateDuration(); + lookup.addMasterBar(masterBar1Lookup); + + // voice 0 + // - normal + lookup.addBeat(beatWithFret(0), MidiUtils.QuarterTime * 0, MidiUtils.QuarterTime * 2); + // - shortened due to grace + lookup.addBeat(beatWithFret(1), MidiUtils.QuarterTime * 2, MidiUtils.QuarterTime - graceNoteOverlap); + + // voice 1 + // - normal + lookup.addBeat(beatWithFret(2), MidiUtils.QuarterTime * 0, MidiUtils.QuarterTime * 3); + + const masterBar2Lookup = new MasterBarTickLookup(); + masterBar2Lookup.masterBar = new MasterBar(); + masterBar2Lookup.masterBar!.timeSignatureNumerator = 3; + masterBar2Lookup.masterBar!.timeSignatureDenominator = 4; + masterBar2Lookup.start = masterBar2Lookup.end; + masterBar2Lookup.tempo = 120; + masterBar2Lookup.end = masterBar2Lookup.start + masterBar2Lookup.masterBar.calculateDuration(); + lookup.addMasterBar(masterBar2Lookup); + + // grace note + lookup.addBeat(beatWithFret(3), -graceNoteOverlap, graceNoteDuration); + // normal note + const onNoteSteal = (-graceNoteOverlap) + graceNoteDuration; + lookup.addBeat(beatWithFret(4), onNoteSteal, MidiUtils.QuarterTime - onNoteSteal); + + return lookup; + } + + it('grace-multivoice-full-in-previous-bar', () => { + const lookup = prepareGraceMultiVoice(120, 120); + + // + // validate first bar + let current = lookup.masterBars[0].firstBeat!; + + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("0,2"); + expect(current.start).to.equal(0); + expect(current.duration).to.equal(1920); + + current = current.nextBeat!; + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("1,2"); + expect(current.start).to.equal(1920); + // quarter note ends earlier due to grace note + expect(current.duration).to.equal(840); + + current = current.nextBeat!; + // on last slice we have the grace note but not the quarter note + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("2,3"); + expect(current.start).to.equal(2760); + expect(current.duration).to.equal(120); + + // + // validate second bar + current = lookup.masterBars[1].firstBeat!; + + // no grace note, normal quarter note + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("4"); + expect(current.start).to.equal(0); + expect(current.duration).to.equal(960); + }) + + it('grace-multivoice-with-overlap', () => { + const lookup = prepareGraceMultiVoice(120, 240); + + // + // validate first bar + let current = lookup.masterBars[0].firstBeat!; + + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("0,2"); + expect(current.start).to.equal(0); + expect(current.duration).to.equal(1920); + + current = current.nextBeat!; + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("1,2"); + expect(current.start).to.equal(1920); + // quarter note ends earlier due to grace note + expect(current.duration).to.equal(840); + + current = current.nextBeat!; + // on last slice we have the grace note but not the quarter note + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("2,3"); + expect(current.start).to.equal(2760); + expect(current.duration).to.equal(120); + + // + // validate second bar + current = lookup.masterBars[1].firstBeat!; + + // half the grace note + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("3"); + expect(current.start).to.equal(0); + expect(current.duration).to.equal(120); + + // no grace note, normal quarter note + current = current.nextBeat!; + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("4"); + expect(current.start).to.equal(120); + expect(current.duration).to.equal(840); + }) + + + + it('cursor-snapping', async () => { const buffer = await TestPlatform.loadFile('test-data/audio/cursor-snapping.gp'); const settings = new Settings(); @@ -495,7 +623,7 @@ describe('MidiTickLookupTest', () => { trackIndexes: number[], durations: number[], currentBeatFrets: number[], - nextBeatFrets: (number | null)[], + nextBeatFrets: number[], skipClean: boolean = false ) { const buffer = ByteBuffer.fromString(tex); @@ -508,11 +636,11 @@ describe('MidiTickLookupTest', () => { let currentLookup: MidiTickLookupFindBeatResult | null = null; const actualIncrementalFrets: number[] = []; - const actualIncrementalNextFrets: (number | null)[] = []; + const actualIncrementalNextFrets: number[] = []; const actualIncrementalTickDurations: number[] = []; const actualCleanFrets: number[] = []; - const actualCleanNextFrets: (number | null)[] = []; + const actualCleanNextFrets: number[] = []; const actualCleanTickDurations: number[] = []; for (let i = 0; i < ticks.length; i++) { @@ -520,15 +648,15 @@ describe('MidiTickLookupTest', () => { Logger.debug("Test", `Checking index ${i} with tick ${ticks[i]}`) expect(currentLookup).to.be.ok; - actualIncrementalFrets.push(currentLookup!.beat.notes[0].fret); - actualIncrementalNextFrets.push(currentLookup!.nextBeat?.beat?.notes?.[0]?.fret ?? null) + actualIncrementalFrets.push(fretOfBeat(currentLookup!.beat)); + actualIncrementalNextFrets.push(fretOfBeat(currentLookup!.nextBeat?.beat ?? 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) + actualCleanFrets.push(fretOfBeat(cleanLookup!.beat)); + actualCleanNextFrets.push(fretOfBeat(cleanLookup!.nextBeat?.beat ?? null)) actualCleanTickDurations.push(cleanLookup!.tickDuration) } } @@ -537,7 +665,7 @@ describe('MidiTickLookupTest', () => { expect(actualIncrementalNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); expect(actualIncrementalTickDurations.join(',')).to.equal(durations.join(',')); - if(!skipClean) { + if (!skipClean) { expect(actualCleanFrets.join(',')).to.equal(currentBeatFrets.join(',')); expect(actualCleanNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); expect(actualCleanTickDurations.join(',')).to.equal(durations.join(',')); @@ -549,7 +677,7 @@ describe('MidiTickLookupTest', () => { function nextBeatSearchTest(trackIndexes: number[], durations: number[], currentBeatFrets: number[], - nextBeatFrets: (number | null)[] + nextBeatFrets: number[] ) { lookupTest( ` @@ -585,7 +713,7 @@ describe('MidiTickLookupTest', () => { ], [ 4, 4, 2, 2, 6, 6, 6, 6, - 9, 9, 7, 7, null, null, null, null + 9, 9, 7, 7, -1, -1, -1, -1 ] ) }); @@ -603,7 +731,7 @@ describe('MidiTickLookupTest', () => { ], [ 2, 2, 2, 2, 6, 6, 6, 6, - 7, 7, 7, 7, null, null, null, null + 7, 7, 7, 7, -1, -1, -1, -1 ] ) }); @@ -627,7 +755,7 @@ describe('MidiTickLookupTest', () => { ], [ 2, 3, 4, 5, - 6, 7, 8, null + 6, 7, 8, -1 ] ) }); @@ -651,7 +779,7 @@ describe('MidiTickLookupTest', () => { ], [ 2, 3, 4, 5, - 6, 7, 8, null + 6, 7, 8, -1 ] ) }); @@ -664,9 +792,9 @@ describe('MidiTickLookupTest', () => { `, [ // first bar, real playback - 0, 480, 960, 1440, + 0, 480, 960, 1440, // gap - 1920, 2400, 2880, 3360, + 1920, 2400, 2880, 3360, // second bar, real playback 3840, 4320, 4800, 5280, // second gap @@ -685,20 +813,50 @@ describe('MidiTickLookupTest', () => { // gap 2, 2, 2, 2, // second bar, real playback - 3, 3, 4, 4, + 3, 3, 4, 4, // second gap 4, 4, 4, 4 ], [ - 2, 2, null, null, + 2, 2, -1, -1, - null, null, null, null, + -1, -1, -1, -1, - 4, 4, null, null, - - null, null, null, null + 4, 4, -1, -1, + + -1, -1, -1, -1 ], true ) }); + + it('empty-bar', () => { + lookupTest( + ` + \\ts 2 4 + | 1.1.1 + `, + [ + // first bar (empty) + 0, 480, 960, 1440, + // second bar, real playback + 1920, 2400, 2880, 3360 + ], + [0], + [ + 1920, 1920, 1920, 1920, + 1920, 1920, 1920, 1920 + ], + [ + // first bar (empty) + -1, -1, -1, -1, + // second bar, real playback + 1, 1, 1, 1 + ], + [ + 1, 1, 1, 1, + -1, -1, -1, -1 + ] + ) + }); });