diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 97f0f569e..009bfe7e6 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -479,7 +479,7 @@ export class MidiFileGenerator { this.generateWhammy(note.beat, noteStart, noteDuration, channel); } else if (note.slideInType !== SlideInType.None || note.slideOutType !== SlideOutType.None) { this.generateSlide(note, noteStart, noteDuration, noteKey, dynamicValue, channel); - } else if (note.vibrato !== VibratoType.None) { + } else if (note.vibrato !== VibratoType.None || (note.isTieDestination && note.tieOrigin!.vibrato !== VibratoType.None)) { this.generateVibrato(note, noteStart, noteDuration, noteKey, channel); } @@ -673,7 +673,11 @@ export class MidiFileGenerator { ): void { let phaseLength: number = 0; let bendAmplitude: number = 0; - switch (note.vibrato) { + const vibratoType = note.vibrato !== VibratoType.None ? note.vibrato : ( + note.isTieDestination ? note.tieOrigin!.vibrato : + VibratoType.Slight /* should never happen unless called wrongly */ + ); + switch (vibratoType) { case VibratoType.Slight: phaseLength = this._settings.player.vibrato.noteSlightLength; bendAmplitude = this._settings.player.vibrato.noteSlightAmplitude; @@ -691,6 +695,8 @@ export class MidiFileGenerator { }); } + + public vibratoResolution:number = 16; private generateVibratorWithParams( noteStart: number, noteDuration: number, @@ -698,7 +704,7 @@ export class MidiFileGenerator { bendAmplitude: number, addBend: (tick: number, value: number) => void ): void { - const resolution: number = 16; + 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 diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index 3bc0ae3ed..246133a42 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -31,6 +31,7 @@ import { TestPlatform } from '@test/TestPlatform'; import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; import { MetaEventType } from '@src/midi/MetaEvent'; import { MetaDataEvent } from '@src/midi/MetaDataEvent'; +import { VibratoType } from '@src/model'; describe('MidiFileGeneratorTest', () => { const parseTex: (tex: string) => Score = (tex: string): Score => { @@ -790,6 +791,84 @@ describe('MidiFileGeneratorTest', () => { expect(handler.midiEvents.length).toEqual(expectedEvents.length); }); + it('tied-vibrato', () => { + let tex: string = '3.3{v}.4 -.3{v}.4'; + let score: Score = parseTex(tex); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].vibrato).toEqual(VibratoType.Slight); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isTieDestination).toBeTrue(); + score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].vibrato = VibratoType.None; + 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 ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterFine, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterCourse, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryFine, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryCoarse, 16), + new ProgramChangeEvent(0, 0, info.primaryChannel, info.program), + + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.VolumeCoarse, 120), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.PanCoarse, 64), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterFine, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterCourse, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryFine, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryCoarse, 16), + new ProgramChangeEvent(0, 0, info.secondaryChannel, info.program), + + new TimeSignatureEvent(0, 4, 4), + new TempoEvent(0, 120), + + new NoteBendEvent(0, 0, info.primaryChannel, note1.realValue, 8192), // no bend + new NoteBendEvent(480, 0, info.primaryChannel, note1.realValue, 8192), // vibrato main note + new NoteBendEvent(600, 0, info.primaryChannel, note1.realValue, 8704), + new NoteBendEvent(720, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(840, 0, info.primaryChannel, note1.realValue, 7680), + new NoteBendEvent(960, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(1080, 0, info.primaryChannel, note1.realValue, 8704), + new NoteBendEvent(1200, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(1320, 0, info.primaryChannel, note1.realValue, 7680), + new NoteEvent( + 0, + 0, + info.primaryChannel, + 1920, + note1.realValue, + note1.dynamics + ), + + new NoteBendEvent(1440, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(1560, 0, info.primaryChannel, note1.realValue, 8704), + new NoteBendEvent(1680, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(1800, 0, info.primaryChannel, note1.realValue, 7680), + new NoteBendEvent(1920, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(2040, 0, info.primaryChannel, note1.realValue, 8704), + new NoteBendEvent(2160, 0, info.primaryChannel, note1.realValue, 8192), + new NoteBendEvent(2280, 0, info.primaryChannel, note1.realValue, 7680), + + // end of track + new TrackEndEvent(3840, 0) // 3840 = end of bar + ]; + for (let i: number = 0; i < handler.midiEvents.length; i++) { + Logger.info('Test', `i[${i}] ${handler.midiEvents[i]}`); + if (i < expectedEvents.length) { + expect(expectedEvents[i].equals(handler.midiEvents[i])) + .withContext(`i[${i}] expected[${expectedEvents[i]}] !== actual[${handler.midiEvents[i]}]`) + .toEqual(true); + } + } + expect(handler.midiEvents.length).toEqual(expectedEvents.length); + }); + it('full-bar-rest', () => { let tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4 | r.1 | 3.3.4 3.3.4 3.3.4'; let score: Score = parseTex(tex);