UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

387 lines (386 loc) 16.7 kB
import * as vexflow from 'vexflow'; import * as util from '../util'; import { Rect } from '../spatial'; import { Beam } from './beam'; import { Articulation } from './articulation'; import { Bend } from './bend'; const GRACE_TAB_NOTE_FONT_SIZE = 7; export class Note { config; log; document; key; constructor(config, log, document, key) { this.config = config; this.log = log; this.document = document; this.key = key; } render() { const voiceEntry = this.document.getVoiceEntry(this.key); util.assert(voiceEntry.type === 'note' || voiceEntry.type === 'chord', 'expected note or chord'); const { autoStem, stemDirection } = this.getVexflowStemParams(voiceEntry); const curveIds = this.getCurveIds(voiceEntry); const isTabStave = this.document.isTabStave(this.key); let vexflowNote; if (isTabStave) { vexflowNote = new vexflow.TabNote({ positions: this.getVexflowTabNotePositions(voiceEntry), duration: voiceEntry.durationType, dots: voiceEntry.dotCount, }); vexflowNote.renderOptions.drawStem = this.config.SHOW_TAB_STEMS; } else { vexflowNote = new vexflow.StaveNote({ keys: this.getVexflowStaveNoteKeys(voiceEntry), duration: voiceEntry.durationType, dots: voiceEntry.dotCount, autoStem, stemDirection, clef: this.document.getStave(this.key).signature.clef.sign, }); } if (!isTabStave) { for (let index = 0; index < voiceEntry.dotCount; index++) { vexflow.Dot.buildAndAttach([vexflowNote], { all: true }); } } if (!isTabStave) { this.renderVexflowAccidentals(vexflowNote, voiceEntry); } this.renderVexflowAnnotations(vexflowNote, voiceEntry); let vexflowGraceNoteGroup = null; let graceBeamRenders = new Array(); let graceCurves = new Array(); if (isTabStave) { const tabGraceEntries = this.renderTabGraceComponents(vexflowNote, voiceEntry); vexflowGraceNoteGroup = tabGraceEntries.vexflowGraceNoteGroup; graceBeamRenders = tabGraceEntries.graceBeamRenders; graceCurves = tabGraceEntries.graceCurves; } else { const staveGraceEntries = this.renderStaveGraceComponents(vexflowNote, voiceEntry); vexflowGraceNoteGroup = staveGraceEntries.vexflowGraceNoteGroup; graceBeamRenders = staveGraceEntries.graceBeamRenders; graceCurves = staveGraceEntries.graceCurves; } const articulationRenders = this.renderArticulations(vexflowNote); const bendRenders = this.renderBends(vexflowNote); return { type: 'note', key: this.key, rect: Rect.empty(), // placeholder stemDirection: voiceEntry.stemDirection, vexflowNote: vexflowNote, curveIds, graceCurves, beamId: voiceEntry.beamId, wedgeId: voiceEntry.wedgeId, tupletIds: voiceEntry.tupletIds, vexflowGraceNoteGroup, graceBeamRenders, pedalMark: voiceEntry.pedalMark, octaveShiftId: voiceEntry.octaveShiftId, vibratoIds: voiceEntry.vibratoIds, articulationRenders, bendRenders, }; } getCurveIds(voiceEntry) { switch (voiceEntry.type) { case 'note': return voiceEntry.curveIds; case 'chord': return voiceEntry.notes.flatMap((note) => note.curveIds); } } getVexflowStemParams(voiceEntry) { let autoStem; let stemDirection; switch (voiceEntry.stemDirection) { case 'up': stemDirection = vexflow.Stem.UP; break; case 'down': stemDirection = vexflow.Stem.DOWN; break; case 'none': break; default: autoStem = true; } return { autoStem, stemDirection }; } getOctaveShift() { let result = 0; // Octave shift from clef. result += this.document.getStave(this.key).signature.clef.octaveShift ?? 0; // Octave shift from spanner. const voiceEntry = this.document.getVoiceEntry(this.key); if (voiceEntry.type === 'note' && voiceEntry.octaveShiftId) { const key = this.document.getOctaveShiftKey(voiceEntry.octaveShiftId); const octaveShift = this.document.getOctaveShift(key); const size = Math.abs(octaveShift.size) - 1; const octaveCount = Math.floor(size / 7); if (octaveShift.size < 0) { result += octaveCount; } else { result -= octaveCount; } } return result; } getVexflowTabNotePositions(voiceEntry) { switch (voiceEntry.type) { case 'note': return voiceEntry.tabPositions.map((t) => this.toVexflowTabNotePosition(t)); case 'chord': return voiceEntry.notes.flatMap((note) => note.tabPositions).map((t) => this.toVexflowTabNotePosition(t)); } } toVexflowTabNotePosition(tabPosition) { const fret = tabPosition.harmonic ? `<${tabPosition.fret}>` : tabPosition.fret; return { str: tabPosition.string, fret }; } getVexflowStaveNoteKeys(voiceEntry) { const octaveShift = this.getOctaveShift(); switch (voiceEntry.type) { case 'note': return [this.getVexflowNoteKey(voiceEntry, octaveShift)]; case 'chord': return voiceEntry.notes.map((note) => this.getVexflowNoteKey(note, octaveShift)); } } getVexflowNoteKey(note, octaveShift) { const step = note.pitch.step; const octave = note.pitch.octave - octaveShift; return note.head ? `${step}/${octave}/${note.head}` : `${step}/${octave}`; } renderVexflowAnnotations(vexflowNote, voiceEntry) { const vexflowAnnotations = new Array(); for (const annotation of voiceEntry.annotations) { const vexflowAnnotation = new vexflow.Annotation(annotation.text); vexflowAnnotations.push(vexflowAnnotation); if (annotation.horizontalJustification) { vexflowAnnotation.setJustification(annotation.horizontalJustification); } if (annotation.verticalJustification) { vexflowAnnotation.setVerticalJustification(annotation.verticalJustification); } vexflowNote.addModifier(vexflowAnnotation); } return vexflowAnnotations; } /** * Returns the vexflow.Accidental objects preserving the note index. */ renderVexflowAccidentals(vexflowNote, voiceEntry) { const vexflowAccidentals = new Array(); switch (voiceEntry.type) { case 'note': vexflowAccidentals.push(this.renderVexflowAccidental(voiceEntry.accidental)); break; case 'chord': vexflowAccidentals.push(...voiceEntry.notes.map((note) => this.renderVexflowAccidental(note.accidental))); break; } for (let index = 0; index < vexflowAccidentals.length; index++) { const vexflowAccidental = vexflowAccidentals[index]; if (vexflowAccidental) { vexflowNote.addModifier(vexflowAccidental, index); } } return vexflowAccidentals; } renderVexflowAccidental(accidental) { if (!accidental) { return null; } const vexflowAccidental = new vexflow.Accidental(accidental.code); if (accidental.isCautionary) { vexflowAccidental.setAsCautionary(); } return vexflowAccidental; } renderTabGraceComponents(vexflowNote, voiceEntry) { if (voiceEntry.graceEntries.length === 0) { return { vexflowGraceNoteGroup: null, graceBeamRenders: [], graceCurves: [] }; } const registry = new Map(); const graceCurves = new Array(); const vexflowGraceTabNotes = new Array(); for (let graceEntryIndex = 0; graceEntryIndex < voiceEntry.graceEntries.length; graceEntryIndex++) { const graceEntry = voiceEntry.graceEntries[graceEntryIndex]; const positions = new Array(); switch (graceEntry.type) { case 'gracenote': positions.push(...graceEntry.tabPositions.map((tabPosition) => this.toVexflowTabNotePosition(tabPosition))); break; case 'gracechord': positions.push(...graceEntry.notes .flatMap((note) => note.tabPositions) .map((tabPosition) => this.toVexflowTabNotePosition(tabPosition))); break; } const vexflowGraceTabNote = new HackedVexflowGraceTabNote({ positions: positions, duration: graceEntry.durationType, }).setFontSize(GRACE_TAB_NOTE_FONT_SIZE); vexflowGraceTabNote.renderOptions.yShift--; vexflowGraceTabNotes.push(vexflowGraceTabNote); if (graceEntry.beamId) { if (!registry.has(graceEntry.beamId)) { registry.set(graceEntry.beamId, []); } registry.get(graceEntry.beamId).push(vexflowGraceTabNote); } const curveIds = new Array(); switch (graceEntry.type) { case 'gracenote': curveIds.push(...graceEntry.curveIds); break; case 'gracechord': curveIds.push(...graceEntry.notes.flatMap((note) => note.curveIds)); break; } for (const curveId of curveIds) { graceCurves.push({ curveId, graceEntryIndex }); } } const vexflowGraceNoteGroup = new vexflow.GraceNoteGroup(vexflowGraceTabNotes); vexflowGraceNoteGroup.setNote(vexflowNote); vexflowGraceNoteGroup.setPosition(vexflow.Modifier.Position.LEFT); vexflowNote.addModifier(vexflowGraceNoteGroup); // Grace notes cannot span voice entries, so we perform all the beaming here. const beams = this.document.getBeams(this.key); const beamKeys = Array.from(registry.keys()).map((beamId) => ({ ...this.key, beamIndex: beams.findIndex((beam) => beam.id === beamId), })); const graceBeamRenders = beamKeys.map((beamKey) => new Beam(this.config, this.log, this.document, beamKey, registry).render()); return { vexflowGraceNoteGroup, graceBeamRenders, graceCurves }; } renderStaveGraceComponents(vexflowNote, voiceEntry) { if (voiceEntry.graceEntries.length === 0) { return { vexflowGraceNoteGroup: null, graceBeamRenders: [], graceCurves: [] }; } const registry = new Map(); const octaveShift = this.document.getStave(this.key).signature.clef.octaveShift ?? 0; const graceCurves = new Array(); const vexflowGraceNotes = new Array(); for (let graceEntryIndex = 0; graceEntryIndex < voiceEntry.graceEntries.length; graceEntryIndex++) { const graceEntry = voiceEntry.graceEntries[graceEntryIndex]; const keys = new Array(); switch (graceEntry.type) { case 'gracenote': keys.push(this.getVexflowNoteKey(graceEntry, octaveShift)); break; case 'gracechord': keys.push(...graceEntry.notes.map((note) => this.getVexflowNoteKey(note, octaveShift))); break; } let slash = false; switch (graceEntry.type) { case 'gracenote': slash = graceEntry.slash; break; case 'gracechord': slash = graceEntry.notes.some((note) => note.slash); break; } const vexflowGraceNote = new vexflow.GraceNote({ keys: keys, duration: graceEntry.durationType, slash, }); const vexflowAccidentals = new Array(); switch (graceEntry.type) { case 'gracenote': vexflowAccidentals.push(this.renderVexflowAccidental(graceEntry.accidental)); break; case 'gracechord': vexflowAccidentals.push(...graceEntry.notes.map((note) => this.renderVexflowAccidental(note.accidental))); break; } for (let index = 0; index < vexflowAccidentals.length; index++) { const vexflowAccidental = vexflowAccidentals[index]; if (vexflowAccidental) { vexflowGraceNote.addModifier(vexflowAccidental, index); } } vexflowGraceNotes.push(vexflowGraceNote); if (graceEntry.beamId) { if (!registry.has(graceEntry.beamId)) { registry.set(graceEntry.beamId, []); } registry.get(graceEntry.beamId).push(vexflowGraceNote); } const curveIds = new Array(); switch (graceEntry.type) { case 'gracenote': curveIds.push(...graceEntry.curveIds); break; case 'gracechord': curveIds.push(...graceEntry.notes.flatMap((note) => note.curveIds)); break; } for (const curveId of curveIds) { graceCurves.push({ curveId, graceEntryIndex }); } } const vexflowGraceNoteGroup = new vexflow.GraceNoteGroup(vexflowGraceNotes); vexflowGraceNoteGroup.setNote(vexflowNote); vexflowGraceNoteGroup.setPosition(vexflow.Modifier.Position.LEFT); vexflowNote.addModifier(vexflowGraceNoteGroup); // Grace notes cannot span voice entries, so we perform all the beaming here. const beams = this.document.getBeams(this.key); const beamKeys = Array.from(registry.keys()).map((beamId) => ({ ...this.key, beamIndex: beams.findIndex((beam) => beam.id === beamId), })); const graceBeamRenders = beamKeys.map((beamKey) => new Beam(this.config, this.log, this.document, beamKey, registry).render()); return { vexflowGraceNoteGroup, graceBeamRenders, graceCurves }; } renderArticulations(vexflowNote) { const articulationRenders = new Array(); const articulations = this.document.getArticulations(this.key); for (let articulationIndex = 0; articulationIndex < articulations.length; articulationIndex++) { const key = { ...this.key, articulationIndex }; const articulationRender = new Articulation(this.config, this.log, this.document, key).render(); articulationRenders.push(articulationRender); } for (const articulationRender of articulationRenders) { for (const vexflowModifier of articulationRender.vexflowModifiers) { vexflowNote.addModifier(vexflowModifier); } } return articulationRenders; } renderBends(vexflowNote) { const bendRenders = new Array(); const voiceEntry = this.document.getVoiceEntry(this.key); const isBendable = voiceEntry.type === 'note' || voiceEntry.type === 'chord'; const hasBends = isBendable && voiceEntry.bends.length > 0; if (hasBends) { const bendRender = new Bend(this.config, this.log, this.document, this.key).render(); bendRenders.push(bendRender); } for (const bendRender of bendRenders) { for (const vexflowModifier of bendRender.vexflowModifiers) { vexflowNote.addModifier(vexflowModifier); } } return bendRenders; } } // See https://github.com/vexflow/vexflow/issues/255 class HackedVexflowGraceTabNote extends vexflow.GraceTabNote { setFontSize(size) { for (const fretElement of this.fretElement) { fretElement.setFontSize(size); } return this; } }