UNPKG

satie

Version:

A sheet music renderer for the web

714 lines (607 loc) 25.7 kB
/** * @source: https://github.com/jnetterf/satie/ * * @license * (C) Josh Netterfield <joshua@nettek.ca> 2015. * Part of the Satie music engraver <https://github.com/jnetterf/satie>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import {Clef, PartSymbol, MeasureStyle, StaffDetails, Transpose, Directive, Time, Key, Footnote, Level, Attributes, KeyOctave, PartSymbolType, SymbolSize, TimeSymbolType, serializeAttributes} from "musicxml-interfaces"; import {find, forEach, times, isEqual} from "lodash"; import * as invariant from "invariant"; import {IModel, Type, ILayout} from "./document"; import {IBoundingRect} from "./private_boundingRect"; import {IAttributesSnapshot} from "./private_attributesSnapshot"; import {IReadOnlyValidationCursor, LayoutCursor} from "./private_cursor"; import {hasAccidental} from "./private_chordUtil"; import {groupsForPart} from "./private_part"; import {createAttributesSnapshot as createSnapshot} from "./private_attributesSnapshot"; import {standardClefs} from "./implAttributes_clefData"; import {CLEF_INDENTATION, clefWidth, keyWidth, timeWidth, clefsEqual, timesEqual, keysEqual} from "./implAttributes_attributesData"; class AttributesModel implements Export.IAttributesModel { _class = "Attributes"; /*---- I.1 IModel ---------------------------------------------------------------------------*/ /** @prototype only */ private _divCount = 0; private _layout: AttributesModel.Layout[]; get divCount() { return this._divCount; } set divCount(count: number) { invariant(isFinite(count), "Count must be finite."); this._divCount = count; } /** defined externally */ staffIdx: number; _snapshot: IAttributesSnapshot; /*---- I.2 Attributes -----------------------------------------------------------------------*/ _parent: IAttributesSnapshot; divisions: number; partSymbol: PartSymbol; measureStyles: MeasureStyle[]; staffDetails: StaffDetails[]; transposes: Transpose[]; staves: number; instruments: string; directives: Directive[]; clefs: Clef[]; times: Time[]; keySignatures: Key[]; /*---- I.3 Editorial ------------------------------------------------------------------------*/ footnote: Footnote; level: Level; /*---- Implementation -----------------------------------------------------------------------*/ refresh(cursor: IReadOnlyValidationCursor): void { this._parent = cursor.staffAttributes; if (!this._parent || !this._parent.divisions) { this.divisions = this.divisions || 1; } this._validateClef(cursor); this._validateTime(cursor); this._validateKey(cursor); this._validateStaves(cursor); this._validateStaffDetails(cursor); this._validateMeasureStyles(cursor); this._snapshot = createSnapshot({ before: cursor.staffAttributes || <IAttributesSnapshot> {}, current: this, staff: cursor.staffIdx, measure: cursor.measureInstance.idx }); } getLayout(cursor: LayoutCursor): Export.IAttributesLayout { if (!this._layout) { this._layout = []; } if (!this._layout[cursor.segmentInstance.owner]) { this._layout[cursor.segmentInstance.owner] = new AttributesModel.Layout(); } let layout = this._layout[cursor.segmentInstance.owner]; layout._refresh(this, this._snapshot, this._parent, cursor); return layout; } constructor({divisions, partSymbol, measureStyles, staffDetails, transposes, staves, instruments, directives, clefs, times, keySignatures, footnote, level}: Attributes = {divisions: 0}) { this.divisions = divisions; this.partSymbol = partSymbol; this.measureStyles = measureStyles; this.staffDetails = staffDetails; this.transposes = transposes; this.staves = staves; this.instruments = instruments; this.directives = directives; this.clefs = clefs; this.times = times; this.keySignatures = keySignatures; this.footnote = footnote; this.level = level; } toXML(): string { let j = this.toJSON(); // Hack: we index staffDetails by 1-index staff, leaving a null at index 0, with MXML doesn't handle. j.staffDetails = j.staffDetails.filter(a => !!a); j.clefs = j.clefs.filter(a => !!a); j.keySignatures = j.keySignatures.filter(a => !!a); return `${serializeAttributes(j)}\n<forward><duration>${this.divCount}</duration></forward>\n`; } toJSON(): Attributes { const { _class, divisions, partSymbol, measureStyles, staffDetails, transposes, staves, instruments, directives, clefs, times, keySignatures, footnote, level, } = this; return { _class, divisions, partSymbol, measureStyles, staffDetails, transposes, staves, instruments, directives, clefs, times, keySignatures, footnote, level, }; } inspect() { return this.toXML(); } calcWidth() { return 0; // TODO } private _validateClef(cursor: IReadOnlyValidationCursor) { const staffIdx = cursor.staffIdx; // Clefs must be an array if (!(this.clefs instanceof Array)) { cursor.patch(staff => staff.attributes(attributes => attributes.clefs([]) )); } // Clefs must have a staff number and be sorted by staff number this.clefs.forEach((clef, clefIdx) => { if (!clef) { return; } if (clef.number !== clefIdx) { cursor.patch(staff => staff.attributes(attributes => attributes.clefsAt(clefIdx, clef => clef.number(clefIdx) ) )); } }); // A clef is mandatory (we haven't implemented clef-less staves yet) if ((!this._parent || !this._parent.clef) && !this.clefs[staffIdx]) { cursor.patch(staff => staff.attributes(attributes => attributes .clefsAt(0, null) // XXX: HACK to fix splice .clefsAt(staffIdx, clef => clef .number(staffIdx) .sign("G") .line(2) ) )); } // Validate the given clef let clef = this.clefs[staffIdx]; if (clef) { if (clef.sign !== clef.sign.toUpperCase()) { cursor.patch(staff => staff.attributes(attributes => attributes.clefsAt(staffIdx, clefb => clefb.sign(clef.sign.toUpperCase()) ) )); } if (clef.line && clef.line !== parseInt("" + clef.line, 10)) { cursor.patch(staff => staff.attributes(attributes => attributes.clefsAt(staffIdx, clefb => clefb.line(parseInt("" + clef.line, 10)) ) )); } // Clef lines can be inferred. if (!clef.line) { let {sign} = clef; let standardClef = find(standardClefs, {sign}); cursor.patch(staff => staff.attributes(attributes => attributes.clefsAt(staffIdx, clefb => clefb.line(standardClef ? standardClef.line : 2) ) )); } } } private _validateTime(cursor: IReadOnlyValidationCursor) { // Times must be an array this.times = this.times || []; // A time signature is mandatory. if ((!this._parent || !this._parent.time) && !this.times[0]) { cursor.patch(staff => staff.attributes(attributes => attributes.timesAt(0, time => time .symbol(TimeSymbolType.Common) .beats(["4"]) .beatTypes([4]) ) )); } } private _validateKey(cursor: IReadOnlyValidationCursor) { // Key signatures must be an array this.keySignatures = this.keySignatures || []; if ((!this._parent || !this._parent.keySignature) && !this.keySignatures[0]) { cursor.patch(staff => staff.attributes(attributes => attributes.keySignaturesAt(0, key => key .fifths(0) .mode("major") ) )); } let ks = this.keySignatures[0]; if (ks && (ks.keySteps || ks.keyAlters || ks.keyOctaves)) { if (ks.keySteps.length !== ks.keyAlters.length) { console.warn( "Expected the number of steps to equal the number of alterations. " + "Ignoring key."); cursor.patch(staff => staff.attributes(attributes => attributes.keySignaturesAt(0, key => key .fifths(0) .keySteps(null) .keyAccidentals(null) .keyAlters(null) ) )); } if (ks.keyAccidentals && ks.keyAccidentals.length !== ks.keySteps.length) { if (ks.keyAccidentals.length) { console.warn( "Currently, if `key-accidentals` are specified, they must be " + "specified for all steps in a key signature due to a limitation " + "in musicxml-interfaces. Ignoring `key-accidentals`"); } cursor.patch(staff => staff.attributes(attributes => attributes.keySignaturesAt(0, key => key .keyAccidentals(null) ) )); } if (ks.keyOctaves) { // Let's sort them (move to prefilter?) let keyOctaves: KeyOctave[] = []; forEach(ks.keyOctaves, octave => { keyOctaves[octave.number - 1] = octave; }); if (!isEqual(ks.keyOctaves, keyOctaves)) { cursor.patch(staff => staff.attributes(attributes => attributes.keySignaturesAt(0, key => key .keyOctaves(keyOctaves) ) )); } } } } private _validateStaffDetails(cursor: IReadOnlyValidationCursor) { // Staff details must be an array this.staffDetails = this.staffDetails || []; // Staff details must have a staff number let sSoFar = 0; this.staffDetails.forEach((staffDetails, i) => { if (staffDetails) { ++sSoFar; if (!staffDetails.number) { cursor.patch(staff => staff.attributes(attributes => attributes.staffDetailsAt(i, sd => sd.number(sSoFar) ) )); } } }); // Staff details must be indexed by staff const staffDetailsByNumber: StaffDetails[] = this.staffDetails.reduce((staffDetails, staffDetail) => { if (staffDetail) { staffDetails[staffDetail.number] = staffDetail; }; return staffDetails; }, []); let needsSorting = this.staffDetails.length !== staffDetailsByNumber.length || this.staffDetails.some((s, i) => { if (!s && !staffDetailsByNumber[i]) { return false; } return !isEqual(s, staffDetailsByNumber[i]); }); if (needsSorting) { cursor.patch(staff => staff.attributes(attributes => attributes.staffDetails(staffDetailsByNumber) )); } // Staff details are required. Staff lines are required if (!this.staffDetails[cursor.staffIdx]) { cursor.patch(staff => staff.attributes(attributes => attributes .staffDetailsAt(0, null) // XXX: HACK .staffDetailsAt(cursor.staffIdx, { number: cursor.staffIdx, }) )); } if ((!this._parent || !this._parent.staffDetails || !this._parent.staffDetails[cursor.staffIdx] || !this._parent.staffDetails[cursor.staffIdx].staffLines) && (!this.staffDetails[cursor.staffIdx] || !this.staffDetails[cursor.staffIdx].staffLines)) { cursor.patch(staff => staff.attributes(attributes => attributes.staffDetailsAt(cursor.staffIdx, l => l.staffLines(5)) )); } } private _validateStaves(cursor: IReadOnlyValidationCursor) { this.staves = this.staves || 1; // FIXME! let currentPartId = cursor.segmentInstance.part; let currentPart = cursor.measureInstance.parts[currentPartId]; times(this.staves, staffMinusOne => { let staff = staffMinusOne + 1; if (!currentPart.staves[staff]) { throw new Error("A staff is missing. The code to add it is not implemented."); } }); if (this.staves > 1 && (!this._parent || !this._parent.partSymbol) && !this.partSymbol) { cursor.patch(staff => staff.attributes(attributes => attributes.partSymbol({ bottomStaff: 1, topStaff: this.staves, type: PartSymbolType.Brace, }) )); } // HACK: Convert part group symbols to part symbols. // Obviously, this won't fly when we have multiple part groups let groups = groupsForPart(cursor.header.partList, cursor.segmentInstance.part); if (groups.length && (!this._parent || !this._parent.partSymbol) && !this.partSymbol) { cursor.patch(staff => staff.attributes(attributes => attributes.partSymbol({ bottomStaff: 1, topStaff: 1, type: PartSymbolType.Bracket }) )); } } private _validateMeasureStyles(cursor: IReadOnlyValidationCursor): void { if (!this.measureStyles) { cursor.patch(staff => staff.attributes(attributes => attributes.measureStyles([]) )); } } } module AttributesModel { export class Layout implements Export.IAttributesLayout { _refresh(model: IModel, attributes: Attributes, prevAttributes: Attributes, cursor: LayoutCursor) { this.model = model; invariant(!!attributes, "Layout must be passed a model"); this.clef = null; this.snapshotClef = null; this.clefSpacing = null; this.time = null; this.tsSpacing = null; this.keySignature = null; this.ksSpacing = null; this.measureNumberVisible = null; this.partSymbol = null; this.staffDetails = null; this.x = cursor.segmentX; this.division = cursor.segmentDivision; this.staffIdx = cursor.staffIdx; let isFirstInLine = cursor.lineBarOnLine === 0 && !this.division; let next = cursor.segmentInstance[cursor.segmentPosition + 1]; let ksVisible = !keysEqual(attributes, prevAttributes) || isFirstInLine; let tsVisible = !timesEqual(attributes, prevAttributes); let clefVisible = !clefsEqual(attributes, prevAttributes, cursor.segmentInstance.owner) || isFirstInLine; let partSymbolVisible = isFirstInLine && attributes.partSymbol && attributes.partSymbol.bottomStaff === cursor.staffIdx; // Measure number if (!cursor.measureInstance.implicit && parseInt(cursor.measureInstance.number, 10) !== 1) { let measureNumbering = cursor.print ? cursor.print.measureNumbering.data : "system"; let firstInMeasure = cursor.segmentDivision === 0; let showNumberBecauseOfSystem = isFirstInLine && measureNumbering === "system"; let showNumberBecauseOfMeasure = this.division === 0 && measureNumbering === "measure" && firstInMeasure; let shouldShowNumber = showNumberBecauseOfSystem || showNumberBecauseOfMeasure; if (shouldShowNumber) { this.measureNumberVisible = cursor.measureInstance.number; } } /*---- Clef layout ------------------------------------*/ const nextChord = cursor.factory.modelHasType(next, Type.Chord) ? next : null; this.snapshotClef = cursor.staffAttributes.clef; if (clefVisible) { let clef = attributes.clefs[cursor.staffIdx]; this.x += CLEF_INDENTATION; cursor.segmentX = this.x; let contextualSpacing = 0; this.clef = Object.create(clef, { "defaultX": { get: () => { if (isFirstInLine) { return this.overrideX; } else { return this.overrideX - 10.5; } } } }); this.clef.defaultY = this.clef.defaultY || 0; this.clef.size = isFirstInLine ? SymbolSize.Full : SymbolSize.Cue; if (nextChord && !ksVisible && !tsVisible) { if (hasAccidental(nextChord, cursor)) { // TODO: what if there are more than 1 accidental? contextualSpacing = 15; } else { contextualSpacing = 25; } } else { contextualSpacing = 12.5; } if (!isFirstInLine) { contextualSpacing -= 19.8; } this.clefSpacing = clefWidth(attributes) + contextualSpacing; } else { this.clefSpacing = 0; } /*---- KS layout --------------------------------------*/ if (ksVisible) { let keySignature = attributes.keySignatures[0]; let contextualSpacing = 0; this.keySignature = Object.create(keySignature, { defaultX: { get: () => { return this.overrideX + this.clefSpacing; } } }); this.keySignature.defaultY = 0; if (nextChord && !tsVisible) { if (hasAccidental(nextChord, cursor)) { // TODO: what if there are more than 1 accidental? contextualSpacing = 25; } else { contextualSpacing = 15; } } else { contextualSpacing = 10; } this.ksSpacing = contextualSpacing + keyWidth(attributes); } else { this.ksSpacing = 0; } /*---- TS layout --------------------------------------*/ if (tsVisible) { let time = attributes.times[0]; let contextualSpacing = 0; this.time = Object.create(time, { defaultX: { get: () => { return this.overrideX + this.clefSpacing + this.ksSpacing; } } }); this.time.defaultY = 0; if (nextChord) { if (hasAccidental(nextChord, cursor)) { // TODO: what if there are more than 1 accidental? contextualSpacing = 25; } else { contextualSpacing = 15; } } else { contextualSpacing = 12.5; } if (!attributes.times[0].beatTypes) { contextualSpacing = 0; } this.tsSpacing = contextualSpacing + timeWidth(attributes); } else { this.tsSpacing = 0; } /*---- Part symbol ------------------------------------*/ if (partSymbolVisible) { let {partSymbol} = cursor.staffAttributes; this.partSymbol = Object.create(partSymbol, { defaultX: { get: () => { return 0; } } }); } this.staffDetails = cursor.staffAttributes.staffDetails[this.staffIdx]; /*---- Geometry ---------------------------------------*/ cursor.segmentX += this.clefSpacing + this.tsSpacing + this.ksSpacing; this.renderedWidth = cursor.segmentX - this.x - 8; } /*---- ILayout ------------------------------------------------------*/ // Constructed: model: IModel; x: number; division: number; staffIdx: number; /** * Set by layout engine. */ overrideX: number; // Prototype: boundingBoxes: IBoundingRect[]; renderClass: Type; expandPolicy: "none"; renderedWidth: number; /*---- AttributesModel ----------------------------------------------*/ clef: Clef; snapshotClef: Clef; clefSpacing: number; time: Time; tsSpacing: number; keySignature: Key; ksSpacing: number; /** undefined if no measure number should be displayed. */ measureNumberVisible: string; partSymbol: PartSymbol; staffDetails: StaffDetails; } Layout.prototype.expandPolicy = "none"; Layout.prototype.renderClass = Type.Attributes; Layout.prototype.boundingBoxes = []; Object.freeze(Layout.prototype.boundingBoxes); }; /** * Registers Attributes in the factory structure passed in. */ function Export(constructors: { [key: number]: any }) { constructors[Type.Attributes] = AttributesModel; } module Export { export interface IAttributesModel extends Attributes, IModel { divisions: number; } export interface IAttributesLayout extends ILayout { model: IModel; clef: Clef; snapshotClef: Clef; clefSpacing: number; time: Time; tsSpacing: number; keySignature: Key; ksSpacing: number; measureNumberVisible: string; partSymbol: PartSymbol; staffIdx: number; staffDetails: StaffDetails; } export function createWarningLayout(cursor: LayoutCursor, prevAttributes: Attributes, nextAttributes: Attributes): IAttributesLayout { let warningLayout = new AttributesModel.Layout(); console.log("Creating warning layout for ", nextAttributes); warningLayout._refresh( null, nextAttributes, prevAttributes, cursor, ); return warningLayout; } } export default Export;