import Vex from "vexflow"; import {GraphicalMeasure} from "../GraphicalMeasure"; import {SourceMeasure} from "../../VoiceData/SourceMeasure"; import {Staff} from "../../VoiceData/Staff"; import {StaffLine} from "../StaffLine"; import {SystemLinesEnum} from "../SystemLinesEnum"; import {ClefInstruction, ClefEnum} from "../../VoiceData/Instructions/ClefInstruction"; import {KeyInstruction} from "../../VoiceData/Instructions/KeyInstruction"; import {RhythmInstruction} from "../../VoiceData/Instructions/RhythmInstruction"; import {VexFlowConverter} from "./VexFlowConverter"; import {VexFlowStaffEntry} from "./VexFlowStaffEntry"; import {Beam} from "../../VoiceData/Beam"; import {GraphicalNote} from "../GraphicalNote"; import {GraphicalStaffEntry} from "../GraphicalStaffEntry"; import StaveConnector = Vex.Flow.StaveConnector; import StaveNote = Vex.Flow.StaveNote; import StemmableNote = Vex.Flow.StemmableNote; import NoteSubGroup = Vex.Flow.NoteSubGroup; import log from "loglevel"; import {unitInPixels} from "./VexFlowMusicSheetDrawer"; import {Tuplet} from "../../VoiceData/Tuplet"; import {RepetitionInstructionEnum, RepetitionInstruction, AlignmentType} from "../../VoiceData/Instructions/RepetitionInstruction"; import {SystemLinePosition} from "../SystemLinePosition"; import {StemDirectionType} from "../../VoiceData/VoiceEntry"; import {GraphicalVoiceEntry} from "../GraphicalVoiceEntry"; import {VexFlowVoiceEntry} from "./VexFlowVoiceEntry"; import {Fraction} from "../../../Common/DataObjects/Fraction"; import {Voice} from "../../VoiceData/Voice"; import {LinkedVoice} from "../../VoiceData/LinkedVoice"; import { EngravingRules } from "../EngravingRules"; import {OrnamentContainer} from "../../VoiceData/OrnamentContainer"; import {TechnicalInstruction, TechnicalInstructionType} from "../../VoiceData/Instructions/TechnicalInstruction"; import {PlacementEnum} from "../../VoiceData/Expressions/AbstractExpression"; import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote"; import {AutoBeamOptions} from "../../../OpenSheetMusicDisplay/OSMDOptions"; import {NoteType, Arpeggio} from "../../VoiceData"; import { SkyBottomLineCalculator } from "../SkyBottomLineCalculator"; // type StemmableNote = Vex.Flow.StemmableNote; export class VexFlowMeasure extends GraphicalMeasure { constructor(staff: Staff, sourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) { super(staff, sourceMeasure, staffLine); this.minimumStaffEntriesWidth = -1; /* * There is no case in which `staffLine === undefined && sourceMeasure === undefined` holds. * Hence, it is not necessary to specify an `else` case. * One can verify this through a usage search for this constructor. */ if (staffLine) { this.rules = staffLine.ParentMusicSystem.rules; } else if (sourceMeasure) { this.rules = sourceMeasure.Rules; } this.resetLayout(); } /** octaveOffset according to active clef */ public octaveOffset: number = 3; /** The VexFlow Voices in the measure */ public vfVoices: { [voiceID: number]: Vex.Flow.Voice; } = {}; /** Call this function (if present) to x-format all the voices in the measure */ public formatVoices: (width: number) => void; /** The VexFlow Ties in the measure */ public vfTies: Vex.Flow.StaveTie[] = []; /** The repetition instructions given as words or symbols (coda, dal segno..) */ public vfRepetitionWords: Vex.Flow.Repetition[] = []; /** The VexFlow Stave (= one measure in a staffline) */ protected stave: Vex.Flow.Stave; /** VexFlow StaveConnectors (vertical lines) */ protected connectors: Vex.Flow.StaveConnector[] = []; /** Intermediate object to construct beams */ private beams: { [voiceID: number]: [Beam, VexFlowVoiceEntry[]][]; } = {}; /** Beams created by (optional) autoBeam function. */ private autoVfBeams: Vex.Flow.Beam[]; /** Beams of tuplet notes created by (optional) autoBeam function. */ private autoTupletVfBeams: Vex.Flow.Beam[]; /** VexFlow Beams */ private vfbeams: { [voiceID: number]: Vex.Flow.Beam[]; }; /** Intermediate object to construct tuplets */ protected tuplets: { [voiceID: number]: [Tuplet, VexFlowVoiceEntry[]][]; } = {}; /** VexFlow Tuplets */ private vftuplets: { [voiceID: number]: Vex.Flow.Tuplet[]; } = {}; // The engraving rules of OSMD. public rules: EngravingRules; // Sets the absolute coordinates of the VFStave on the canvas public setAbsoluteCoordinates(x: number, y: number): void { this.stave.setX(x).setY(y); } /** * Reset all the geometric values and parameters of this measure and put it in an initialized state. * This is needed to evaluate a measure a second time by system builder. */ public resetLayout(): void { // Take into account some space for the begin and end lines of the stave // Will be changed when repetitions will be implemented //this.beginInstructionsWidth = 20 / UnitInPixels; //this.endInstructionsWidth = 20 / UnitInPixels; // TODO save beginning and end bar type, set these again after new stave. this.stave = new Vex.Flow.Stave(0, 0, 0, { space_above_staff_ln: 0, space_below_staff_ln: 0, }); // constructor sets beginning and end bar type to standard this.stave.setBegBarType(Vex.Flow.Barline.type.NONE); // technically not correct, but we'd need to set the next measure's beginning bar type if (this.parentSourceMeasure && this.parentSourceMeasure.endingBarStyleEnum === SystemLinesEnum.None) { // fix for vexflow ignoring ending barline style after new stave, apparently this.stave.setEndBarType(Vex.Flow.Barline.type.NONE); } // the correct bar types seem to be set later this.updateInstructionWidth(); } public clean(): void { this.vfTies.length = 0; this.connectors = []; // Clean up instructions this.resetLayout(); } /** * returns the x-width (in units) of a given measure line {SystemLinesEnum}. * @param line * @returns the x-width in osmd units */ public getLineWidth(line: SystemLinesEnum): number { switch (line) { // return 0 for the normal lines, as the line width will be considered at the updateInstructionWidth() method using the stavemodifiers. // case SystemLinesEnum.SingleThin: // return 5.0 / unitInPixels; // case SystemLinesEnum.DoubleThin: // return 5.0 / unitInPixels; // case SystemLinesEnum.ThinBold: // return 5.0 / unitInPixels; // but just add a little extra space for repetitions (cosmetics): case SystemLinesEnum.BoldThinDots: case SystemLinesEnum.DotsThinBold: return 10.0 / unitInPixels; case SystemLinesEnum.DotsBoldBoldDots: return 10.0 / unitInPixels; default: return 0; } } /** * adds the given clef to the begin of the measure. * This has to update/increase BeginInstructionsWidth. * @param clef */ public addClefAtBegin(clef: ClefInstruction): void { this.octaveOffset = clef.OctaveOffset; if (clef.ClefType === ClefEnum.TAB) { this.stave.addClef("tab", undefined, undefined, undefined); } else { const vfclef: { type: string, size: string, annotation: string } = VexFlowConverter.Clef(clef, "default"); this.stave.addClef(vfclef.type, vfclef.size, vfclef.annotation, Vex.Flow.StaveModifier.Position.BEGIN); } this.updateInstructionWidth(); } /** * adds the given key to the begin of the measure. * This has to update/increase BeginInstructionsWidth. * @param currentKey the new valid key. * @param previousKey the old cancelled key. Needed to show which accidentals are not valid any more. * @param currentClef the valid clef. Needed to put the accidentals on the right y-positions. */ public addKeyAtBegin(currentKey: KeyInstruction, previousKey: KeyInstruction, currentClef: ClefInstruction): void { this.stave.setKeySignature( VexFlowConverter.keySignature(currentKey), VexFlowConverter.keySignature(previousKey), undefined ); this.updateInstructionWidth(); } /** * adds the given rhythm to the begin of the measure. * This has to update/increase BeginInstructionsWidth. * @param rhythm */ public addRhythmAtBegin(rhythm: RhythmInstruction): void { const timeSig: Vex.Flow.TimeSignature = VexFlowConverter.TimeSignature(rhythm); this.stave.addModifier( timeSig, Vex.Flow.StaveModifier.Position.BEGIN ); this.updateInstructionWidth(); } /** * adds the given clef to the end of the measure. * This has to update/increase EndInstructionsWidth. * @param clef */ public addClefAtEnd(clef: ClefInstruction): void { const vfclef: { type: string, size: string, annotation: string } = VexFlowConverter.Clef(clef, "small"); this.stave.setEndClef(vfclef.type, vfclef.size, vfclef.annotation); this.updateInstructionWidth(); } public addMeasureLine(lineType: SystemLinesEnum, linePosition: SystemLinePosition): void { switch (linePosition) { case SystemLinePosition.MeasureBegin: switch (lineType) { case SystemLinesEnum.BoldThinDots: this.stave.setBegBarType(Vex.Flow.Barline.type.REPEAT_BEGIN); break; default: //this.stave.setBegBarType(Vex.Flow.Barline.type.NONE); // not necessary, it seems break; } break; case SystemLinePosition.MeasureEnd: switch (lineType) { case SystemLinesEnum.DotsBoldBoldDots: this.stave.setEndBarType(Vex.Flow.Barline.type.REPEAT_BOTH); break; case SystemLinesEnum.DotsThinBold: this.stave.setEndBarType(Vex.Flow.Barline.type.REPEAT_END); break; case SystemLinesEnum.DoubleThin: this.stave.setEndBarType(Vex.Flow.Barline.type.DOUBLE); break; case SystemLinesEnum.ThinBold: this.stave.setEndBarType(Vex.Flow.Barline.type.END); break; case SystemLinesEnum.None: this.stave.setEndBarType(Vex.Flow.Barline.type.NONE); break; // TODO: Add support for additional Barline types when VexFlow supports them default: break; } break; default: break; } } /** * Adds a measure number to the top left corner of the measure * This method is not used currently in favor of the calculateMeasureNumberPlacement * method in the MusicSheetCalculator.ts */ public addMeasureNumber(): void { const text: string = this.MeasureNumber.toString(); const position: number = StavePositionEnum.ABOVE; //Vex.Flow.StaveModifier.Position.ABOVE; const options: any = { justification: 1, shift_x: 0, shift_y: 0, }; this.stave.setText(text, position, options); } public addWordRepetition(repetitionInstruction: RepetitionInstruction): void { let instruction: Vex.Flow.Repetition.type = undefined; let position: any = Vex.Flow.StaveModifier.Position.END; switch (repetitionInstruction.type) { case RepetitionInstructionEnum.Segno: // create Segno Symbol: instruction = Vex.Flow.Repetition.type.SEGNO_LEFT; position = Vex.Flow.StaveModifier.Position.BEGIN; break; case RepetitionInstructionEnum.Coda: // create Coda Symbol: instruction = Vex.Flow.Repetition.type.CODA_LEFT; position = Vex.Flow.StaveModifier.Position.BEGIN; break; case RepetitionInstructionEnum.DaCapo: instruction = Vex.Flow.Repetition.type.DC; break; case RepetitionInstructionEnum.DalSegno: instruction = Vex.Flow.Repetition.type.DS; break; case RepetitionInstructionEnum.Fine: instruction = Vex.Flow.Repetition.type.FINE; break; case RepetitionInstructionEnum.ToCoda: //instruction = "To Coda"; break; case RepetitionInstructionEnum.DaCapoAlFine: instruction = Vex.Flow.Repetition.type.DC_AL_FINE; break; case RepetitionInstructionEnum.DaCapoAlCoda: instruction = Vex.Flow.Repetition.type.DC_AL_CODA; break; case RepetitionInstructionEnum.DalSegnoAlFine: instruction = Vex.Flow.Repetition.type.DS_AL_FINE; break; case RepetitionInstructionEnum.DalSegnoAlCoda: instruction = Vex.Flow.Repetition.type.DS_AL_CODA; break; default: break; } if (instruction !== undefined) { this.stave.addModifier(new Vex.Flow.Repetition(instruction, 0, 0), position); return; } this.addVolta(repetitionInstruction); } private addVolta(repetitionInstruction: RepetitionInstruction): void { let voltaType: number = Vex.Flow.Volta.type.BEGIN; if (repetitionInstruction.type === RepetitionInstructionEnum.Ending) { switch (repetitionInstruction.alignment) { case AlignmentType.Begin: if (this.parentSourceMeasure.endsRepetitionEnding()) { voltaType = Vex.Flow.Volta.type.BEGIN_END; } else { voltaType = Vex.Flow.Volta.type.BEGIN; } break; case AlignmentType.End: if (this.parentSourceMeasure.beginsRepetitionEnding()) { //voltaType = Vex.Flow.Volta.type.BEGIN_END; // don't add BEGIN_END volta a second time: return; } else { voltaType = Vex.Flow.Volta.type.END; } break; default: break; } const skyBottomLineCalculator: SkyBottomLineCalculator = this.ParentStaffLine.SkyBottomLineCalculator; //Because of loss of accuracy when sampling (see SkyBottomLineCalculator.updateInRange), measures tend to overlap //This causes getSkyLineMinInRange to return an incorrect min value (one from the previous measure, which has been modified) //We need to offset the end of what we are measuring by a bit to prevent this, otherwise volta pairs step up const start: number = this.PositionAndShape.AbsolutePosition.x + this.PositionAndShape.BorderMarginLeft + 0.4; const end: number = this.PositionAndShape.AbsolutePosition.x + this.PositionAndShape.BorderMarginRight; //2 unit gap, since volta is positioned from y center it seems. //This prevents cases where the volta is rendered over another element const skylineMinForMeasure: number = skyBottomLineCalculator.getSkyLineMinInRange( start, end ) - 2; //-6 OSMD units is the 0 value that the volta is placed on. .1 extra so the skyline goes above the volta //instead of on the line itself let newSkylineValueForMeasure: number = -6.1 + this.rules.VoltaOffset; let vexFlowVoltaHeight: number = this.rules.VoltaOffset; //EngravingRules default offset is 2.5, can be user set. //2.5 gives us a good default value to work with. //if we calculate that the minimum skyline allowed by elements is above the default volta position, need to adjust volta up further if (skylineMinForMeasure < newSkylineValueForMeasure) { const skylineDifference: number = skylineMinForMeasure - newSkylineValueForMeasure; vexFlowVoltaHeight += skylineDifference; newSkylineValueForMeasure = skylineMinForMeasure; } /* Code here that is commented out is for finding the previous measure volta height and matching it or resetting it to ours. Still needs work. When we get to higher measure numbers, prev measures don't seem to be available? //if we already have a volta in the prev measure, should match it's height, or if we are higher, it should match ours //find previous sibling measure that may have volta let measureIndex = this.parentSourceMeasure.measureListIndex; //this.parentSourceMeasure.getPreviousMeasure(); if(measureIndex > 0){ let prevMeasureIndex = measureIndex-1; let prevMeasure : VexFlowMeasure = undefined; // find first visible StaffLine //Need to find the uppermost, where the volta would go const measures: VexFlowMeasure[] = this.ParentMusicSystem.GraphicalMeasures[prevMeasureIndex]; if(measures !== undefined && measures.length > 0){ for (let idx: number = 0, len: number = measures.length; idx < len; ++idx) { const graphicalMeasure: VexFlowMeasure = measures[idx]; if (graphicalMeasure.ParentStaffLine !== undefined && graphicalMeasure.ParentStaff.ParentInstrument.Visible) { prevMeasure = graphicalMeasure; break; } } } if(prevMeasure !== undefined){ let prevStaveModifiers = prevMeasure.stave.getModifiers(); for(let i = 0; i < prevStaveModifiers.length; i++){ let nextStaveModifier = prevStaveModifiers[i]; if(nextStaveModifier.hasOwnProperty("volta")){ const prevskyBottomLineCalculator: SkyBottomLineCalculator = prevMeasure.ParentStaffLine.SkyBottomLineCalculator; const prevStart: number = prevMeasure.PositionAndShape.AbsolutePosition.x + prevMeasure.PositionAndShape.BorderMarginLeft + 0.4; const prevEnd: number = prevMeasure.PositionAndShape.AbsolutePosition.x + prevMeasure.PositionAndShape.BorderMarginRight; let prevMeasureSkyline: number = prevskyBottomLineCalculator.getSkyLineMinInRange(prevStart, prevEnd); //if prev skyline is higher, use it if(prevMeasureSkyline <= newSkylineValueForMeasure){ let skylineDifference = prevMeasureSkyline - newSkylineValueForMeasure; vexFlowVoltaHeight += skylineDifference; newSkylineValueForMeasure = prevMeasureSkyline; } else { //otherwise, we are higher. Need to adjust prev (nextStaveModifier as any).y_shift = vexFlowVoltaHeight*10; prevMeasure.ParentStaffLine.SkyBottomLineCalculator.updateSkyLineInRange(prevStart, prevEnd, newSkylineValueForMeasure); } } } } }*/ //convert to VF units (pixels) vexFlowVoltaHeight *= 10; this.stave.setVoltaType(voltaType, repetitionInstruction.endingIndices[0], vexFlowVoltaHeight); skyBottomLineCalculator.updateSkyLineInRange(start, end, newSkylineValueForMeasure); //this.ParentStaffLine.SkyBottomLineCalculator.calculateLines(); } } /** * Sets the overall x-width of the measure. * @param width */ public setWidth(width: number): void { super.setWidth(width); // Set the width of the Vex.Flow.Stave this.stave.setWidth(width * unitInPixels); // Force the width of the Begin Instructions //this.stave.setNoteStartX(this.beginInstructionsWidth * UnitInPixels); } /** * This method is called after the StaffEntriesScaleFactor has been set. * Here the final x-positions of the staff entries have to be set. * (multiply the minimal positions with the scaling factor, considering the BeginInstructionsWidth) */ public layoutSymbols(): void { // vexflow does the x-layout } /** * Draw this measure on a VexFlow CanvasContext * @param ctx */ public draw(ctx: Vex.IRenderContext): void { // Draw stave lines this.stave.setContext(ctx).draw(); // Draw all voices for (const voiceID in this.vfVoices) { if (this.vfVoices.hasOwnProperty(voiceID)) { ctx.save(); this.vfVoices[voiceID].draw(ctx, this.stave); ctx.restore(); // this.vfVoices[voiceID].tickables.forEach(t => t.getBoundingBox().draw(ctx)); // this.vfVoices[voiceID].tickables.forEach(t => t.getBoundingBox().draw(ctx)); } } // Draw beams for (const voiceID in this.vfbeams) { if (this.vfbeams.hasOwnProperty(voiceID)) { for (const beam of this.vfbeams[voiceID]) { beam.setContext(ctx).draw(); } } } // Draw auto-generated beams from Beam.generateBeams() if (this.autoVfBeams) { for (const beam of this.autoVfBeams) { beam.setContext(ctx).draw(); } } if (this.autoTupletVfBeams) { for (const beam of this.autoTupletVfBeams) { beam.setContext(ctx).draw(); } } // Draw tuplets for (const voiceID in this.vftuplets) { if (this.vftuplets.hasOwnProperty(voiceID)) { for (const tuplet of this.vftuplets[voiceID]) { tuplet.setContext(ctx).draw(); } } } // Draw ties for (const tie of this.vfTies) { tie.setContext(ctx).draw(); } // Draw vertical lines for (const connector of this.connectors) { connector.setContext(ctx).draw(); } } // this currently formats multiple measures, see VexFlowMusicSheetCalculator.formatMeasures() public format(): void { // If this is the first stave in the vertical measure, call the format // method to set the width of all the voices if (this.formatVoices) { // set the width of the voices to the current measure width: // (The width of the voices does not include the instructions (StaveModifiers)) this.formatVoices((this.PositionAndShape.Size.width - this.beginInstructionsWidth - this.endInstructionsWidth) * unitInPixels); } } /** * Returns all the voices that are present in this measure */ public getVoicesWithinMeasure(): Voice[] { const voices: Voice[] = []; for (const gse of this.staffEntries) { for (const gve of gse.graphicalVoiceEntries) { if (voices.indexOf(gve.parentVoiceEntry.ParentVoice) === -1) { voices.push(gve.parentVoiceEntry.ParentVoice); } } } return voices; } /** * Returns all the graphicalVoiceEntries of a given Voice. * @param voice the voice for which the graphicalVoiceEntries shall be returned. */ public getGraphicalVoiceEntriesPerVoice(voice: Voice): GraphicalVoiceEntry[] { const voiceEntries: GraphicalVoiceEntry[] = []; for (const gse of this.staffEntries) { for (const gve of gse.graphicalVoiceEntries) { if (gve.parentVoiceEntry.ParentVoice === voice) { voiceEntries.push(gve); } } } return voiceEntries; } /** * Finds the gaps between the existing notes within a measure. * Problem here is, that the graphicalVoiceEntry does not exist yet and * that Tied notes are not present in the normal voiceEntries. * To handle this, calculation with absolute timestamps is needed. * And the graphical notes have to be analysed directly (and not the voiceEntries, as it actually should be -> needs refactoring) * @param voice the voice for which the ghost notes shall be searched. */ protected getRestFilledVexFlowStaveNotesPerVoice(voice: Voice): GraphicalVoiceEntry[] { let latestVoiceTimestamp: Fraction = undefined; const gvEntries: GraphicalVoiceEntry[] = this.getGraphicalVoiceEntriesPerVoice(voice); for (let idx: number = 0, len: number = gvEntries.length; idx < len; ++idx) { const gve: GraphicalVoiceEntry = gvEntries[idx]; const gNotesStartTimestamp: Fraction = gve.notes[0].sourceNote.getAbsoluteTimestamp(); // find the voiceEntry end timestamp: let gNotesEndTimestamp: Fraction = new Fraction(); for (const graphicalNote of gve.notes) { const noteEnd: Fraction = Fraction.plus(graphicalNote.sourceNote.getAbsoluteTimestamp(), graphicalNote.sourceNote.Length); if (gNotesEndTimestamp.lt(noteEnd)) { gNotesEndTimestamp = noteEnd; } } // check if this voice has just been found the first time: if (latestVoiceTimestamp === undefined) { // if this voice is new, check for a gap from measure start to the start of the current voice entry: const gapFromMeasureStart: Fraction = Fraction.minus(gNotesStartTimestamp, this.parentSourceMeasure.AbsoluteTimestamp); if (gapFromMeasureStart.RealValue > 0) { log.trace(`Ghost Found at start (measure ${this.MeasureNumber})`); // happens too often for valid measures to be logged to debug const vfghost: Vex.Flow.GhostNote = VexFlowConverter.GhostNote(gapFromMeasureStart); const ghostGve: VexFlowVoiceEntry = new VexFlowVoiceEntry(undefined, undefined); ghostGve.vfStaveNote = vfghost; gvEntries.splice(0, 0, ghostGve); idx++; } } else { // get the length of the empty space between notes: const inBetweenLength: Fraction = Fraction.minus(gNotesStartTimestamp, latestVoiceTimestamp); if (inBetweenLength.RealValue > 0) { log.trace(`Ghost Found in between (measure ${this.MeasureNumber})`); // happens too often for valid measures to be logged to debug const vfghost: Vex.Flow.GhostNote = VexFlowConverter.GhostNote(inBetweenLength); const ghostGve: VexFlowVoiceEntry = new VexFlowVoiceEntry(undefined, undefined); ghostGve.vfStaveNote = vfghost; // add element before current element: gvEntries.splice(idx, 0, ghostGve); // and increase index, as we added an element: idx++; } } // finally set the latest timestamp of this voice to the end timestamp of the longest note in the current voiceEntry: latestVoiceTimestamp = gNotesEndTimestamp; } const measureEndTimestamp: Fraction = Fraction.plus(this.parentSourceMeasure.AbsoluteTimestamp, this.parentSourceMeasure.Duration); const restLength: Fraction = Fraction.minus(measureEndTimestamp, latestVoiceTimestamp); if (restLength.RealValue > 0) { // fill the gap with a rest ghost note // starting from lastFraction // with length restLength: log.trace(`Ghost Found at end (measure ${this.MeasureNumber})`); // happens too often for valid measures to be logged to debug const vfghost: Vex.Flow.GhostNote = VexFlowConverter.GhostNote(restLength); const ghostGve: VexFlowVoiceEntry = new VexFlowVoiceEntry(undefined, undefined); ghostGve.vfStaveNote = vfghost; gvEntries.push(ghostGve); } return gvEntries; } /** * Add a note to a beam * @param graphicalNote * @param beam */ public handleBeam(graphicalNote: GraphicalNote, beam: Beam): void { const voiceID: number = graphicalNote.sourceNote.ParentVoiceEntry.ParentVoice.VoiceId; let beams: [Beam, VexFlowVoiceEntry[]][] = this.beams[voiceID]; if (beams === undefined) { beams = this.beams[voiceID] = []; } let data: [Beam, VexFlowVoiceEntry[]]; for (const mybeam of beams) { if (mybeam[0] === beam) { data = mybeam; } } if (data === undefined) { data = [beam, []]; beams.push(data); } const parent: VexFlowVoiceEntry = graphicalNote.parentVoiceEntry as VexFlowVoiceEntry; if (data[1].indexOf(parent) < 0) { data[1].push(parent); } } public handleTuplet(graphicalNote: GraphicalNote, tuplet: Tuplet): void { const voiceID: number = graphicalNote.sourceNote.ParentVoiceEntry.ParentVoice.VoiceId; tuplet = graphicalNote.sourceNote.NoteTuplet; let tuplets: [Tuplet, VexFlowVoiceEntry[]][] = this.tuplets[voiceID]; if (tuplets === undefined) { tuplets = this.tuplets[voiceID] = []; } let currentTupletBuilder: [Tuplet, VexFlowVoiceEntry[]]; for (const t of tuplets) { if (t[0] === tuplet) { currentTupletBuilder = t; } } if (currentTupletBuilder === undefined) { currentTupletBuilder = [tuplet, []]; tuplets.push(currentTupletBuilder); } const parent: VexFlowVoiceEntry = graphicalNote.parentVoiceEntry as VexFlowVoiceEntry; if (currentTupletBuilder[1].indexOf(parent) < 0) { currentTupletBuilder[1].push(parent); } } /** * Complete the creation of VexFlow Beams in this measure */ public finalizeBeams(): void { // The following line resets the created Vex.Flow Beams and // created them brand new. Is this needed? And more importantly, // should the old beams be removed manually by the notes? this.vfbeams = {}; const beamedNotes: StaveNote[] = []; // already beamed notes, will be ignored by this.autoBeamNotes() for (const voiceID in this.beams) { if (this.beams.hasOwnProperty(voiceID)) { let vfbeams: Vex.Flow.Beam[] = this.vfbeams[voiceID]; if (vfbeams === undefined) { vfbeams = this.vfbeams[voiceID] = []; } for (const beam of this.beams[voiceID]) { let beamHasQuarterNoteOrLonger: boolean = false; for (const note of beam[0].Notes) { if (note.Length.RealValue >= new Fraction(1, 4).RealValue // check whether the note has a TypeLength that's also not suitable for a beam (bigger than an eigth) && (note.TypeLength === undefined || note.TypeLength.RealValue > 0.125)) { beamHasQuarterNoteOrLonger = true; break; } } if (beamHasQuarterNoteOrLonger) { log.debug("Beam between note >= quarter, likely tremolo, currently unsupported. continuing."); continue; } const notes: Vex.Flow.StaveNote[] = []; const psBeam: Beam = beam[0]; const voiceEntries: VexFlowVoiceEntry[] = beam[1]; let autoStemBeam: boolean = true; for (const gve of voiceEntries) { if (gve.parentVoiceEntry.ParentVoice === psBeam.Notes[0].ParentVoiceEntry.ParentVoice) { autoStemBeam = gve.parentVoiceEntry.WantedStemDirection === StemDirectionType.Undefined; } } let isGraceBeam: boolean = false; let beamColor: string; const stemColors: string[] = []; for (const entry of voiceEntries) { const note: Vex.Flow.StaveNote = ((entry).vfStaveNote as StaveNote); if (note !== undefined) { notes.push(note); beamedNotes.push(note); } if (entry.parentVoiceEntry.IsGrace) { isGraceBeam = true; } if (entry.parentVoiceEntry.StemColor && this.rules.ColoringEnabled) { stemColors.push(entry.parentVoiceEntry.StemColor); } } if (notes.length > 1) { const vfBeam: Vex.Flow.Beam = new Vex.Flow.Beam(notes, autoStemBeam); if (isGraceBeam) { // smaller beam, as in Vexflow.GraceNoteGroup.beamNotes() (vfBeam).render_options.beam_width = 3; (vfBeam).render_options.partial_beam_length = 4; } if (stemColors.length >= 2 && this.rules.ColorBeams) { beamColor = stemColors[0]; for (const stemColor of stemColors) { if (stemColor !== beamColor) { beamColor = undefined; break; } } vfBeam.setStyle({ fillStyle: beamColor, strokeStyle: beamColor }); } vfbeams.push(vfBeam); } else { log.debug("Warning! Beam with no notes!"); } } } } if (this.rules.AutoBeamNotes) { this.autoBeamNotes(beamedNotes); // try to autobeam notes except those that are already beamed (beamedNotes). } } /** Automatically creates beams for notes except beamedNotes, using Vexflow's Beam.generateBeams(). * Takes options from this.rules.AutoBeamOptions. * @param beamedNotes notes that will not be autobeamed (usually because they are already beamed) */ private autoBeamNotes(beamedNotes: StemmableNote[]): void { let notesToAutoBeam: StemmableNote[] = []; let consecutiveBeamableNotes: StemmableNote[] = []; let currentTuplet: Tuplet; let tupletNotesToAutoBeam: StaveNote[] = []; this.autoTupletVfBeams = []; const separateAutoBeams: StemmableNote[][] = []; // a set of separate beams, each having a set of notes (StemmableNote[]). this.autoVfBeams = []; // final Vex.Flow.Beams will be pushed/collected into this let timeSignature: Fraction = this.parentSourceMeasure.ActiveTimeSignature; if (timeSignature === undefined) { // this doesn't happen in OSMD, but maybe in a SourceGenerator timeSignature = this.parentSourceMeasure.Duration; // suboptimal, can be 1/1 in a 4/4 time signature } /*if (this.parentSourceMeasure.FirstInstructionsStaffEntries[0]) { for (const instruction of this.parentSourceMeasure.FirstInstructionsStaffEntries[0].Instructions) { if (instruction instanceof RhythmInstruction) { // there is not always a RhythmInstruction, but this could be useful some time. timeSignature = (instruction as RhythmInstruction).Rhythm; } } }*/ for (const staffEntry of this.staffEntries) { for (const gve of staffEntry.graphicalVoiceEntries) { const vfStaveNote: StaveNote = (gve as VexFlowVoiceEntry).vfStaveNote; const gNote: GraphicalNote = gve.notes[0]; // TODO check for all notes within the graphical voice entry const isOnBeat: boolean = staffEntry.relInMeasureTimestamp.isOnBeat(timeSignature); const haveTwoOrMoreNotesToBeamAlready: boolean = consecutiveBeamableNotes.length >= 2; //const noteIsQuarterOrLonger: boolean = gNote.sourceNote.Length.CompareTo(new Fraction(1, 4)) >= 0; // trusting Fraction class, no float check const noteIsQuarterOrLonger: boolean = gNote.sourceNote.Length.RealValue - new Fraction(1, 4).RealValue > (-Fraction.FloatInaccuracyTolerance); const unbeamableNote: boolean = gve.parentVoiceEntry.IsGrace || // don't beam grace notes noteIsQuarterOrLonger || // don't beam quarter or longer notes beamedNotes.contains(vfStaveNote); if (unbeamableNote || isOnBeat) { // end beam if (haveTwoOrMoreNotesToBeamAlready) { // if we already have at least 2 notes to beam, beam them. don't beam notes surrounded by quarter notes etc. for (const note of consecutiveBeamableNotes) { notesToAutoBeam.push(note); // "flush" already beamed notes } separateAutoBeams.push(notesToAutoBeam.slice()); // copy array, otherwise this beam gets the next notes of next beam later notesToAutoBeam = []; // reset notesToAutoBeam, otherwise the next beam includes the previous beam's notes too } consecutiveBeamableNotes = []; // reset notes to beam if (unbeamableNote) { continue; } // else, note will be pushed to consecutiveBeamableNotes after tuplet check, also for note on new beat } // create beams for tuplets separately const noteTuplet: Tuplet = gve.notes[0].sourceNote.NoteTuplet; if (noteTuplet) { // check if there are quarter notes or longer in the tuplet, then don't beam. // (TODO: check for consecutiveBeamableNotes inside tuplets like for non-tuplet notes above // e.g quarter eigth eighth -> beam the two eigth notes) let tupletContainsUnbeamableNote: boolean = false; for (const notes of noteTuplet.Notes) { for (const note of notes) { //const stavenote: StemmableNote = (gve as VexFlowVoiceEntry).vfStaveNote; //console.log("note " + note.ToString() + ", stavenote type: " + stavenote.getNoteType()); if (note.NoteTypeXml >= NoteType.QUARTER || // quarter note or longer: don't beam // TODO: don't take Note (head) type from XML, but from current model, // so that rendering can react dynamically to changes compared to the XML. // however, taking the note length as fraction is tricky because of tuplets. // a quarter in a triplet has length < quarter, but quarter note head, which Vexflow can't beam. note.ParentVoiceEntry.IsGrace || note.isRest() && !this.rules.AutoBeamOptions.beam_rests) { tupletContainsUnbeamableNote = true; break; } } if (tupletContainsUnbeamableNote) { break; } } if (currentTuplet === undefined) { currentTuplet = noteTuplet; } else { if (currentTuplet !== noteTuplet) { // new tuplet, finish old one if (tupletNotesToAutoBeam.length > 1) { this.autoTupletVfBeams.push(new Vex.Flow.Beam(tupletNotesToAutoBeam, true)); } tupletNotesToAutoBeam = []; currentTuplet = noteTuplet; } } if (!tupletContainsUnbeamableNote) { tupletNotesToAutoBeam.push(vfStaveNote); } continue; } else { currentTuplet = undefined; } consecutiveBeamableNotes.push(vfStaveNote); // also happens on new beat } } if (tupletNotesToAutoBeam.length >= 2) { this.autoTupletVfBeams.push(new Vex.Flow.Beam(tupletNotesToAutoBeam, true)); } if (consecutiveBeamableNotes.length >= 2) { for (const note of consecutiveBeamableNotes) { notesToAutoBeam.push(note); } separateAutoBeams.push(notesToAutoBeam); } // create options for generateBeams const autoBeamOptions: AutoBeamOptions = this.rules.AutoBeamOptions; const generateBeamOptions: any = { beam_middle_only: autoBeamOptions.beam_middle_rests_only, beam_rests: autoBeamOptions.beam_rests, maintain_stem_directions: autoBeamOptions.maintain_stem_directions, }; if (autoBeamOptions.groups && autoBeamOptions.groups.length) { const groups: Vex.Flow.Fraction[] = []; for (const fraction of autoBeamOptions.groups) { groups.push(new Vex.Flow.Fraction(fraction[0], fraction[1])); } generateBeamOptions.groups = groups; } for (const notesForSeparateAutoBeam of separateAutoBeams) { const newBeams: Vex.Flow.Beam[] = Vex.Flow.Beam.generateBeams(notesForSeparateAutoBeam, generateBeamOptions); for (const beam of newBeams) { this.autoVfBeams.push(beam); } } } /** * Complete the creation of VexFlow Tuplets in this measure */ public finalizeTuplets(): void { // The following line resets the created Vex.Flow Tuplets and // created them brand new. Is this needed? And more importantly, // should the old tuplets be removed manually from the notes? this.vftuplets = {}; for (const voiceID in this.tuplets) { if (this.tuplets.hasOwnProperty(voiceID)) { let vftuplets: Vex.Flow.Tuplet[] = this.vftuplets[voiceID]; if (vftuplets === undefined) { vftuplets = this.vftuplets[voiceID] = []; } for (const tupletBuilder of this.tuplets[voiceID]) { const tupletStaveNotes: Vex.Flow.StaveNote[] = []; const tupletVoiceEntries: VexFlowVoiceEntry[] = tupletBuilder[1]; for (const tupletVoiceEntry of tupletVoiceEntries) { tupletStaveNotes.push(((tupletVoiceEntry).vfStaveNote as StaveNote)); } if (tupletStaveNotes.length > 1) { const tuplet: Tuplet = tupletBuilder[0]; const notesOccupied: number = tuplet.Notes[0][0].NormalNotes; const bracketed: boolean = tuplet.Bracket || (tuplet.TupletLabelNumber === 3 && this.rules.TripletsBracketed) || (tuplet.TupletLabelNumber !== 3 && this.rules.TupletsBracketed); vftuplets.push(new Vex.Flow.Tuplet( tupletStaveNotes, { bracketed: bracketed, notes_occupied: notesOccupied, num_notes: tuplet.TupletLabelNumber, //, location: -1, ratioed: true ratioed: this.rules.TupletsRatioed, })); } else { log.debug("Warning! Tuplet with no notes! Trying to ignore, but this is a serious problem."); } } } } } public layoutStaffEntry(graphicalStaffEntry: GraphicalStaffEntry): void { return; } public graphicalMeasureCreatedCalculations(): void { let graceSlur: boolean; let graceGVoiceEntriesBefore: GraphicalVoiceEntry[] = []; for (const graphicalStaffEntry of this.staffEntries as VexFlowStaffEntry[]) { graceSlur = false; graceGVoiceEntriesBefore = []; // create vex flow Stave Notes: for (const gve of graphicalStaffEntry.graphicalVoiceEntries) { if (gve.parentVoiceEntry.IsGrace) { // save grace notes for the next non-grace note graceGVoiceEntriesBefore.push(gve); if (!graceSlur) { graceSlur = gve.parentVoiceEntry.GraceSlur; } continue; } if (gve.notes[0].sourceNote.PrintObject) { (gve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.StaveNote(gve); } else { // note can now also be added as StaveNote instead of GhostNote, because we set it to transparent (gve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.StaveNote(gve); // previous method: add as GhostNote instead of StaveNote. Can cause formatting issues if critical notes are missing in the measure // don't render note. add ghost note, otherwise Vexflow can have issues with layouting when voices not complete. //(gve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.GhostNote(gve.notes[0].sourceNote.Length); //graceGVoiceEntriesBefore = []; // if note is not rendered, its grace notes shouldn't be rendered, might need to be removed //continue; } if (graceGVoiceEntriesBefore.length > 0) { // add grace notes that came before this main note to a GraceNoteGroup in Vexflow, attached to the main note const graceNotes: Vex.Flow.GraceNote[] = []; for (let i: number = 0; i < graceGVoiceEntriesBefore.length; i++) { const gveGrace: VexFlowVoiceEntry = graceGVoiceEntriesBefore[i]; //if (gveGrace.notes[0].sourceNote.PrintObject) { // grace notes should generally be rendered independently of main note instead of skipped if main note is invisible // could be an option to make grace notes transparent if main note is transparent. set grace notes' PrintObject to false then. const vfStaveNote: StaveNote = VexFlowConverter.StaveNote(gveGrace); gveGrace.vfStaveNote = vfStaveNote; graceNotes.push(vfStaveNote); } const graceNoteGroup: Vex.Flow.GraceNoteGroup = new Vex.Flow.GraceNoteGroup(graceNotes, graceSlur); ((gve as VexFlowVoiceEntry).vfStaveNote as StaveNote).addModifier(0, graceNoteGroup); graceGVoiceEntriesBefore = []; } } } // remaining grace notes at end of measure, turned into stand-alone grace notes: if (graceGVoiceEntriesBefore.length > 0) { for (const graceGve of graceGVoiceEntriesBefore) { (graceGve as VexFlowVoiceEntry).vfStaveNote = VexFlowConverter.StaveNote(graceGve); graceGve.parentVoiceEntry.GraceAfterMainNote = true; } } // const t0: number = performance.now(); this.finalizeBeams(); // const t1: number = performance.now(); // console.log("Call to finalizeBeams in VexFlowMeasure took " + (t1 - t0) + " milliseconds."); this.finalizeTuplets(); const voices: Voice[] = this.getVoicesWithinMeasure(); for (const voice of voices) { if (voice === undefined) { continue; } const isMainVoice: boolean = !(voice instanceof LinkedVoice); // add a vexFlow voice for this voice: this.vfVoices[voice.VoiceId] = new Vex.Flow.Voice({ beat_value: this.parentSourceMeasure.Duration.Denominator, num_beats: this.parentSourceMeasure.Duration.Numerator, resolution: Vex.Flow.RESOLUTION, }).setMode(Vex.Flow.Voice.Mode.SOFT); const restFilledEntries: GraphicalVoiceEntry[] = this.getRestFilledVexFlowStaveNotesPerVoice(voice); // create vex flow voices and add tickables to it: for (const voiceEntry of restFilledEntries) { if (voiceEntry.parentVoiceEntry) { if (voiceEntry.parentVoiceEntry.IsGrace && !voiceEntry.parentVoiceEntry.GraceAfterMainNote) { continue; } } const vexFlowVoiceEntry: VexFlowVoiceEntry = voiceEntry as VexFlowVoiceEntry; if (voiceEntry.notes.length === 0 || !voiceEntry.notes[0] || !voiceEntry.notes[0].sourceNote.PrintObject) { // GhostNote, don't add modifiers like in-measure clefs this.vfVoices[voice.VoiceId].addTickable(vexFlowVoiceEntry.vfStaveNote); continue; } // check for in-measure clefs: // only add clefs in main voice (to not add them twice) if (isMainVoice) { const vfse: VexFlowStaffEntry = vexFlowVoiceEntry.parentStaffEntry as VexFlowStaffEntry; if (vfse && vfse.vfClefBefore !== undefined) { // add clef as NoteSubGroup so that we get modifier layouting const clefModifier: NoteSubGroup = new NoteSubGroup( [vfse.vfClefBefore] ); // The cast is necesary because...vexflow -> see types if (vexFlowVoiceEntry.vfStaveNote.getCategory && vexFlowVoiceEntry.vfStaveNote.getCategory() === "stavenotes") { // GhostNotes and other StemmableNotes don't have this function (vexFlowVoiceEntry.vfStaveNote as Vex.Flow.StaveNote).addModifier(0, clefModifier); } } } // add fingering if (voiceEntry.parentVoiceEntry && this.rules.RenderFingerings) { this.createFingerings(voiceEntry); } // add Arpeggio if (voiceEntry.parentVoiceEntry && voiceEntry.parentVoiceEntry.Arpeggio !== undefined) { const arpeggio: Arpeggio = voiceEntry.parentVoiceEntry.Arpeggio; // TODO right now our arpeggio object has all arpeggio notes from arpeggios across all voices. // see VoiceGenerator. Doesn't matter for Vexflow for now though if (voiceEntry.notes && voiceEntry.notes.length > 1) { const type: Vex.Flow.Stroke.Type = VexFlowConverter.StrokeTypeFromArpeggioType(arpeggio.type); const stroke: Vex.Flow.Stroke = new Vex.Flow.Stroke(type, { all_voices: this.rules.ArpeggiosGoAcrossVoices // default: false. This causes arpeggios to always go across all voices, which is often unwanted. // also, this can cause infinite height of stroke, see #546 }); //if (arpeggio.notes.length === vexFlowVoiceEntry.notes.length) { // different workaround for endless y bug if (this.rules.RenderArpeggios) { vexFlowVoiceEntry.vfStaveNote.addStroke(0, stroke); } } else { log.debug(`[OSMD] arpeggio in measure ${this.MeasureNumber} could not be drawn. voice entry had less than two notes, arpeggio is likely between voice entries, not currently supported in Vexflow.`); // TODO: create new arpeggio with all the arpeggio's notes (arpeggio.notes), perhaps with GhostNotes in a new vfStaveNote. not easy. } } this.vfVoices[voice.VoiceId].addTickable(vexFlowVoiceEntry.vfStaveNote); } } this.createArticulations(); this.createOrnaments(); this.setStemDirectionFromVexFlow(); } /** * Copy the stem directions chosen by VexFlow to the StemDirection variable of the graphical notes */ private setStemDirectionFromVexFlow(): void { //if StemDirection was not set then read out what VexFlow has chosen for ( const vfStaffEntry of this.staffEntries ) { for ( const gVoiceEntry of vfStaffEntry.graphicalVoiceEntries) { for ( const gnote of gVoiceEntry.notes) { const vfnote: [StaveNote, number] = (gnote as VexFlowGraphicalNote).vfnote; if (vfnote === undefined || vfnote[0] === undefined) { continue; } const vfStemDir: number = vfnote[0].getStemDirection(); switch (vfStemDir) { case (Vex.Flow.Stem.UP): gVoiceEntry.parentVoiceEntry.StemDirection = StemDirectionType.Up; break; case (Vex.Flow.Stem.DOWN): gVoiceEntry.parentVoiceEntry.StemDirection = StemDirectionType.Down; break; default: } } } } } /** * Create the articulations for all notes of the current staff entry */ protected createArticulations(): void { for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) { const graphicalStaffEntry: VexFlowStaffEntry = (this.staffEntries[idx] as VexFlowStaffEntry); // create vex flow articulation: const graphicalVoiceEntries: GraphicalVoiceEntry[] = graphicalStaffEntry.graphicalVoiceEntries; for (const gve of graphicalVoiceEntries) { const vfStaveNote: StemmableNote = (gve as VexFlowVoiceEntry).vfStaveNote; VexFlowConverter.generateArticulations(vfStaveNote, gve.notes[0].sourceNote.ParentVoiceEntry.Articulations); } } } /** * Create the ornaments for all notes of the current staff entry */ protected createOrnaments(): void { for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) { const graphicalStaffEntry: VexFlowStaffEntry = (this.staffEntries[idx] as VexFlowStaffEntry); const gvoices: { [voiceID: number]: GraphicalVoiceEntry; } = graphicalStaffEntry.graphicalVoiceEntries; for (const voiceID in gvoices) { if (gvoices.hasOwnProperty(voiceID)) { const vfStaveNote: StemmableNote = (gvoices[voiceID] as VexFlowVoiceEntry).vfStaveNote; const ornamentContainer: OrnamentContainer = gvoices[voiceID].notes[0].sourceNote.ParentVoiceEntry.OrnamentContainer; if (ornamentContainer !== undefined) { VexFlowConverter.generateOrnaments(vfStaveNote, ornamentContainer); } } } } } protected createFingerings(voiceEntry: GraphicalVoiceEntry): void { const vexFlowVoiceEntry: VexFlowVoiceEntry = voiceEntry as VexFlowVoiceEntry; const technicalInstructions: TechnicalInstruction[] = voiceEntry.parentVoiceEntry.TechnicalInstructions; let fingeringsCount: number = 0; for (const instruction of technicalInstructions) { if (instruction.type === TechnicalInstructionType.Fingering) { fingeringsCount++; } } let fingeringIndex: number = -1; for (const fingeringInstruction of technicalInstructions) { if (fingeringInstruction.type !== TechnicalInstructionType.Fingering) { continue; } fingeringIndex++; // 0 for first fingering let fingeringPosition: PlacementEnum = this.rules.FingeringPosition; if (fingeringInstruction.placement !== PlacementEnum.NotYetDefined) { fingeringPosition = fingeringInstruction.placement; } let modifierPosition: any; // Vex.Flow.Stavemodifier.Position switch (fingeringPosition) { default: case PlacementEnum.Left: modifierPosition = Vex.Flow.StaveModifier.Position.LEFT; break; case PlacementEnum.Right: modifierPosition = Vex.Flow.StaveModifier.Position.RIGHT; break; case PlacementEnum.Above: modifierPosition = Vex.Flow.StaveModifier.Position.ABOVE; break; case PlacementEnum.Below: modifierPosition = Vex.Flow.StaveModifier.Position.BELOW; break; case PlacementEnum.NotYetDefined: // automatic fingering placement, could be more complex/customizable const sourceStaff: Staff = voiceEntry.parentStaffEntry.sourceStaffEntry.ParentStaff; if (voiceEntry.notes.length > 1 || voiceEntry.parentStaffEntry.graphicalVoiceEntries.length > 1) { modifierPosition = Vex.Flow.StaveModifier.Position.LEFT; } else if (sourceStaff.idInMusicSheet === 0) { modifierPosition = Vex.Flow.StaveModifier.Position.ABOVE; fingeringPosition = PlacementEnum.Above; } else { modifierPosition = Vex.Flow.StaveModifier.Position.BELOW; fingeringPosition = PlacementEnum.Below; } } const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(fingeringInstruction.value); fretFinger.setPosition(modifierPosition); if (fingeringPosition === PlacementEnum.Above || fingeringPosition === PlacementEnum.Below) { const offsetYSign: number = fingeringPosition === PlacementEnum.Above ? -1 : 1; // minus y is up const ordering: number = fingeringPosition === PlacementEnum.Above ? fingeringIndex : fingeringsCount - 1 - fingeringIndex; // reverse order for fingerings below staff if (this.rules.FingeringInsideStafflines && fingeringsCount > 1) { // y-shift for single fingering is ok // experimental, bounding boxes wrong for fretFinger above/below, better would be creating Labels // set y-shift. vexflow fretfinger simply places directly above/below note const perFingeringShift: number = fretFinger.getWidth() / 2; const shiftCount: number = fingeringsCount * 2.5; (fretFinger).setOffsetY(offsetYSign * (ordering + shiftCount) * perFingeringShift); } else if (!this.rules.FingeringInsideStafflines) { // use StringNumber for placement above/below stafflines const stringNumber: Vex.Flow.StringNumber = new Vex.Flow.StringNumber(fingeringInstruction.value); (stringNumber).radius = 0; // hack to remove the circle around the number stringNumber.setPosition(modifierPosition); stringNumber.setOffsetY(offsetYSign * ordering * stringNumber.getWidth() * 2 / 3); // Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :( vexFlowVoiceEntry.vfStaveNote.addModifier((fingeringIndex as any), (stringNumber as any)); continue; } } if (vexFlowVoiceEntry.vfStaveNote.getCategory() === "tabnotes") { // TODO this doesn't work yet. don't add fingering for tabs for now. // vexFlowVoiceEntry.vfStaveNote.addModifier(fretFinger, fingeringIndex); } else { // Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :( vexFlowVoiceEntry.vfStaveNote.addModifier((fingeringIndex as any), (fretFinger as any)); } } } /** * Creates a line from 'top' to this measure, of type 'lineType' * @param top * @param lineType */ public lineTo(top: VexFlowMeasure, lineType: any): void { const connector: StaveConnector = new Vex.Flow.StaveConnector(top.getVFStave(), this.stave); connector.setType(lineType); this.connectors.push(connector); } /** * Return the VexFlow Stave corresponding to this graphicalMeasure * @returns {Vex.Flow.Stave} */ public getVFStave(): Vex.Flow.Stave { return this.stave; } /** * After re-running the formatting on the VexFlow Stave, update the * space needed by Instructions (in VexFlow: StaveModifiers) */ protected updateInstructionWidth(): void { let vfBeginInstructionsWidth: number = 0; let vfEndInstructionsWidth: number = 0; const modifiers: Vex.Flow.StaveModifier[] = this.stave.getModifiers(); for (const mod of modifiers) { if (mod.getPosition() === StavePositionEnum.BEGIN) { //Vex.Flow.StaveModifier.Position.BEGIN) { vfBeginInstructionsWidth += mod.getWidth() + mod.getPadding(undefined); } else if (mod.getPosition() === StavePositionEnum.END) { //Vex.Flow.StaveModifier.Position.END) { vfEndInstructionsWidth += mod.getWidth() + mod.getPadding(undefined); } } this.beginInstructionsWidth = vfBeginInstructionsWidth / unitInPixels; this.endInstructionsWidth = vfEndInstructionsWidth / unitInPixels; } } // Gives the position of the Stave - replaces the function get Position() in the description of class StaveModifier in vexflow.d.ts // The latter gave an error because function cannot be defined in the class descriptions in vexflow.d.ts export enum StavePositionEnum { LEFT = 1, RIGHT = 2, ABOVE = 3, BELOW = 4, BEGIN = 5, END = 6 }