@stringsync/vexml
Version:
MusicXML to Vexflow
164 lines (163 loc) • 6.58 kB
JavaScript
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));
}
}