diff --git a/package.json b/package.json index 6b0a2c567..99c4a5c34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab", - "version": "0.9.8", + "version": "0.9.9", "description": "alphaTab is a music notation and guitar tablature rendering library", "keywords": [ "guitar", diff --git a/src/PlayerSettings.ts b/src/PlayerSettings.ts index 8ce0c6723..7851d250a 100644 --- a/src/PlayerSettings.ts +++ b/src/PlayerSettings.ts @@ -62,6 +62,34 @@ export class VibratoPlaybackSettings { public beatSlightAmplitude: number = 3; } +/** + * This object defines the details on how to generate the slide effects. + * @json + */ +export class SlidePlaybackSettings { + /** + * Gets or sets 1/4 tones (bend value) offset that + * simple slides like slide-out-below or slide-in-above use. + */ + public simpleSlidePitchOffset: number = 6; + + /** + * Gets or sets the percentage which the simple slides should take up + * from the whole note. for "slide into" effects the slide will take place + * from time 0 where the note is plucked to 25% of the overall note duration. + * For "slide out" effects the slide will start 75% and finish at 100% of the overall + * note duration. + */ + public simpleSlideDurationRatio: number = 0.25; + + /** + * Gets or sets the percentage which the legato and shift slides should take up + * from the whole note. For a value 0.5 the sliding will start at 50% of the overall note duration + * and finish at 100% + */ + public shiftSlideDurationRatio: number = 0.5; +} + /** * The player settings control how the audio playback and UI is behaving. * @json @@ -128,6 +156,11 @@ export class PlayerSettings { */ public readonly vibrato: VibratoPlaybackSettings = new VibratoPlaybackSettings(); + /** + * Gets or sets the setitngs on how the slide audio is generated. + */ + public readonly slide: SlidePlaybackSettings = new SlidePlaybackSettings(); + /** * Gets or sets whether the triplet feel should be applied/played during audio playback. */ diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 56a325367..530f9a63a 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -40,7 +40,7 @@ import { SynthConstants } from '@src/synth/SynthConstants'; export class MidiNoteDuration { public noteOnly: number = 0; - public untilTieEnd: number = 0; + public untilTieOrSlideEnd: number = 0; public letRingEnd: number = 0; } @@ -146,7 +146,13 @@ export class MidiFileGenerator { // Set PitchBendRangeCoarse to 12 this._handler.addControlChange(track.index, 0, channel, ControllerType.DataEntryFine, 0); - this._handler.addControlChange(track.index, 0, channel, ControllerType.DataEntryCoarse, MidiFileGenerator.PitchBendRangeInSemitones); + this._handler.addControlChange( + track.index, + 0, + channel, + ControllerType.DataEntryCoarse, + MidiFileGenerator.PitchBendRangeInSemitones + ); this._handler.addProgramChange(track.index, 0, channel, playbackInfo.program); } @@ -157,7 +163,8 @@ export class MidiFileGenerator { private generateMasterBar(masterBar: MasterBar, previousMasterBar: MasterBar | null, currentTick: number): void { // time signature - if (!previousMasterBar || + if ( + !previousMasterBar || previousMasterBar.timeSignatureDenominator !== masterBar.timeSignatureDenominator || previousMasterBar.timeSignatureNumerator !== masterBar.timeSignatureNumerator ) { @@ -394,7 +401,7 @@ export class MidiFileGenerator { const brushOffset: number = note.isStringed && note.string <= brushInfo.length ? brushInfo[note.string - 1] : 0; const noteStart: number = beatStart + brushOffset; const noteDuration: MidiNoteDuration = this.getNoteDuration(note, beatDuration); - noteDuration.untilTieEnd -= brushOffset; + noteDuration.untilTieOrSlideEnd -= brushOffset; noteDuration.noteOnly -= brushOffset; noteDuration.letRingEnd -= brushOffset; const dynamicValue: DynamicValue = MidiFileGenerator.getDynamicValue(note); @@ -408,7 +415,10 @@ export class MidiFileGenerator { initialBend = MidiFileGenerator.getPitchWheel(note.bendPoints[0].value); } else if (note.beat.hasWhammyBar) { initialBend = MidiFileGenerator.getPitchWheel(note.beat.whammyBarPoints[0].value); - } else if (note.isTieDestination) { + } else if ( + note.isTieDestination || + (note.slideOrigin && note.slideOrigin.slideOutType === SlideOutType.Legato) + ) { initialBend = -1; } else { initialBend = MidiFileGenerator.getPitchWheel(0); @@ -447,13 +457,15 @@ export class MidiFileGenerator { } else if (note.beat.hasWhammyBar && note.index === 0) { this.generateWhammy(note.beat, noteStart, noteDuration, channel); } else if (note.slideInType !== SlideInType.None || note.slideOutType !== SlideOutType.None) { - // TODO GenerateSlide(note, noteStart, noteDuration, noteKey, dynamicValue, channel); + this.generateSlide(note, noteStart, noteDuration, noteKey, dynamicValue, channel); } else if (note.vibrato !== VibratoType.None) { this.generateVibrato(note, noteStart, noteDuration, channel); } - if (!note.isTieDestination) { - let noteSoundDuration: number = Math.max(noteDuration.untilTieEnd, noteDuration.letRingEnd); + // for tied notes, and target notes of legato slides we do not pick the note + // the previous one is extended + if (!note.isTieDestination && (!note.slideOrigin || note.slideOrigin.slideOutType !== SlideOutType.Legato)) { + let noteSoundDuration: number = Math.max(noteDuration.untilTieOrSlideEnd, noteDuration.letRingEnd); this._handler.addNote(track.index, noteStart, noteSoundDuration, noteKey, dynamicValue, channel); } } @@ -461,11 +473,11 @@ export class MidiFileGenerator { private getNoteDuration(note: Note, duration: number): MidiNoteDuration { const durationWithEffects: MidiNoteDuration = new MidiNoteDuration(); durationWithEffects.noteOnly = duration; - durationWithEffects.untilTieEnd = duration; + durationWithEffects.untilTieOrSlideEnd = duration; durationWithEffects.letRingEnd = duration; if (note.isDead) { durationWithEffects.noteOnly = this.applyStaticDuration(MidiFileGenerator.DefaultDurationDead, duration); - durationWithEffects.untilTieEnd = durationWithEffects.noteOnly; + durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; durationWithEffects.letRingEnd = durationWithEffects.noteOnly; return durationWithEffects; } @@ -474,13 +486,13 @@ export class MidiFileGenerator { MidiFileGenerator.DefaultDurationPalmMute, duration ); - durationWithEffects.untilTieEnd = durationWithEffects.noteOnly; + durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; durationWithEffects.letRingEnd = durationWithEffects.noteOnly; return durationWithEffects; } if (note.isStaccato) { durationWithEffects.noteOnly = (duration / 2) | 0; - durationWithEffects.untilTieEnd = durationWithEffects.noteOnly; + durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; durationWithEffects.letRingEnd = durationWithEffects.noteOnly; return durationWithEffects; } @@ -494,8 +506,9 @@ export class MidiFileGenerator { endNote, endNote.beat.playbackDuration ); - const endTick: number = endNote.beat.absolutePlaybackStart + tieDestinationDuration.untilTieEnd; - durationWithEffects.untilTieEnd = endTick - startTick; + const endTick: number = + endNote.beat.absolutePlaybackStart + tieDestinationDuration.untilTieOrSlideEnd; + durationWithEffects.untilTieOrSlideEnd = endTick - startTick; } else { // for continuing ties, take the current duration + the one from the destination // this branch will be entered as part of the recusion of the if branch @@ -503,9 +516,20 @@ export class MidiFileGenerator { endNote, endNote.beat.playbackDuration ); - durationWithEffects.untilTieEnd = duration + tieDestinationDuration.untilTieEnd; + durationWithEffects.untilTieOrSlideEnd = duration + tieDestinationDuration.untilTieOrSlideEnd; } } + } else if (note.slideOutType === SlideOutType.Legato) { + const endNote: Note = note.slideTarget!; + if (endNote) { + const startTick: number = note.beat.absolutePlaybackStart; + const slideTargetDuration: MidiNoteDuration = this.getNoteDuration( + endNote, + endNote.beat.playbackDuration + ); + const endTick: number = endNote.beat.absolutePlaybackStart + slideTargetDuration.untilTieOrSlideEnd; + durationWithEffects.untilTieOrSlideEnd = endTick - startTick; + } } if (note.isLetRing && this._settings.notation.notationMode === NotationMode.GuitarPro) { @@ -539,7 +563,7 @@ export class MidiFileGenerator { durationWithEffects.letRingEnd = letRingEnd; } } else { - durationWithEffects.letRingEnd = durationWithEffects.untilTieEnd; + durationWithEffects.letRingEnd = durationWithEffects.untilTieOrSlideEnd; } return durationWithEffects; } @@ -666,32 +690,90 @@ export class MidiFileGenerator { /** * Maximum semitones that are supported in bends in one direction (up or down) - * GP has 8 full tones on whammys. + * GP has 8 full tones on whammys. */ private static readonly PitchBendRangeInSemitones = 8 * 2; /** * The value on how many pitch-values are used for one semitone */ - private static readonly PitchValuePerSemitone: number = SynthConstants.DefaultPitchWheel / MidiFileGenerator.PitchBendRangeInSemitones; + private static readonly PitchValuePerSemitone: number = + SynthConstants.DefaultPitchWheel / MidiFileGenerator.PitchBendRangeInSemitones; /** - * The minimum number of breakpoints generated per semitone bend. + * The minimum number of breakpoints generated per semitone bend. */ private static readonly MinBreakpointsPerSemitone = 6; /** - * How long until a new breakpoint is generated for a bend. + * How long until a new breakpoint is generated for a bend. */ private static readonly MillisecondsPerBreakpoint = 150; /** - * Calculates the midi pitch wheel value for the give bend value. + * Calculates the midi pitch wheel value for the give bend value. */ public static getPitchWheel(bendValue: number) { // bend values are 1/4 notes therefore we only take half a semitone value per bend value return SynthConstants.DefaultPitchWheel + (bendValue / 2) * MidiFileGenerator.PitchValuePerSemitone; } + private generateSlide( + note: Note, + noteStart: number, + noteDuration: MidiNoteDuration, + noteKey: number, + dynamicValue: DynamicValue, + channel: number + ) { + let duration: number = + note.slideOutType === SlideOutType.Legato ? noteDuration.noteOnly : noteDuration.untilTieOrSlideEnd; + let playedBendPoints: BendPoint[] = []; + let track: Track = note.beat.voice.bar.staff.track; + + const simpleSlidePitchOffset = this._settings.player.slide.simpleSlidePitchOffset; + const simpleSlideDurationOffset = Math.floor(BendPoint.MaxPosition * this._settings.player.slide.simpleSlideDurationRatio); + const shiftSlideDurationOffset = Math.floor(BendPoint.MaxPosition * this._settings.player.slide.shiftSlideDurationRatio); + + // Shift Slide: Play note, move up to target note, play end note + // Legato Slide: Play note, move up to target note, no pick on end note, just keep it ringing + + // 2 bend points: one on 0/0, dy/MaxPos. + + // Slide into from above/below: Play note on lower pitch, slide into it quickly at start + // Slide out above/blow: Play note on normal pitch, slide out quickly at end + + switch (note.slideInType) { + case SlideInType.IntoFromAbove: + playedBendPoints.push(new BendPoint(0, simpleSlidePitchOffset)); + playedBendPoints.push(new BendPoint(simpleSlideDurationOffset, 0)); + break; + case SlideInType.IntoFromBelow: + playedBendPoints.push(new BendPoint(0, -simpleSlidePitchOffset)); + playedBendPoints.push(new BendPoint(simpleSlideDurationOffset, 0)); + break; + } + + switch (note.slideOutType) { + case SlideOutType.Legato: + case SlideOutType.Shift: + playedBendPoints.push(new BendPoint(shiftSlideDurationOffset, 0)); + // normal note values are in 1/2 tones, bends are in 1/4 tones + const dy = (note.slideTarget!.realValue - note.realValue) * 2; + playedBendPoints.push(new BendPoint(BendPoint.MaxPosition, dy)); + break; + case SlideOutType.OutDown: + playedBendPoints.push(new BendPoint(BendPoint.MaxPosition - simpleSlideDurationOffset, 0)); + playedBendPoints.push(new BendPoint(BendPoint.MaxPosition, -simpleSlidePitchOffset)); + break; + case SlideOutType.OutUp: + playedBendPoints.push(new BendPoint(BendPoint.MaxPosition - simpleSlideDurationOffset, 0)); + playedBendPoints.push(new BendPoint(BendPoint.MaxPosition, simpleSlidePitchOffset)); + break; + } + + this.generateWhammyOrBend(noteStart, channel, duration, playedBendPoints, track); + } + private generateBend(note: Note, noteStart: number, noteDuration: MidiNoteDuration, channel: number): void { let bendPoints: BendPoint[] = note.bendPoints; let track: Track = note.beat.voice.bar.staff.track; @@ -798,11 +880,7 @@ export class MidiFileGenerator { duration, track, false, - [ - note.bendPoints[0].value, - note.bendPoints[1].value, - note.bendPoints[2].value - ], + [note.bendPoints[0].value, note.bendPoints[1].value, note.bendPoints[2].value], bendDuration ); return; @@ -1033,15 +1111,15 @@ export class MidiFileGenerator { ); const ticksPerBreakpoint: number = ticksBetweenPoints / numberOfSteps; const pitchPerBreakpoint = (nextBendValue - currentBendValue) / numberOfSteps; - - for(let i = 0; i < numberOfSteps; i++) { + + for (let i = 0; i < numberOfSteps; i++) { this._handler.addBend(track.index, currentTick | 0, channel, Math.round(currentBendValue)); currentBendValue += pitchPerBreakpoint; currentTick += ticksPerBreakpoint; } // final bend value if needed - if(currentBendValue < nextBendValue) { + if (currentBendValue < nextBendValue) { this._handler.addBend(track.index, currentTick | 0, channel, nextBendValue); } } @@ -1059,7 +1137,7 @@ export class MidiFileGenerator { let trillLength: number = MidiUtils.toTicks(note.trillSpeed); let realKey: boolean = true; let tick: number = noteStart; - let end: number = noteStart + noteDuration.untilTieEnd; + let end: number = noteStart + noteDuration.untilTieOrSlideEnd; while (tick + 10 < end) { // only the rest on last trill play if (tick + trillLength >= end) { @@ -1082,7 +1160,7 @@ export class MidiFileGenerator { const track: Track = note.beat.voice.bar.staff.track; let tpLength: number = MidiUtils.toTicks(note.beat.tremoloSpeed!); let tick: number = noteStart; - const end: number = noteStart + noteDuration.untilTieEnd; + const end: number = noteStart + noteDuration.untilTieOrSlideEnd; while (tick + 10 < end) { // only the rest on last trill play if (tick + tpLength >= end) { diff --git a/src/model/Note.ts b/src/model/Note.ts index ae12adb7a..ea663e913 100644 --- a/src/model/Note.ts +++ b/src/model/Note.ts @@ -233,6 +233,11 @@ export class Note { */ public slideTarget: Note | null = null; + /** + * Gets or sets the source note for several slide types. + */ + public slideOrigin: Note | null = null; + /** * Gets or sets whether a vibrato is played on the note. */ @@ -614,6 +619,8 @@ export class Note { this.slideTarget = nextNoteOnLine.value; if (!this.slideTarget) { this.slideOutType = SlideOutType.None; + } else { + this.slideTarget.slideOrigin = this; } break; }