@stringsync/vexml
Version:
MusicXML to Vexflow
212 lines (211 loc) • 9.63 kB
JavaScript
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,
};
}
}