diff --git a/src/Environment.ts b/src/Environment.ts index c291ecf94..0b0c1cd72 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -527,7 +527,7 @@ export class Environment { new WideBeatVibratoEffectInfo(), new SlightBeatVibratoEffectInfo(), new WideNoteVibratoEffectInfo(), - new SlightNoteVibratoEffectInfo(), + new SlightNoteVibratoEffectInfo(false), new LeftHandTapEffectInfo(), new GolpeEffectInfo(GolpeType.Finger) ], @@ -558,7 +558,7 @@ export class Environment { new WideBeatVibratoEffectInfo(), new SlightBeatVibratoEffectInfo(), new WideNoteVibratoEffectInfo(), - new SlightNoteVibratoEffectInfo(), + new SlightNoteVibratoEffectInfo(true), new TapEffectInfo(), new FadeEffectInfo(), new HarmonicsEffectInfo(HarmonicType.Natural), diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 715ee0a1e..cd42c48f3 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -373,6 +373,7 @@ export class MidiFileGenerator { barStartTick + beatStart, beat.playbackDuration, phaseLength, + 0, bendAmplitude, (tick, value) => { this._handler.addBend( @@ -480,6 +481,20 @@ export class MidiFileGenerator { } } + private determineChannel(track: Track, note: Note): number { + // on tied notes use the same channel as the previous note + if(note.isTieDestination) { + return this.determineChannel(track, note.tieOrigin!); + } + + // on certain effects we use the secondary channel to avoid interference with other notes + if(note.hasBend || note.beat.hasWhammyBar || note.beat.vibrato !== VibratoType.None) { + return track.playbackInfo.secondaryChannel; + } + + return track.playbackInfo.primaryChannel; + } + private generateNote( note: Note, beatStart: number, @@ -503,10 +518,7 @@ export class MidiFileGenerator { noteDuration.noteOnly -= brushOffset; noteDuration.letRingEnd -= brushOffset; const velocity: number = MidiFileGenerator.getNoteVelocity(note); - const channel: number = - note.hasBend || note.beat.hasWhammyBar || note.beat.vibrato !== VibratoType.None - ? track.playbackInfo.secondaryChannel - : track.playbackInfo.primaryChannel; + const channel: number = this.determineChannel(track, note); let initialBend: number = 0; if (note.hasBend) { @@ -808,9 +820,22 @@ export class MidiFileGenerator { return; } const track: Track = note.beat.voice.bar.staff.track; - this.generateVibratorWithParams(noteStart, noteDuration.noteOnly, phaseLength, bendAmplitude, (tick, value) => { - this._handler.addNoteBend(track.index, tick, channel, noteKey, value); - }); + let bendBase = 0; + // if this is a vibrato at the end of a bend, the vibrato wave needs to start at the pitch where the bend ends + if (note.isTieDestination && note.tieOrigin!.hasBend) { + const bendPoints = note.tieOrigin!.bendPoints!; + bendBase = bendPoints[bendPoints.length - 1].value; + } + this.generateVibratorWithParams( + noteStart, + noteDuration.noteOnly, + phaseLength, + bendBase, + bendAmplitude, + (tick, value) => { + this._handler.addNoteBend(track.index, tick, channel, noteKey, value); + } + ); } public vibratoResolution: number = 16; @@ -818,25 +843,27 @@ export class MidiFileGenerator { noteStart: number, noteDuration: number, phaseLength: number, + bendBase: number, bendAmplitude: number, addBend: (tick: number, value: number) => void ): void { const resolution: number = this.vibratoResolution; const phaseHalf: number = (phaseLength / 2) | 0; - // 1st Phase stays at bend 0, - // then we have a sine wave with the given amplitude and phase length - noteStart += phaseLength; + // vibrato is a sine wave with the given amplitude and phase length const noteEnd: number = noteStart + noteDuration; while (noteStart < noteEnd) { let phase: number = 0; const phaseDuration: number = noteStart + phaseLength < noteEnd ? phaseLength : noteEnd - noteStart; while (phase < phaseDuration) { - let bend: number = bendAmplitude * Math.sin((phase * Math.PI) / phaseHalf); + let bend: number = bendBase + bendAmplitude * Math.sin((phase * Math.PI) / phaseHalf); addBend((noteStart + phase) | 0, MidiFileGenerator.getPitchWheel(bend)); phase += resolution; } noteStart += phaseLength; } + + // reset at end + addBend((noteEnd) | 0, MidiFileGenerator.getPitchWheel(bendBase)); } /** @@ -955,7 +982,11 @@ export class MidiFileGenerator { let duration: number; if (note.isTieOrigin && this._settings.notation.extendBendArrowsOnTiedNotes) { let endNote: Note = note; - while (endNote.isTieOrigin && !endNote.tieDestination!.hasBend) { + while ( + endNote.isTieOrigin && + !endNote.tieDestination!.hasBend && + endNote.tieDestination!.vibrato == VibratoType.None + ) { endNote = endNote.tieDestination!; } duration = @@ -1316,10 +1347,7 @@ export class MidiFileGenerator { currentTick += ticksPerBreakpoint; } - // final bend value if needed - if (currentBendValue < nextBendValue) { - addBend(currentTick | 0, nextBendValue); - } + addBend(currentTick | 0, nextBendValue); } private generateTrill( diff --git a/src/rendering/effects/SlightNoteVibratoEffectInfo.ts b/src/rendering/effects/SlightNoteVibratoEffectInfo.ts index 31a93d05e..a13601bef 100644 --- a/src/rendering/effects/SlightNoteVibratoEffectInfo.ts +++ b/src/rendering/effects/SlightNoteVibratoEffectInfo.ts @@ -9,15 +9,23 @@ import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; import { NotationElement } from '@src/NotationSettings'; export class SlightNoteVibratoEffectInfo extends NoteEffectInfoBase { + + // for tied bends ending in a vibrato, the vibrato is drawn by the TabBendGlyph for proper alignment + private _hideOnTiedBend: boolean; + public get notationElement(): NotationElement { return NotationElement.EffectSlightNoteVibrato; } protected shouldCreateGlyphForNote(note: Note): boolean { - return ( + let hasVibrato = note.vibrato === VibratoType.Slight || - (note.isTieDestination && note.tieOrigin!.vibrato === VibratoType.Slight) - ); + (note.isTieDestination && note.tieOrigin!.vibrato === VibratoType.Slight); + + if (this._hideOnTiedBend && hasVibrato && note.isTieDestination && note.tieOrigin!.hasBend) { + hasVibrato = false; + } + return hasVibrato; } public get sizingMode(): EffectBarGlyphSizing { @@ -28,7 +36,8 @@ export class SlightNoteVibratoEffectInfo extends NoteEffectInfoBase { return new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2); } - public constructor() { + public constructor(hideOnTiedBend: boolean) { super(); + this._hideOnTiedBend = hideOnTiedBend; } } diff --git a/src/rendering/glyphs/TabBendGlyph.ts b/src/rendering/glyphs/TabBendGlyph.ts index 7cc193a1b..f096587b2 100644 --- a/src/rendering/glyphs/TabBendGlyph.ts +++ b/src/rendering/glyphs/TabBendGlyph.ts @@ -11,6 +11,8 @@ import { TabBendRenderPoint } from '@src/rendering/glyphs/TabBendRenderPoint'; import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; import { RenderingResources } from '@src/RenderingResources'; import { BendPoint } from '@src/model/BendPoint'; +import { VibratoType } from '@src/model'; +import { NoteVibratoGlyph } from './NoteVibratoGlyph'; export class TabBendGlyph extends Glyph { private static readonly ArrowSize: number = 6; @@ -188,7 +190,9 @@ export class TabBendGlyph extends Glyph { break; case BendType.BendRelease: renderingPoints.push(new TabBendRenderPoint(0, note.bendPoints![0].value)); - renderingPoints.push(new TabBendRenderPoint((BendPoint.MaxPosition / 2) | 0, note.bendPoints![1].value)); + renderingPoints.push( + new TabBendRenderPoint((BendPoint.MaxPosition / 2) | 0, note.bendPoints![1].value) + ); renderingPoints.push(new TabBendRenderPoint(BendPoint.MaxPosition, note.bendPoints![3].value)); break; case BendType.Bend: @@ -214,7 +218,7 @@ export class TabBendGlyph extends Glyph { let startNoteRenderer: BarRendererBase = this.renderer; let endNote: Note = note; let isMultiBeatBend: boolean = false; - let endNoteRenderer: TabBarRenderer | null = null; + let endNoteRenderer: BarRendererBase | null = null; let endNoteHasBend: boolean = false; let slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; let endBeat: Beat | null = null; @@ -223,17 +227,22 @@ export class TabBendGlyph extends Glyph { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( this.renderer.staff.staveId, nextNote.beat.voice.bar - ) as TabBarRenderer; + ); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { break; } endNote = nextNote; isMultiBeatBend = true; - if (endNote.hasBend || !this.renderer.settings.notation.extendBendArrowsOnTiedNotes) { + if ( + endNote.hasBend || + !this.renderer.settings.notation.extendBendArrowsOnTiedNotes || + endNote.vibrato != VibratoType.None + ) { endNoteHasBend = true; break; } } + endBeat = endNote.beat; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( this.renderer.staff.staveId, @@ -297,18 +306,24 @@ export class TabBendGlyph extends Glyph { secondPt = new TabBendRenderPoint(BendPoint.MaxPosition, firstPt.value); secondPt.lineValue = firstPt.lineValue; - this.paintBend( - note, - firstPt, - secondPt, - startX, - topY, - dX, - slurText, - canvas - ); + this.paintBend(note, firstPt, secondPt, startX, topY, dX, slurText, canvas); } } + + if (endNote.vibrato !== VibratoType.None) { + const vibratoStartX = endX - cx + TabBendGlyph.ArrowSize - endNoteRenderer.x; + const vibratoStartY: number = + topY - + cy - + TabBendGlyph.BendValueHeight * renderPoints[renderPoints.length - 1].lineValue; + + const vibrato = new NoteVibratoGlyph(vibratoStartX, vibratoStartY, endNote.vibrato, 1.2); + vibrato.beat = endNote.beat; + vibrato.renderer = endNoteRenderer; + vibrato.doLayout(); + vibrato.paint(cx + endNoteRenderer.x, cy, canvas); + } + canvas.color = color; } } @@ -371,7 +386,7 @@ export class TabBendGlyph extends Glyph { canvas.fill(); arrowOffset = -arrowSize; } - canvas.stroke(); + canvas.beginPath(); if (firstPt.value === secondPt.value) { // draw horizontal dashed line // to really have the line ending at the right position @@ -395,7 +410,7 @@ export class TabBendGlyph extends Glyph { } } else { if (x2 > x1) { - // draw bezier lien from first to second point + // draw bezier line from first to second point canvas.moveTo(x1, y1); canvas.bezierCurveTo((x1 + x2) / 2, y1, x2, y1, x2, y2 + arrowOffset); canvas.stroke(); diff --git a/test-data/test-results.html b/test-data/test-results.html index 1092958f1..5ed77b950 100644 --- a/test-data/test-results.html +++ b/test-data/test-results.html @@ -193,7 +193,12 @@ img.src = '/' + result.diffFile; }) - await Promise.all([expected, actual, diff]); + try { + await Promise.all([expected, actual, diff]); + } + catch(e) { + // ignore potentially missing images + } setupComparer(comparer, result); diff --git a/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png b/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png new file mode 100644 index 000000000..324096118 Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png differ diff --git a/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png b/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png new file mode 100644 index 000000000..5a3a8949d Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png differ diff --git a/test-data/visual-tests/effects-and-annotations/bend-vibrato.gp b/test-data/visual-tests/effects-and-annotations/bend-vibrato.gp new file mode 100644 index 000000000..fdedf2500 Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/bend-vibrato.gp differ diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index 0e4d84c7b..af0daf060 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -131,6 +131,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(9 * 80, 0, info.secondaryChannel, note.realValue, 8960), new FlatNoteBendEvent(10 * 80, 0, info.secondaryChannel, note.realValue, 9045), new FlatNoteBendEvent(11 * 80, 0, info.secondaryChannel, note.realValue, 9131), + new FlatNoteBendEvent(12 * 80, 0, info.secondaryChannel, note.realValue, 9216), // note itself new FlatNoteEvent( @@ -266,6 +267,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(ticks[6] + 12 * 9, 0, info.secondaryChannel, 67, 8960), new FlatNoteBendEvent(ticks[6] + 12 * 10, 0, info.secondaryChannel, 67, 9045), new FlatNoteBendEvent(ticks[6] + 12 * 11, 0, info.secondaryChannel, 67, 9131), + new FlatNoteBendEvent(ticks[6] + 12 * 12, 0, info.secondaryChannel, 67, 9216), new FlatNoteEvent(ticks[6], 0, info.secondaryChannel, 3840, 67, mfVelocity), // end of track @@ -327,6 +329,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(10 * 40, 0, info.secondaryChannel, note.realValue, 9045), new FlatNoteBendEvent(11 * 40, 0, info.secondaryChannel, note.realValue, 9131), new FlatNoteBendEvent(12 * 40, 0, info.secondaryChannel, note.realValue, 9216), // full bend + new FlatNoteBendEvent(12 * 40, 0, info.secondaryChannel, note.realValue, 9216), // full bend new FlatNoteBendEvent(13 * 40, 0, info.secondaryChannel, note.realValue, 9131), new FlatNoteBendEvent(14 * 40, 0, info.secondaryChannel, note.realValue, 9045), new FlatNoteBendEvent(15 * 40, 0, info.secondaryChannel, note.realValue, 8960), @@ -412,6 +415,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(9 * 80, 0, info.secondaryChannel, 62, 8960), new FlatNoteBendEvent(10 * 80, 0, info.secondaryChannel, 62, 9045), new FlatNoteBendEvent(11 * 80, 0, info.secondaryChannel, 62, 9131), + new FlatNoteBendEvent(12 * 80, 0, info.secondaryChannel, 62, 9216), // note itself new FlatNoteEvent( @@ -693,6 +697,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(9 * 80, 0, info.secondaryChannel, note1.realValue, 8960), new FlatNoteBendEvent(10 * 80, 0, info.secondaryChannel, note1.realValue, 9045), new FlatNoteBendEvent(11 * 80, 0, info.secondaryChannel, note1.realValue, 9131), + new FlatNoteBendEvent(12 * 80, 0, info.secondaryChannel, note1.realValue, 9216), // note itself new FlatNoteEvent( @@ -730,6 +735,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(21 * 40, 0, info.secondaryChannel, note2.realValue, 9984), new FlatNoteBendEvent(22 * 40, 0, info.secondaryChannel, note2.realValue, 10069), new FlatNoteBendEvent(23 * 40, 0, info.secondaryChannel, note2.realValue, 10155), + new FlatNoteBendEvent(24 * 40, 0, info.secondaryChannel, note2.realValue, 10240), // note itself new FlatNoteEvent( @@ -796,15 +802,16 @@ describe('MidiFileGeneratorTest', () => { new FlatTimeSignatureEvent(0, 4, 4), new FlatTempoEvent(0, 120), - new FlatNoteBendEvent(0, 0, info.primaryChannel, note1.realValue, 8192), // no bend - new FlatNoteBendEvent(480, 0, info.primaryChannel, note1.realValue, 8192), // vibrato main note + new FlatNoteBendEvent(0, 0, info.primaryChannel, note1.realValue, 8192), // no bend (note itself) + new FlatNoteBendEvent(0, 0, info.primaryChannel, note1.realValue, 8192), // no bend (vibrato start on main note) + new FlatNoteBendEvent(120, 0, info.primaryChannel, note1.realValue, 8704), + new FlatNoteBendEvent(240, 0, info.primaryChannel, note1.realValue, 8192), + new FlatNoteBendEvent(360, 0, info.primaryChannel, note1.realValue, 7680), + new FlatNoteBendEvent(480, 0, info.primaryChannel, note1.realValue, 8192), new FlatNoteBendEvent(600, 0, info.primaryChannel, note1.realValue, 8704), new FlatNoteBendEvent(720, 0, info.primaryChannel, note1.realValue, 8192), new FlatNoteBendEvent(840, 0, info.primaryChannel, note1.realValue, 7680), - new FlatNoteBendEvent(960, 0, info.primaryChannel, note1.realValue, 8192), - new FlatNoteBendEvent(1080, 0, info.primaryChannel, note1.realValue, 8704), - new FlatNoteBendEvent(1200, 0, info.primaryChannel, note1.realValue, 8192), - new FlatNoteBendEvent(1320, 0, info.primaryChannel, note1.realValue, 7680), + new FlatNoteBendEvent(960, 0, info.primaryChannel, note1.realValue, 8192), // end of quarter note (main) new FlatNoteEvent( 0, 0, @@ -814,14 +821,158 @@ describe('MidiFileGeneratorTest', () => { MidiUtils.dynamicToVelocity(note1.dynamics as number) ), + new FlatNoteBendEvent(960, 0, info.primaryChannel, note1.realValue, 8192), // no bend (vibrato start on main note) + new FlatNoteBendEvent(1080, 0, info.primaryChannel, note1.realValue, 8704), // continued vibrato on tied note + new FlatNoteBendEvent(1200, 0, info.primaryChannel, note1.realValue, 8192), + new FlatNoteBendEvent(1320, 0, info.primaryChannel, note1.realValue, 7680), new FlatNoteBendEvent(1440, 0, info.primaryChannel, note1.realValue, 8192), new FlatNoteBendEvent(1560, 0, info.primaryChannel, note1.realValue, 8704), new FlatNoteBendEvent(1680, 0, info.primaryChannel, note1.realValue, 8192), new FlatNoteBendEvent(1800, 0, info.primaryChannel, note1.realValue, 7680), - new FlatNoteBendEvent(1920, 0, info.primaryChannel, note1.realValue, 8192), - new FlatNoteBendEvent(2040, 0, info.primaryChannel, note1.realValue, 8704), - new FlatNoteBendEvent(2160, 0, info.primaryChannel, note1.realValue, 8192), - new FlatNoteBendEvent(2280, 0, info.primaryChannel, note1.realValue, 7680), + new FlatNoteBendEvent(1920, 0, info.primaryChannel, note1.realValue, 8192), // end of second quarter note + + // end of track + new FlatTrackEndEvent(3840, 0) // 3840 = end of bar + ]; + + assertEvents(handler.midiEvents, expectedEvents); + }); + + it('bend-tied-no-vibrato', () => { + let tex: string = '3.3{b (0 4)}.4 -.3.4'; + let score: Score = parseTex(tex); + + let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const settings = new Settings(); + settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events + let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; + generator.generate(); + let info: PlaybackInformation = score.tracks[0].playbackInfo; + let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + let expectedEvents: FlatMidiEvent[] = [ + // channel init + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterFine, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterCourse, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryFine, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryCoarse, 16), + new FlatProgramChangeEvent(0, 0, info.primaryChannel, info.program), + + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.VolumeCoarse, 120), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.PanCoarse, 64), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterFine, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterCourse, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryFine, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryCoarse, 16), + new FlatProgramChangeEvent(0, 0, info.secondaryChannel, info.program), + + new FlatTimeSignatureEvent(0, 4, 4), + new FlatTempoEvent(0, 120), + + // bend spans the whole range of both quarter notes + new FlatNoteBendEvent(0, 0, info.secondaryChannel, note1.realValue, 8192), // no bend + new FlatNoteBendEvent(0, 0, info.secondaryChannel, note1.realValue, 8192), + new FlatNoteBendEvent(160, 0, info.secondaryChannel, note1.realValue, 8277), + new FlatNoteBendEvent(320, 0, info.secondaryChannel, note1.realValue, 8363), + new FlatNoteBendEvent(480, 0, info.secondaryChannel, note1.realValue, 8448), + new FlatNoteBendEvent(640, 0, info.secondaryChannel, note1.realValue, 8533), + new FlatNoteBendEvent(800, 0, info.secondaryChannel, note1.realValue, 8619), + new FlatNoteBendEvent(960, 0, info.secondaryChannel, note1.realValue, 8704), + new FlatNoteBendEvent(1120, 0, info.secondaryChannel, note1.realValue, 8789), + new FlatNoteBendEvent(1280, 0, info.secondaryChannel, note1.realValue, 8875), + new FlatNoteBendEvent(1440, 0, info.secondaryChannel, note1.realValue, 8960), + new FlatNoteBendEvent(1600, 0, info.secondaryChannel, note1.realValue, 9045), + new FlatNoteBendEvent(1760, 0, info.secondaryChannel, note1.realValue, 9131), + new FlatNoteBendEvent(1920, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteEvent( + 0, + 0, + info.secondaryChannel, + 1920, + note1.realValue, + MidiUtils.dynamicToVelocity(note1.dynamics as number) + ), + + // end of track + new FlatTrackEndEvent(3840, 0) // 3840 = end of bar + ]; + + assertEvents(handler.midiEvents, expectedEvents); + }); + + it('bend-tied-vibrato', () => { + let tex: string = '3.3{b (0 4)}.4 -.3{v}.4'; + let score: Score = parseTex(tex); + + let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const settings = new Settings(); + settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events + let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; + generator.generate(); + let info: PlaybackInformation = score.tracks[0].playbackInfo; + let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + let expectedEvents: FlatMidiEvent[] = [ + // channel init + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterFine, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterCourse, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryFine, 0), + new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryCoarse, 16), + new FlatProgramChangeEvent(0, 0, info.primaryChannel, info.program), + + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.VolumeCoarse, 120), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.PanCoarse, 64), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterFine, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterCourse, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryFine, 0), + new FlatControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryCoarse, 16), + new FlatProgramChangeEvent(0, 0, info.secondaryChannel, info.program), + + new FlatTimeSignatureEvent(0, 4, 4), + new FlatTempoEvent(0, 120), + + // bend only takes first quarter note until tied note + new FlatNoteBendEvent(0, 0, info.secondaryChannel, note1.realValue, 8192), // no bend + new FlatNoteBendEvent(0, 0, info.secondaryChannel, note1.realValue, 8192), + new FlatNoteBendEvent(80, 0, info.secondaryChannel, note1.realValue, 8277), + new FlatNoteBendEvent(160, 0, info.secondaryChannel, note1.realValue, 8363), + new FlatNoteBendEvent(240, 0, info.secondaryChannel, note1.realValue, 8448), + new FlatNoteBendEvent(320, 0, info.secondaryChannel, note1.realValue, 8533), + new FlatNoteBendEvent(400, 0, info.secondaryChannel, note1.realValue, 8619), + new FlatNoteBendEvent(480, 0, info.secondaryChannel, note1.realValue, 8704), + new FlatNoteBendEvent(560, 0, info.secondaryChannel, note1.realValue, 8789), + new FlatNoteBendEvent(640, 0, info.secondaryChannel, note1.realValue, 8875), + new FlatNoteBendEvent(720, 0, info.secondaryChannel, note1.realValue, 8960), + new FlatNoteBendEvent(800, 0, info.secondaryChannel, note1.realValue, 9045), + new FlatNoteBendEvent(880, 0, info.secondaryChannel, note1.realValue, 9131), + new FlatNoteBendEvent(960, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteEvent( + 0, + 0, + info.secondaryChannel, + 1920, + note1.realValue, + MidiUtils.dynamicToVelocity(note1.dynamics as number) + ), + + // vibrato starts on tied note on height of the bend-end + new FlatNoteBendEvent(960, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteBendEvent(1080, 0, info.secondaryChannel, note1.realValue, 9728), + new FlatNoteBendEvent(1200, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteBendEvent(1320, 0, info.secondaryChannel, note1.realValue, 8704), + new FlatNoteBendEvent(1440, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteBendEvent(1560, 0, info.secondaryChannel, note1.realValue, 9728), + new FlatNoteBendEvent(1680, 0, info.secondaryChannel, note1.realValue, 9216), + new FlatNoteBendEvent(1800, 0, info.secondaryChannel, note1.realValue, 8704), + new FlatNoteBendEvent(1920, 0, info.secondaryChannel, note1.realValue, 9216), // end of track new FlatTrackEndEvent(3840, 0) // 3840 = end of bar diff --git a/test/visualTests/features/EffectsAndAnnotations.test.ts b/test/visualTests/features/EffectsAndAnnotations.test.ts index 7211729d9..d59ff25c6 100644 --- a/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -1,8 +1,10 @@ import { SystemsLayoutMode } from '@src/DisplaySettings'; +import { ScoreLoader } from '@src/importer'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { BeatBarreEffectInfo } from '@src/rendering/effects/BeatBarreEffectInfo'; import { Settings } from '@src/Settings'; +import { TestPlatform } from '@test/TestPlatform'; import { VisualTestHelper } from '@test/visualTests/VisualTestHelper'; import { expect } from 'chai'; @@ -241,4 +243,21 @@ describe('EffectsAndAnnotationsTests', () => { settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; await VisualTestHelper.runVisualTest('effects-and-annotations/rasgueado.gp', settings); }); + + it('bend-vibrato-default', async () => { + const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/effects-and-annotations/bend-vibrato.gp`); + const referenceFileName = 'effects-and-annotations/bend-vibrato-default.png'; + const settings = new Settings(); + const score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); + await VisualTestHelper.runVisualTestScore(score, referenceFileName, settings); + }); + + it('bend-vibrato-songbook', async () => { + const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/effects-and-annotations/bend-vibrato.gp`); + const referenceFileName = 'effects-and-annotations/bend-vibrato-songbook.png'; + const settings = new Settings(); + settings.setSongBookModeSettings(); + const score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); + await VisualTestHelper.runVisualTestScore(score, referenceFileName, settings); + }); });