UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

214 lines (213 loc) 9.48 kB
"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;