@stringsync/vexml
Version:
MusicXML to Vexflow
214 lines (213 loc) • 9.48 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Voice = void 0;
const vexflow = __importStar(require("vexflow"));
const util = __importStar(require("../util"));
const spatial_1 = require("../spatial");
const note_1 = require("./note");
const rest_1 = require("./rest");
const util_1 = require("../util");
const beam_1 = require("./beam");
const tuplet_1 = require("./tuplet");
const dynamics_1 = require("./dynamics");
const DURATION_TYPE_VALUES = [
{ type: '1/2', value: new util_1.Fraction(2, 1) },
{ type: '1', value: new util_1.Fraction(1, 1) },
{ type: '2', value: new util_1.Fraction(1, 2) },
{ type: '4', value: new util_1.Fraction(1, 4) },
{ type: '8', value: new util_1.Fraction(1, 8) },
{ type: '16', value: new util_1.Fraction(1, 16) },
{ type: '32', value: new util_1.Fraction(1, 32) },
{ type: '64', value: new util_1.Fraction(1, 64) },
{ type: '128', value: new util_1.Fraction(1, 128) },
{ type: '256', value: new util_1.Fraction(1, 256) },
{ type: '512', value: new util_1.Fraction(1, 512) },
{ type: '1024', value: new util_1.Fraction(1, 1024) },
];
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: spatial_1.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 = util_1.Fraction.fromFractionLike(entry.measureBeat);
const duration = util_1.Fraction.fromFractionLike(entry.duration);
if (currentMeasureBeat.isLessThan(measureBeat)) {
const beats = measureBeat.subtract(currentMeasureBeat).divide(new util_1.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_1.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_1.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_1.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 = util_1.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) => util_1.Fraction.fromFractionLike(e.measureBeat).add(util_1.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_1.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_1.Tuplet(this.config, this.log, this.document, tupletKey, registry).render();
tupletRenders.push(tupletRender);
}
}
return tupletRenders;
}
}
exports.Voice = Voice;