@stringsync/vexml
Version:
MusicXML to Vexflow
177 lines (176 loc) • 7.78 kB
JavaScript
import * as vexflow from 'vexflow';
import * as util from '../util';
import { Rect } from '../spatial';
import { Note } from './note';
import { Rest } from './rest';
import { Fraction } from '../util';
import { Beam } from './beam';
import { Tuplet } from './tuplet';
import { Dynamics } from './dynamics';
const DURATION_TYPE_VALUES = [
{ type: '1/2', value: new Fraction(2, 1) },
{ type: '1', value: new Fraction(1, 1) },
{ type: '2', value: new Fraction(1, 2) },
{ type: '4', value: new Fraction(1, 4) },
{ type: '8', value: new Fraction(1, 8) },
{ type: '16', value: new Fraction(1, 16) },
{ type: '32', value: new Fraction(1, 32) },
{ type: '64', value: new Fraction(1, 64) },
{ type: '128', value: new Fraction(1, 128) },
{ type: '256', value: new Fraction(1, 256) },
{ type: '512', value: new Fraction(1, 512) },
{ type: '1024', value: new Fraction(1, 1024) },
];
export class Voice {
config;
log;
document;
key;
constructor(config, log, document, key) {
this.config = config;
this.log = log;
this.document = document;
this.key = key;
}
render() {
const startMeasureBeat = this.getStartMeasureBeat();
const { vexflowVoices, entryRenders } = this.renderVoices(startMeasureBeat);
const beamRenders = this.renderBeams(entryRenders);
const tupletRenders = this.renderTuplets(entryRenders);
return {
type: 'voice',
key: this.key,
rect: Rect.empty(), // placeholder
startMeasureBeat,
entryRenders,
beamRenders,
tupletRenders,
vexflowVoices,
};
}
renderVoices(startMeasureBeat) {
const vexflowVoices = [new vexflow.Voice().setMode(vexflow.Voice.Mode.SOFT)];
const entryRenders = new Array();
const entryCount = this.document.getVoiceEntryCount(this.key);
let currentMeasureBeat = startMeasureBeat;
for (let voiceEntryIndex = 0; voiceEntryIndex < entryCount; voiceEntryIndex++) {
const vexflowVoice = vexflowVoices[0];
const voiceEntryKey = { ...this.key, voiceEntryIndex };
const entry = this.document.getVoiceEntry(voiceEntryKey);
const measureBeat = Fraction.fromFractionLike(entry.measureBeat);
const duration = Fraction.fromFractionLike(entry.duration);
if (currentMeasureBeat.isLessThan(measureBeat)) {
const beats = measureBeat.subtract(currentMeasureBeat).divide(new Fraction(4));
const vexflowGhostNote = this.renderVexflowGhostNote(beats);
vexflowVoice.addTickable(vexflowGhostNote);
// NOTE: We don't need to add this is entryRenders because it's a vexflow-specific detail for formatting and
// vexml doesn't need to do anything with it.
}
currentMeasureBeat = measureBeat.add(duration);
if (entry.type === 'note' || entry.type === 'chord') {
const noteRender = new Note(this.config, this.log, this.document, voiceEntryKey).render();
vexflowVoice.addTickable(noteRender.vexflowNote);
entryRenders.push(noteRender);
}
else if (entry.type === 'rest') {
const restRender = new Rest(this.config, this.log, this.document, voiceEntryKey).render();
vexflowVoice.addTickable(restRender.vexflowNote);
entryRenders.push(restRender);
}
else if (entry.type === 'dynamics') {
const dynamicsRender = new Dynamics(this.config, this.log, this.document, voiceEntryKey).render();
vexflowVoice.addTickable(dynamicsRender.vexflowNote);
entryRenders.push(dynamicsRender);
}
else {
util.assertUnreachable();
}
}
return { vexflowVoices, entryRenders };
}
getStartMeasureBeat() {
let measureBeat = Fraction.zero();
this.document
.getMeasure(this.key)
.fragments.filter((_, fragmentIndex) => fragmentIndex < this.key.fragmentIndex)
.flatMap((f) => f.parts)
.flatMap((p) => p.staves)
.flatMap((s) => s.voices)
.flatMap((v) => v.entries)
.map((e) => Fraction.fromFractionLike(e.measureBeat).add(Fraction.fromFractionLike(e.duration)))
.forEach((m) => {
if (m.isGreaterThan(measureBeat)) {
measureBeat = m;
}
});
return measureBeat;
}
renderVexflowGhostNote(beatDuration) {
let closestDurationType = '1/2';
for (const { type, value } of DURATION_TYPE_VALUES) {
if (value.isLessThanOrEqualTo(beatDuration)) {
closestDurationType = type;
break;
}
}
return new vexflow.GhostNote({ duration: closestDurationType });
}
renderBeams(entryRenders) {
const registry = new Map();
const beams = this.document.getBeams(this.key);
// This inherently ignores grace beams because we don't include grace beams in the entry render object.
for (const entryRender of entryRenders) {
if (entryRender.type !== 'note') {
continue;
}
if (!entryRender.beamId) {
continue;
}
if (!registry.has(entryRender.beamId)) {
registry.set(entryRender.beamId, []);
}
const vexflowNote = entryRender.vexflowNote;
if (vexflowNote instanceof vexflow.StaveNote ||
// Rendering beams for tab notes will create stems for notes. Therefore, we only render beams for tab notes if
// the config specifies to show stems for tab staves.
(this.config.SHOW_TAB_STEMS && vexflowNote instanceof vexflow.TabNote)) {
registry.get(entryRender.beamId).push(vexflowNote);
}
}
const beamRenders = new Array();
for (let beamIndex = 0; beamIndex < beams.length; beamIndex++) {
const beamKey = { ...this.key, beamIndex };
const beam = this.document.getBeam(beamKey);
const entryRenderCount = registry.get(beam.id)?.length ?? 0;
if (entryRenderCount > 1) {
const beamRender = new Beam(this.config, this.log, this.document, beamKey, registry).render();
beamRenders.push(beamRender);
}
}
return beamRenders;
}
renderTuplets(entryRenders) {
const registry = new Map();
const tuplets = this.document.getTuplets(this.key);
const tupletableRenders = entryRenders.filter((e) => e.type === 'note' || e.type === 'rest');
for (const entryRender of tupletableRenders) {
for (const tupletId of entryRender.tupletIds) {
if (!registry.has(tupletId)) {
registry.set(tupletId, []);
}
registry.get(tupletId).push(entryRender);
}
}
const tupletRenders = new Array();
for (let tupletIndex = 0; tupletIndex < tuplets.length; tupletIndex++) {
const tupletKey = { ...this.key, tupletIndex };
const tuplet = this.document.getTuplet(tupletKey);
const entryRenderCount = registry.get(tuplet.id)?.length ?? 0;
if (entryRenderCount > 1) {
const tupletRender = new Tuplet(this.config, this.log, this.document, tupletKey, registry).render();
tupletRenders.push(tupletRender);
}
}
return tupletRenders;
}
}