UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

164 lines (163 loc) 6.58 kB
import * as util from '../../util'; import { Fraction } from './fraction'; /** Represents a musical time signature. */ export class Time { config; log; partId; staveNumber; components; symbol; constructor(config, log, partId, staveNumber, components, symbol) { this.config = config; this.log = log; this.partId = partId; this.staveNumber = staveNumber; this.components = components; this.symbol = symbol; } static default(config, log, partId, staveNumber) { return Time.common(config, log, partId, staveNumber); } static create(config, log, partId, staveNumber, musicXML) { const time = musicXML.time; if (time.isHidden()) { return Time.hidden(config, log, partId, staveNumber); } // The symbol overrides any other time specifications. This is done to avoid incompatible symbol and time signature // specifications. const symbol = time.getSymbol(); switch (symbol) { case 'common': return Time.common(config, log, partId, staveNumber); case 'cut': return Time.cut(config, log, partId, staveNumber); case 'hidden': return Time.hidden(config, log, partId, staveNumber); } const beats = time.getBeats(); const beatTypes = time.getBeatTypes(); const times = new Array(); const len = Math.min(beats.length, beatTypes.length); for (let index = 0; index < len; index++) { const beatsPerMeasure = beats[index]; const beatValue = beatTypes[index]; const nextTime = Time.parse(config, log, partId, staveNumber, beatsPerMeasure, beatValue); times.push(nextTime); } if (times.length === 0) { return null; } if (symbol === 'single-number') { return Time.singleNumber(config, log, partId, staveNumber, Time.combine(config, log, partId, staveNumber, times)); } if (times.length === 1) { return times[0]; } return Time.combine(config, log, partId, staveNumber, times); } /** Creates a Time for each stave. */ static createMulti(config, log, partId, staveCount, musicXML) { const times = new Array(); for (let index = 0; index < staveCount; index++) { const time = Time.create(config, log, partId, index + 1, musicXML); times.push(time); } return times; } /** Returns a simple Time, composed of two numbers. */ static simple(config, log, partId, staveNumber, beatsPerMeasure, beatValue) { const components = [new util.Fraction(beatsPerMeasure, beatValue)]; return new Time(config, log, partId, staveNumber, components, null); } /** * Returns a Time composed of many components. * * The parameter type signature ensures that there are at least two Fractions present. */ static complex(config, log, partId, staveNumber, components) { return new Time(config, log, partId, staveNumber, components, null); } /** * Returns a Time that should be hidden. * * NOTE: It contains time signature components, but purely to simplify rendering downstream. It shouldn't be used for * calculations. */ static hidden(config, log, partId, staveNumber) { const components = [new util.Fraction(4, 4)]; return new Time(config, log, partId, staveNumber, components, 'hidden'); } /** Returns a Time in cut time. */ static cut(config, log, partId, staveNumber) { const components = [new util.Fraction(2, 2)]; return new Time(config, log, partId, staveNumber, components, 'cut'); } /** Returns a Time in common time. */ static common(config, log, partId, staveNumber) { const components = [new util.Fraction(4, 4)]; return new Time(config, log, partId, staveNumber, components, 'common'); } /** Combines multiple time signatures into a single one, ignoring any symbols. */ static combine(config, log, partId, staveNumber, times) { const components = times.flatMap((time) => time.components); return new Time(config, log, partId, staveNumber, components, null); } /** Creates a new time signature that should be displayed as a single number. */ static singleNumber(config, log, partId, staveNumber, time) { return new Time(config, log, partId, staveNumber, [time.toFraction()], 'single-number'); } static parse(config, log, partId, staveNumber, beatsPerMeasure, beatValue) { const denominator = parseInt(beatValue.trim(), 10); const numerators = beatsPerMeasure.split('+').map((b) => parseInt(b.trim(), 10)); if (numerators.length > 1) { const fractions = numerators.map((numerator) => new util.Fraction(numerator, denominator)); return Time.complex(config, log, partId, staveNumber, fractions); } return Time.simple(config, log, partId, staveNumber, numerators[0], denominator); } parse() { return { type: 'time', symbol: this.symbol, components: this.getComponents().map((component) => component.parse()), }; } getPartId() { return this.partId; } getStaveNumber() { return this.staveNumber; } /** Returns a fraction that represents the combination of all */ toFraction() { let sum = new util.Fraction(0, 1); for (const component of this.components) { sum = sum.add(component); } return sum.simplify(); } isEqual(timeSignature) { return (this.partId === timeSignature.partId && this.staveNumber === timeSignature.staveNumber && this.isEquivalent(timeSignature)); } isEquivalent(timeSignature) { if (this.symbol !== timeSignature.symbol) { return false; } if (this.components.length !== timeSignature.components.length) { return false; } for (let i = 0; i < this.components.length; i++) { // We use isEqual instead of isEquivalent because they would be _displayed_ differently. if (!this.components[i].isEqual(timeSignature.components[i])) { return false; } } return true; } getComponents() { return this.components.map((component) => new Fraction(component)); } }