UNPKG

satie

Version:

A sheet music renderer for the web

535 lines (534 loc) 24.9 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/>. */ "use strict"; var musicxml_interfaces_1 = require("musicxml-interfaces"); var lodash_1 = require("lodash"); var invariant = require("invariant"); var document_1 = require("./document"); var private_chordUtil_1 = require("./private_chordUtil"); var private_part_1 = require("./private_part"); var private_attributesSnapshot_1 = require("./private_attributesSnapshot"); var implAttributes_clefData_1 = require("./implAttributes_clefData"); var implAttributes_attributesData_1 = require("./implAttributes_attributesData"); var AttributesModel = (function () { function AttributesModel(_a) { var _b = _a === void 0 ? { divisions: 0 } : _a, divisions = _b.divisions, partSymbol = _b.partSymbol, measureStyles = _b.measureStyles, staffDetails = _b.staffDetails, transposes = _b.transposes, staves = _b.staves, instruments = _b.instruments, directives = _b.directives, clefs = _b.clefs, times = _b.times, keySignatures = _b.keySignatures, footnote = _b.footnote, level = _b.level; this._class = "Attributes"; /*---- I.1 IModel ---------------------------------------------------------------------------*/ /** @prototype only */ this._divCount = 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; } Object.defineProperty(AttributesModel.prototype, "divCount", { get: function () { return this._divCount; }, set: function (count) { invariant(isFinite(count), "Count must be finite."); this._divCount = count; }, enumerable: true, configurable: true }); /*---- Implementation -----------------------------------------------------------------------*/ AttributesModel.prototype.refresh = function (cursor) { 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 = private_attributesSnapshot_1.createAttributesSnapshot({ before: cursor.staffAttributes || {}, current: this, staff: cursor.staffIdx, measure: cursor.measureInstance.idx }); }; AttributesModel.prototype.getLayout = function (cursor) { if (!this._layout) { this._layout = []; } if (!this._layout[cursor.segmentInstance.owner]) { this._layout[cursor.segmentInstance.owner] = new AttributesModel.Layout(); } var layout = this._layout[cursor.segmentInstance.owner]; layout._refresh(this, this._snapshot, this._parent, cursor); return layout; }; AttributesModel.prototype.toXML = function () { var 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(function (a) { return !!a; }); j.clefs = j.clefs.filter(function (a) { return !!a; }); j.keySignatures = j.keySignatures.filter(function (a) { return !!a; }); return musicxml_interfaces_1.serializeAttributes(j) + "\n<forward><duration>" + this.divCount + "</duration></forward>\n"; }; AttributesModel.prototype.toJSON = function () { var _a = this, _class = _a._class, divisions = _a.divisions, partSymbol = _a.partSymbol, measureStyles = _a.measureStyles, staffDetails = _a.staffDetails, transposes = _a.transposes, staves = _a.staves, instruments = _a.instruments, directives = _a.directives, clefs = _a.clefs, times = _a.times, keySignatures = _a.keySignatures, footnote = _a.footnote, level = _a.level; return { _class: _class, divisions: divisions, partSymbol: partSymbol, measureStyles: measureStyles, staffDetails: staffDetails, transposes: transposes, staves: staves, instruments: instruments, directives: directives, clefs: clefs, times: times, keySignatures: keySignatures, footnote: footnote, level: level, }; }; AttributesModel.prototype.inspect = function () { return this.toXML(); }; AttributesModel.prototype.calcWidth = function () { return 0; // TODO }; AttributesModel.prototype._validateClef = function (cursor) { var staffIdx = cursor.staffIdx; // Clefs must be an array if (!(this.clefs instanceof Array)) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.clefs([]); }); }); } // Clefs must have a staff number and be sorted by staff number this.clefs.forEach(function (clef, clefIdx) { if (!clef) { return; } if (clef.number !== clefIdx) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.clefsAt(clefIdx, function (clef) { return 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(function (staff) { return staff.attributes(function (attributes) { return attributes .clefsAt(0, null) // XXX: HACK to fix splice .clefsAt(staffIdx, function (clef) { return clef .number(staffIdx) .sign("G") .line(2); }); }); }); } // Validate the given clef var clef = this.clefs[staffIdx]; if (clef) { if (clef.sign !== clef.sign.toUpperCase()) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.clefsAt(staffIdx, function (clefb) { return clefb.sign(clef.sign.toUpperCase()); }); }); }); } if (clef.line && clef.line !== parseInt("" + clef.line, 10)) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.clefsAt(staffIdx, function (clefb) { return clefb.line(parseInt("" + clef.line, 10)); }); }); }); } // Clef lines can be inferred. if (!clef.line) { var sign = clef.sign; var standardClef_1 = lodash_1.find(implAttributes_clefData_1.standardClefs, { sign: sign }); cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.clefsAt(staffIdx, function (clefb) { return clefb.line(standardClef_1 ? standardClef_1.line : 2); }); }); }); } } }; AttributesModel.prototype._validateTime = function (cursor) { // 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(function (staff) { return staff.attributes(function (attributes) { return attributes.timesAt(0, function (time) { return time .symbol(musicxml_interfaces_1.TimeSymbolType.Common) .beats(["4"]) .beatTypes([4]); }); }); }); } }; AttributesModel.prototype._validateKey = function (cursor) { // Key signatures must be an array this.keySignatures = this.keySignatures || []; if ((!this._parent || !this._parent.keySignature) && !this.keySignatures[0]) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.keySignaturesAt(0, function (key) { return key .fifths(0) .mode("major"); }); }); }); } var 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(function (staff) { return staff.attributes(function (attributes) { return attributes.keySignaturesAt(0, function (key) { return 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(function (staff) { return staff.attributes(function (attributes) { return attributes.keySignaturesAt(0, function (key) { return key .keyAccidentals(null); }); }); }); } if (ks.keyOctaves) { // Let's sort them (move to prefilter?) var keyOctaves_1 = []; lodash_1.forEach(ks.keyOctaves, function (octave) { keyOctaves_1[octave.number - 1] = octave; }); if (!lodash_1.isEqual(ks.keyOctaves, keyOctaves_1)) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.keySignaturesAt(0, function (key) { return key .keyOctaves(keyOctaves_1); }); }); }); } } } }; AttributesModel.prototype._validateStaffDetails = function (cursor) { // Staff details must be an array this.staffDetails = this.staffDetails || []; // Staff details must have a staff number var sSoFar = 0; this.staffDetails.forEach(function (staffDetails, i) { if (staffDetails) { ++sSoFar; if (!staffDetails.number) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.staffDetailsAt(i, function (sd) { return sd.number(sSoFar); }); }); }); } } }); // Staff details must be indexed by staff var staffDetailsByNumber = this.staffDetails.reduce(function (staffDetails, staffDetail) { if (staffDetail) { staffDetails[staffDetail.number] = staffDetail; } ; return staffDetails; }, []); var needsSorting = this.staffDetails.length !== staffDetailsByNumber.length || this.staffDetails.some(function (s, i) { if (!s && !staffDetailsByNumber[i]) { return false; } return !lodash_1.isEqual(s, staffDetailsByNumber[i]); }); if (needsSorting) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.staffDetails(staffDetailsByNumber); }); }); } // Staff details are required. Staff lines are required if (!this.staffDetails[cursor.staffIdx]) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return 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(function (staff) { return staff.attributes(function (attributes) { return attributes.staffDetailsAt(cursor.staffIdx, function (l) { return l.staffLines(5); }); }); }); } }; AttributesModel.prototype._validateStaves = function (cursor) { var _this = this; this.staves = this.staves || 1; // FIXME! var currentPartId = cursor.segmentInstance.part; var currentPart = cursor.measureInstance.parts[currentPartId]; lodash_1.times(this.staves, function (staffMinusOne) { var 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(function (staff) { return staff.attributes(function (attributes) { return attributes.partSymbol({ bottomStaff: 1, topStaff: _this.staves, type: musicxml_interfaces_1.PartSymbolType.Brace, }); }); }); } // HACK: Convert part group symbols to part symbols. // Obviously, this won't fly when we have multiple part groups var groups = private_part_1.groupsForPart(cursor.header.partList, cursor.segmentInstance.part); if (groups.length && (!this._parent || !this._parent.partSymbol) && !this.partSymbol) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.partSymbol({ bottomStaff: 1, topStaff: 1, type: musicxml_interfaces_1.PartSymbolType.Bracket }); }); }); } }; AttributesModel.prototype._validateMeasureStyles = function (cursor) { if (!this.measureStyles) { cursor.patch(function (staff) { return staff.attributes(function (attributes) { return attributes.measureStyles([]); }); }); } }; return AttributesModel; }()); (function (AttributesModel) { var Layout = (function () { function Layout() { } Layout.prototype._refresh = function (model, attributes, prevAttributes, cursor) { var _this = this; 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; var isFirstInLine = cursor.lineBarOnLine === 0 && !this.division; var next = cursor.segmentInstance[cursor.segmentPosition + 1]; var ksVisible = !implAttributes_attributesData_1.keysEqual(attributes, prevAttributes) || isFirstInLine; var tsVisible = !implAttributes_attributesData_1.timesEqual(attributes, prevAttributes); var clefVisible = !implAttributes_attributesData_1.clefsEqual(attributes, prevAttributes, cursor.segmentInstance.owner) || isFirstInLine; var partSymbolVisible = isFirstInLine && attributes.partSymbol && attributes.partSymbol.bottomStaff === cursor.staffIdx; // Measure number if (!cursor.measureInstance.implicit && parseInt(cursor.measureInstance.number, 10) !== 1) { var measureNumbering = cursor.print ? cursor.print.measureNumbering.data : "system"; var firstInMeasure = cursor.segmentDivision === 0; var showNumberBecauseOfSystem = isFirstInLine && measureNumbering === "system"; var showNumberBecauseOfMeasure = this.division === 0 && measureNumbering === "measure" && firstInMeasure; var shouldShowNumber = showNumberBecauseOfSystem || showNumberBecauseOfMeasure; if (shouldShowNumber) { this.measureNumberVisible = cursor.measureInstance.number; } } /*---- Clef layout ------------------------------------*/ var nextChord = cursor.factory.modelHasType(next, document_1.Type.Chord) ? next : null; this.snapshotClef = cursor.staffAttributes.clef; if (clefVisible) { var clef = attributes.clefs[cursor.staffIdx]; this.x += implAttributes_attributesData_1.CLEF_INDENTATION; cursor.segmentX = this.x; var contextualSpacing = 0; this.clef = Object.create(clef, { "defaultX": { get: function () { if (isFirstInLine) { return _this.overrideX; } else { return _this.overrideX - 10.5; } } } }); this.clef.defaultY = this.clef.defaultY || 0; this.clef.size = isFirstInLine ? musicxml_interfaces_1.SymbolSize.Full : musicxml_interfaces_1.SymbolSize.Cue; if (nextChord && !ksVisible && !tsVisible) { if (private_chordUtil_1.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 = implAttributes_attributesData_1.clefWidth(attributes) + contextualSpacing; } else { this.clefSpacing = 0; } /*---- KS layout --------------------------------------*/ if (ksVisible) { var keySignature = attributes.keySignatures[0]; var contextualSpacing = 0; this.keySignature = Object.create(keySignature, { defaultX: { get: function () { return _this.overrideX + _this.clefSpacing; } } }); this.keySignature.defaultY = 0; if (nextChord && !tsVisible) { if (private_chordUtil_1.hasAccidental(nextChord, cursor)) { // TODO: what if there are more than 1 accidental? contextualSpacing = 25; } else { contextualSpacing = 15; } } else { contextualSpacing = 10; } this.ksSpacing = contextualSpacing + implAttributes_attributesData_1.keyWidth(attributes); } else { this.ksSpacing = 0; } /*---- TS layout --------------------------------------*/ if (tsVisible) { var time = attributes.times[0]; var contextualSpacing = 0; this.time = Object.create(time, { defaultX: { get: function () { return _this.overrideX + _this.clefSpacing + _this.ksSpacing; } } }); this.time.defaultY = 0; if (nextChord) { if (private_chordUtil_1.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 + implAttributes_attributesData_1.timeWidth(attributes); } else { this.tsSpacing = 0; } /*---- Part symbol ------------------------------------*/ if (partSymbolVisible) { var partSymbol = cursor.staffAttributes.partSymbol; this.partSymbol = Object.create(partSymbol, { defaultX: { get: function () { 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; }; return Layout; }()); AttributesModel.Layout = Layout; Layout.prototype.expandPolicy = "none"; Layout.prototype.renderClass = document_1.Type.Attributes; Layout.prototype.boundingBoxes = []; Object.freeze(Layout.prototype.boundingBoxes); })(AttributesModel || (AttributesModel = {})); ; /** * Registers Attributes in the factory structure passed in. */ function Export(constructors) { constructors[document_1.Type.Attributes] = AttributesModel; } (function (Export) { function createWarningLayout(cursor, prevAttributes, nextAttributes) { var warningLayout = new AttributesModel.Layout(); console.log("Creating warning layout for ", nextAttributes); warningLayout._refresh(null, nextAttributes, prevAttributes, cursor); return warningLayout; } Export.createWarningLayout = createWarningLayout; })(Export || (Export = {})); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = Export;