UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

212 lines (211 loc) 9.63 kB
import * as conversions from './conversions'; import * as util from '../../util'; import { Accidental } from './accidental'; import { Fraction } from './fraction'; import { VoiceEntryContext } from './contexts'; import { Annotation } from './annotation'; import { Pitch } from './pitch'; import { Curve } from './curve'; import { Beam } from './beam'; import { Tuplet } from './tuplet'; import { Vibrato } from './vibrato'; import { Articulation } from './articulation'; import { Bend } from './bend'; import { TabPosition } from './tabposition'; export class Note { config; log; pitch; head; durationType; dotCount; stemDirection; duration; measureBeat; lyrics; accidental; curves; tuplets; beam; slash; graceEntries; vibratos; articulations; bends; tabPositions; constructor(config, log, pitch, head, durationType, dotCount, stemDirection, duration, measureBeat, lyrics, accidental, curves, tuplets, beam, slash, graceEntries, vibratos, articulations, bends, tabPositions) { this.config = config; this.log = log; this.pitch = pitch; this.head = head; this.durationType = durationType; this.dotCount = dotCount; this.stemDirection = stemDirection; this.duration = duration; this.measureBeat = measureBeat; this.lyrics = lyrics; this.accidental = accidental; this.curves = curves; this.tuplets = tuplets; this.beam = beam; this.slash = slash; this.graceEntries = graceEntries; this.vibratos = vibratos; this.articulations = articulations; this.bends = bends; this.tabPositions = tabPositions; } static create(config, log, measureBeat, duration, musicXML) { const pitch = new Pitch(config, log, musicXML.note.getStep(), musicXML.note.getOctave()); const head = conversions.fromNoteheadToNotehead(musicXML.note.getNotehead()); let durationType = conversions.fromNoteTypeToDurationType(musicXML.note.getType()); let dotCount = musicXML.note.getDotCount(); if (!durationType) { [durationType, dotCount] = conversions.fromFractionToDurationType(duration); } const stem = conversions.fromStemToStemDirection(musicXML.note.getStem()); const annotations = musicXML.note.getLyrics().map((lyric) => Annotation.fromLyric(config, log, { lyric })); const code = conversions.fromAccidentalTypeToAccidentalCode(musicXML.note.getAccidentalType()) ?? conversions.fromAlterToAccidentalCode(musicXML.note.getAlter()) ?? 'n'; const isCautionary = musicXML.note.hasAccidentalCautionary(); const accidental = new Accidental(config, log, code, isCautionary); const curves = musicXML.note.getNotations().flatMap((notation) => Curve.create(config, log, { notation })); const tuplets = musicXML.note .getNotations() .flatMap((notation) => notation.getTuplets()) .map((tuplet) => Tuplet.create(config, log, { tuplet })); const vibratos = musicXML.note .getNotations() .flatMap((notation) => notation.getOrnaments()) .flatMap((ornament) => ornament.getWavyLines()) .map((entry) => Vibrato.create(config, log, { wavyLine: entry.value })); // Since data.Note is a superset of data.GraceNote, we can use the same model. We terminate recursion by checking if // the note is a grace note. const graceEntries = new Array(); if (!musicXML.note.isGrace()) { for (const graceNote of musicXML.note.getGraceNotes()) { if (graceNote.isChordTail()) { continue; } const note = Note.create(config, log, measureBeat, util.Fraction.zero(), { note: graceNote }); if (graceNote.isChordHead()) { const tail = graceNote .getChordTail() .map((note) => Note.create(config, log, measureBeat, util.Fraction.zero(), { note })); graceEntries.push({ type: 'gracechord', head: note, tail }); } else { graceEntries.push({ type: 'gracenote', note }); } } } // MusicXML encodes each beam line as a separate <beam>. We only care about the presence of beams, so we only check // the first one. vexflow will eventually do the heavy lifting of inferring the note durations and beam structures. let beam = null; if (musicXML.note.getBeams().length > 0) { beam = Beam.create(config, log, { beam: musicXML.note.getBeams().at(0) }); } const articulations = Articulation.create(config, log, { note: musicXML.note }); const bends = musicXML.note .getNotations() .flatMap((n) => n.getTechnicals()) .flatMap((t) => t.getBends()) .map((bend) => Bend.create(config, log, { bend })); const slash = musicXML.note.hasGraceSlash(); const fretPositions = TabPosition.create(config, log, { note: musicXML.note }); return new Note(config, log, pitch, head, durationType, dotCount, stem, new Fraction(duration), new Fraction(measureBeat), annotations, accidental, curves, tuplets, beam, slash, graceEntries, vibratos, articulations, bends, fretPositions); } parse(voiceCtx) { const voiceEntryCtx = VoiceEntryContext.note(voiceCtx, this.pitch.getStep(), this.pitch.getOctave()); const tupletIds = util.unique([ ...this.tuplets.map((tuplet) => tuplet.parse(voiceEntryCtx)).filter((id) => id !== null), ...voiceEntryCtx.continueOpenTuplets(), ]); // Grace entries need to be parsed before the curves because a slur may start on a grace entry. const graceEntries = this.parseGraceEntries(voiceEntryCtx); const curveIds = this.curves.map((curve) => curve.parse(voiceEntryCtx)); return { type: 'note', pitch: this.pitch.parse(), head: this.head, dotCount: this.dotCount, stemDirection: this.stemDirection, durationType: this.durationType, duration: this.duration.parse(), measureBeat: this.measureBeat.parse(), accidental: this.maybeParseAccidental(voiceEntryCtx) ?? null, annotations: this.parseAnnotations(), curveIds, tupletIds, beamId: this.beam?.parse(voiceEntryCtx) ?? null, graceEntries, wedgeId: voiceEntryCtx.continueOpenWedge(), pedalMark: voiceEntryCtx.continueOpenPedal(), octaveShiftId: voiceEntryCtx.continueOpenOctaveShift(), vibratoIds: this.vibratos.map((vibrato) => vibrato.parse(voiceEntryCtx)), articulations: this.articulations.map((articulation) => articulation.parse()), bends: this.bends.map((bend) => bend.parse()), tabPositions: this.tabPositions.map((fretPosition) => fretPosition.parse()), }; } parseAnnotations() { return [...this.lyrics].map((annotation) => annotation.parse()); } maybeParseAccidental(voiceEntryCtx) { const isCautionary = this.accidental.isCautionary; const noteAccidental = this.accidental.code; const keyAccidental = voiceEntryCtx.getKeyAccidental(); const activeAccidental = voiceEntryCtx.getActiveAccidental(); if (!isCautionary && keyAccidental === noteAccidental) { return null; } if (!isCautionary && activeAccidental === noteAccidental) { return null; } return this.accidental.parse(voiceEntryCtx); } parseGraceEntries(voiceEntryCtx) { return this.graceEntries.map((graceEntry) => { switch (graceEntry.type) { case 'gracenote': return this.parseGraceNote(voiceEntryCtx, graceEntry); case 'gracechord': return this.parseGraceChord(voiceEntryCtx, graceEntry); default: util.assertUnreachable(); } }); } parseGraceNote(voiceEntryCtx, graceNote) { const note = graceNote.note; return { type: 'gracenote', head: note.head, accidental: note.maybeParseAccidental(voiceEntryCtx), beamId: note.beam?.parse(voiceEntryCtx) ?? null, durationType: note.durationType, curveIds: note.curves.map((curve) => curve.parse(voiceEntryCtx)), pitch: note.pitch.parse(), slash: note.slash, tabPositions: note.tabPositions.map((tabPosition) => tabPosition.parse()), }; } parseGraceChord(voiceEntryCtx, graceChord) { const notes = [graceChord.head, ...graceChord.tail].map((note) => ({ type: 'gracechordnote', pitch: note.pitch.parse(), head: note.head, accidental: note.maybeParseAccidental(voiceEntryCtx), curveIds: note.curves.map((curve) => curve.parse(voiceEntryCtx)), slash: note.slash, tabPositions: note.tabPositions.map((tabPosition) => tabPosition.parse()), })); return { type: 'gracechord', beamId: graceChord.head.beam?.parse(voiceEntryCtx) ?? null, durationType: graceChord.head.durationType, notes, }; } }