UNPKG

satie

Version:

A sheet music renderer for the web

643 lines (642 loc) 28.8 kB
/** * This file is part of Satie music engraver <https://github.com/jnetterf/satie>. * Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present. * * Satie 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. * * Satie 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 Satie. 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_metre_checkBeaming_1 = require("./private_metre_checkBeaming"); var private_chordUtil_1 = require("./private_chordUtil"); var private_smufl_1 = require("./private_smufl"); var implChord_noteImpl_1 = require("./implChord_noteImpl"); var implChord_lyrics_1 = require("./implChord_lyrics"); var implChord_notation_1 = require("./implChord_notation"); var IDEAL_STEM_HEIGHT = 35; var MIN_STEM_HEIGHT = 30; var BASE_GRACE_WIDTH = 11.4; var BASE_STD_WIDTH = 30; var GRACE_FLATTEN_FACTOR = 0.1; var ACCIDENTAL_WIDTH = 0.73; var LOG_STRETCH = 28; var countToNotehead = (_a = {}, _a[musicxml_interfaces_1.Count.Maxima] = "noteheadDoubleWhole", _a[musicxml_interfaces_1.Count.Long] = "noteheadDoubleWhole", _a[musicxml_interfaces_1.Count.Breve] = "noteheadDoubleWhole", _a[musicxml_interfaces_1.Count.Whole] = "noteheadWhole", _a[-1] = "noteheadWhole", _a[musicxml_interfaces_1.Count.Half] = "noteheadHalf", _a[musicxml_interfaces_1.Count.Quarter] = "noteheadBlack", _a[musicxml_interfaces_1.Count.Eighth] = "noteheadBlack", _a[musicxml_interfaces_1.Count._16th] = "noteheadBlack", _a[musicxml_interfaces_1.Count._32nd] = "noteheadBlack", _a[musicxml_interfaces_1.Count._64th] = "noteheadBlack", _a[musicxml_interfaces_1.Count._128th] = "noteheadBlack", _a[musicxml_interfaces_1.Count._256th] = "noteheadBlack", _a[musicxml_interfaces_1.Count._512th] = "noteheadBlack", _a[musicxml_interfaces_1.Count._1024th] = "noteheadBlack", _a); var countToRest = (_b = {}, _b[musicxml_interfaces_1.Count.Maxima] = "restLonga", _b[musicxml_interfaces_1.Count.Long] = "restLonga", _b[musicxml_interfaces_1.Count.Breve] = "restDoubleWhole", _b[musicxml_interfaces_1.Count.Whole] = "restWhole", _b[-1] = "restWhole", _b[musicxml_interfaces_1.Count.Half] = "restHalf", _b[musicxml_interfaces_1.Count.Quarter] = "restQuarter", _b[musicxml_interfaces_1.Count.Eighth] = "rest8th", _b[musicxml_interfaces_1.Count._16th] = "rest16th", _b[musicxml_interfaces_1.Count._32nd] = "rest32nd", _b[musicxml_interfaces_1.Count._64th] = "rest64th", _b[musicxml_interfaces_1.Count._128th] = "rest128th", _b[musicxml_interfaces_1.Count._256th] = "rest256th", _b[musicxml_interfaces_1.Count._512th] = "rest512th", _b[musicxml_interfaces_1.Count._1024th] = "rest1024th", _b); /** * A model that represents 1 or more notes in the same voice, starting on the same beat, and each * with the same duration. Any number of these notes may be rests. */ var ChordModelImpl = (function () { /*---- Implementation -----------------------------------------------------------------------*/ /** * We accept either a Note from musicxml-interfaces, or an IChord, which * is an array-like element of Notes. In either case, we create a deep copy. */ function ChordModelImpl(spec) { var _this = this; this.length = 0; this._init = false; if (!!spec) { if (spec._class === "Note") { this[0] = new implChord_noteImpl_1.default(this, 0, spec); this.length = 1; } else if (spec.length) { lodash_1.forEach(spec, function (note, idx) { _this[idx] = new implChord_noteImpl_1.default(_this, idx, note); }); this.length = spec.length; } } } Object.defineProperty(ChordModelImpl.prototype, "staffIdx", { get: function () { return this[0].staff || 1; }, set: function (n) { // Ignore. }, enumerable: true, configurable: true }); Object.defineProperty(ChordModelImpl.prototype, "satieLedger", { get: function () { return private_chordUtil_1.ledgerLines(this, this._clef); }, enumerable: true, configurable: true }); Object.defineProperty(ChordModelImpl.prototype, "rest", { get: function () { return lodash_1.some(this, function (note) { return note.rest; }); }, enumerable: true, configurable: true }); Object.defineProperty(ChordModelImpl.prototype, "timeModification", { get: function () { return this[0].timeModification; }, enumerable: true, configurable: true }); Object.defineProperty(ChordModelImpl.prototype, "notes", { get: function () { var _this = this; return lodash_1.times(this.length, function (i) { return _this[i]; }); }, enumerable: true, configurable: true }); Object.defineProperty(ChordModelImpl.prototype, "count", { get: function () { var noteType = this[0].noteType; if (!noteType) { return null; } return noteType.duration; }, enumerable: true, configurable: true }); ChordModelImpl.prototype.push = function () { var _this = this; var notes = []; for (var _i = 0; _i < arguments.length; _i++) { notes[_i] = arguments[_i]; } lodash_1.forEach(notes, function (note) { _this[_this.length] = new implChord_noteImpl_1.default(_this, _this.length, note); ++_this.length; }); return this.length; }; ChordModelImpl.prototype.splice = function (start, deleteCount) { var _this = this; var replacements = []; for (var _i = 2; _i < arguments.length; _i++) { replacements[_i - 2] = arguments[_i]; } var notes = this.notes; notes.splice.apply(notes, [start, deleteCount].concat(replacements)); lodash_1.times(this.length, function (i) { return delete _this[i]; }); lodash_1.forEach(notes, function (n, i) { invariant(n instanceof implChord_noteImpl_1.default, "Notes must be NoteImpls in Chords"); _this[i] = n; }); this.length = notes.length; }; ChordModelImpl.prototype.refresh = function (cursor) { var _this = this; if (!this[0].noteType || !this[0].noteType.duration) { var count_1 = this._implyCountFromPerformanceData(cursor); cursor.dangerouslyPatchWithoutValidation(function (voice) { return lodash_1.reduce(_this, function (builder, note, idx) { return builder .note(idx, function (j) { return j.noteType({ duration: count_1 }); }); }, voice); }); } try { var divCount = private_chordUtil_1.divisions(this, cursor.staffAttributes); if (divCount !== this.divCount) { cursor.fixup([ { p: [cursor.measureInstance.uuid, "parts", cursor.segmentInstance.part, "voices", cursor.segmentInstance.owner, cursor.segmentPosition, "divCount"], oi: divCount, od: this.divCount, }, ]); } } catch (err) { if (err instanceof private_chordUtil_1.FractionalDivisionsException) { cursor.fixup([ { p: ["divisions"], oi: err.requiredDivisions, od: cursor.staffAttributes.divisions, } ]); } } invariant(isFinite(this.divCount), "The beat count must be numeric"); invariant(this.divCount >= 0, "The beat count must be non-negative."); var direction = this._pickDirection(cursor); var clef = cursor.staffAttributes.clef; this._clef = clef; lodash_1.forEach(this, function (note, idx) { if (!note.grace && note.duration !== _this.divCount) { cursor.patch(function (partBuilder) { return partBuilder .note(idx, function (note) { return note .duration(_this.divCount); }); }); } if (idx > 0 && !note.chord) { cursor.patch(function (partBuilder) { return partBuilder .note(idx, function (note) { return note .chord({}); }); }); } else if (idx === 0 && note.chord) { cursor.patch(function (partBuilder) { return partBuilder .note(idx, function (note) { return note .chord(null); }); }); } note.refresh(cursor); note.updateAccidental(cursor); }); // Check for second intervals: var notesSortedByY = this.notes.sort(function (a, b) { return a.defaultY - b.defaultY; }); var _loop_1 = function (i) { if (i + 1 < notesSortedByY.length && notesSortedByY[i + 1].defaultY - notesSortedByY[i].defaultY === 5) { if (direction > 0) { if (notesSortedByY[i].relativeX !== 0 || notesSortedByY[i + 1].relativeX !== 13) { cursor.patch(function (voice) { return voice .note(notesSortedByY[i]._idx, function (note) { return note.relativeX(0); }) .note(notesSortedByY[i + 1]._idx, function (note) { return note.relativeX(13); }); }); } } else { if (notesSortedByY[i].relativeX !== -13 || notesSortedByY[i + 1].relativeX !== 0) { cursor.patch(function (voice) { return voice .note(notesSortedByY[i]._idx, function (note) { return note.relativeX(-13); }) .note(notesSortedByY[i + 1]._idx, function (note) { return note.relativeX(0); }); }); } } ++i; } else { if (notesSortedByY[i].relativeX !== 0) { cursor.patch(function (voice) { return voice .note(notesSortedByY[i]._idx, function (note) { return note.relativeX(0); }); }); } } out_i_1 = i; }; var out_i_1; for (var i = 0; i < notesSortedByY.length; ++i) { _loop_1(i); i = out_i_1; } this.wholebar = this.divCount === private_chordUtil_1.barDivisions(cursor.staffAttributes) || this.divCount === -1; var count = this.count; invariant(isFinite(count) && count !== null, "%s is not a valid count", count); for (var i = 0; i < this.length; ++i) { invariant(this[i].noteType.duration === count, "Inconsistent count (%s != %s)", this[i].noteType.duration, count); } this._checkMulitpleRest(cursor); this._implyNoteheads(cursor); if (!this._init) { if (private_chordUtil_1.countToIsBeamable[count]) { this.satieFlag = private_chordUtil_1.countToFlag[count]; } else { this.satieFlag = null; } if (this._hasStem()) { this.satieStem = { direction: direction, stemHeight: this._getStemHeight(direction, clef), stemStart: private_chordUtil_1.startingLine(this, direction, clef) }; this.satieDirection = direction === 1 ? musicxml_interfaces_1.StemType.Up : musicxml_interfaces_1.StemType.Down; } else { this.satieStem = null; this.satieDirection = NaN; } } }; ChordModelImpl.prototype.getLayout = function (cursor) { this._init = true; if (!this._layout) { this._layout = new ChordModelImpl.Layout(); } this._layout.refresh(this, cursor); return this._layout; }; ChordModelImpl.prototype.toJSON = function () { var data = lodash_1.map(this, function (note) { return note; }); return data; }; ChordModelImpl.prototype.toXML = function () { var xml = ""; for (var i = 0; i < this.length; ++i) { xml += musicxml_interfaces_1.serializeNote(this[i]) + "\n"; } return xml; }; ChordModelImpl.prototype.inspect = function () { return this.toXML(); }; ChordModelImpl.prototype.calcWidth = function (shortest) { var accidentalWidth = this.calcAccidentalWidth(); // TODO: Each note's width has a linear component proportional to log of its duration // with respect to the shortest length var extraWidth = this.divCount ? (Math.log(this.divCount) - Math.log(shortest)) * LOG_STRETCH : 0; var grace = this[0].grace; if (grace) { // TODO: Put grace notes in own segment extraWidth *= GRACE_FLATTEN_FACTOR; } var baseWidth = grace ? BASE_GRACE_WIDTH : BASE_STD_WIDTH; invariant(extraWidth >= 0, "Invalid extraWidth %s. shortest is %s, got %s", extraWidth, shortest, this.divCount); var totalWidth = baseWidth + extraWidth + accidentalWidth + this.calcDotWidth(); return totalWidth; }; ChordModelImpl.prototype.calcAccidentalWidth = function () { return lodash_1.reduce(this, function (maxWidth, note) { return Math.max(maxWidth, note.accidental ? -note.accidental.defaultX : 0); }, 0) * ACCIDENTAL_WIDTH; }; ChordModelImpl.prototype.calcDotWidth = function () { if (this.wholebar || this.satieMultipleRest) { return 0; } return lodash_1.max(lodash_1.map(this, function (m) { return (m.dots || []).length; })) * 6; }; ChordModelImpl.prototype._implyCountFromPerformanceData = function (cursor) { var _this = this; var count; var _a = cursor.staffAttributes, time = _a.time, divisions = _a.divisions; var ts = { beatType: time.beatTypes[0], beats: lodash_1.reduce(time.beats, function (sum, beat) { return sum + parseInt(beat, 10); }, 0) }; var factor = ts.beatType / 4; var beats = factor * (this[0].duration / divisions); count = 4 / (this[0].duration / divisions); // Try dots var dotFactor = 1; var dots = 0; while (!isPO2(1 / (beats / dotFactor / 4))) { if (dots === 5) { dots = 0; break; } ++dots; dotFactor += Math.pow(1 / 2, dots); } if (dots > 0) { count = (1 / (beats / dotFactor / 4 / factor)); cursor.patch(function (voiceA) { return lodash_1.reduce(lodash_1.times(_this.length), function (voice, idx) { return voice.note(idx, function (note) { return note.dots(lodash_1.times(dots, function (dot) { return ({}); })); }); }, voiceA); }); } // Try tuplets // TODO // Try ties if (!isPO2(count)) { // Whole bar rests can still exist even when there's no single NOTE duration // that spans a bar. if (beats === ts.beats && !!this[0].rest) { count = musicxml_interfaces_1.Count.Whole; } else { var nextPO2 = Math.pow(2, Math.ceil(Math.log(this.count) / Math.log(2))); count = nextPO2; } } // TODO: Find the best match for performance data function isPO2(n) { if (Math.abs(Math.round(n) - n) > 0.00001) { return false; } n = Math.round(n); /* tslint:disable */ return !!n && !(n & (n - 1)); /* tslint:enable */ } return count; }; ChordModelImpl.prototype._getStemHeight = function (direction, clef) { var heightFromOtherNotes = (private_chordUtil_1.highestLine(this, clef) - private_chordUtil_1.lowestLine(this, clef)) * 10; var start = private_chordUtil_1.heightDeterminingLine(this, direction, clef) * 10; var idealExtreme = start + direction * IDEAL_STEM_HEIGHT; var result; if (idealExtreme >= 65) { result = Math.max(MIN_STEM_HEIGHT, IDEAL_STEM_HEIGHT - (idealExtreme - 65)); } else if (idealExtreme <= -15) { result = Math.max(MIN_STEM_HEIGHT, IDEAL_STEM_HEIGHT - (-15 - idealExtreme)); } else { result = 35; } // All stems in the main voice should touch the center line. if (start > 30 && direction === -1 && start - result > 30) { result = start - 30; } else if (start < 30 && direction === 1 && start + result < 30) { result = 30 - start; } // Grace note stems are short (though still proportionally pretty tall) if (this[0].grace) { result *= 0.75; } result += heightFromOtherNotes; return result; }; ChordModelImpl.prototype._pickDirection = function (cursor) { var clef = cursor.staffAttributes.clef; var avgLine = private_chordUtil_1.averageLine(this, clef); if (avgLine > 3) { return -1; } else if (avgLine < 3) { return 1; } else { // There's no "right answer" here. We'll follow what "Behind Bars" recommends. // TODO: Consider notes outside current bar // TODO: Consider notes outside current stave // TODO: Handle clef changes correctly var notes = cursor.segmentInstance.filter(function (el) { return cursor.factory.modelHasType(el, document_1.Type.Chord); }); var nIdx = notes.indexOf(this); // 1. Continue the stem direction of surrounding stems that are in one // direction only var linePrev = nIdx > 0 ? private_chordUtil_1.averageLine(notes[nIdx - 1], clef) : 3; if (linePrev === 3 && nIdx > 0) { // Note, the solution obtained may not be ideal, because we greedily resolve // ties in a forward direction. linePrev = notes[nIdx - 1].satieDirection === 1 ? 2.99 : 3.01; } var lineNext = nIdx + 1 < notes.length ? private_chordUtil_1.averageLine(notes[nIdx + 1], clef) : 3; if (linePrev > 3 && lineNext > 3) { return -1; } if (linePrev < 3 && lineNext < 3) { return 1; } // 2. When the stem direction varies within a bar, maintain the stem direction // of the notes that are part of the same beat or half-bar. // (Note: we use the more general beaming pattern instead of half-bar to // decide boundries) var time = cursor.staffAttributes.time; var beamingPattern = private_metre_checkBeaming_1.getBeamingPattern(time); var bpDivisions = lodash_1.map(beamingPattern, function (seg) { return private_chordUtil_1.divisions(seg, cursor.staffAttributes); }); var currDivision = cursor.segmentDivision; var prevDivisionStart = 0; var i = 0; for (; i < bpDivisions.length; ++i) { if (prevDivisionStart + bpDivisions[i] >= currDivision) { break; } prevDivisionStart += bpDivisions[i]; } var nextDivisionStart = prevDivisionStart + bpDivisions[i] || NaN; var prevExists = prevDivisionStart < currDivision; var nextExists = nextDivisionStart > currDivision + this.divCount; var considerPrev = prevExists ? notes[nIdx - 1] : null; var considerNext = nextExists ? notes[nIdx + 1] : null; if (considerPrev && !considerNext && linePrev !== 3) { return linePrev > 3 ? -1 : 1; } else if (considerNext && !considerPrev && lineNext !== 3) { return lineNext > 3 ? -1 : 1; } // 2b. Check beat when considerPrev && considerNext // TODO: Implement me // 3. When there is no clear-cut case for either direction, the convention // is to use down-stem return -1; } }; ChordModelImpl.prototype._checkMulitpleRest = function (cursor) { var measureStyle = cursor.staffAttributes.measureStyle; var multipleRest = measureStyle && measureStyle.multipleRest; if (multipleRest && multipleRest.count > 1) { this.satieMultipleRest = measureStyle.multipleRest; } }; ChordModelImpl.prototype._implyNoteheads = function (cursor) { var _this = this; var measureStyle = cursor.staffAttributes.measureStyle; if (measureStyle) { lodash_1.forEach(this, function (note) { if (measureStyle.slash) { note.notehead = note.notehead || { type: null }; note.notehead.type = musicxml_interfaces_1.NoteheadType.Slash; if (!measureStyle.slash.useStems) { note.stem = { type: musicxml_interfaces_1.StemType.None }; } } }); } if (this.rest) { if (this.satieMultipleRest) { this.noteheadGlyph = ["restHBar"]; } else { this.noteheadGlyph = [countToRest[this.count]]; } } else { this.noteheadGlyph = lodash_1.times(this.length, function () { return countToNotehead[_this.count]; }); } this.noteheadGlyph = this.noteheadGlyph.map(function (stdGlyph, idx) { return private_chordUtil_1.getNoteheadGlyph(_this[idx].notehead, stdGlyph); }); }; ChordModelImpl.prototype._hasStem = function () { if (this[0] && this[0].stem && this[0].stem.type === musicxml_interfaces_1.StemType.None) { return false; } return private_chordUtil_1.countToHasStem[this.count]; }; return ChordModelImpl; }()); (function (ChordModelImpl) { var Layout = (function () { function Layout() { } /*---- Implementation ----------------------------------------------------*/ Layout.prototype.refresh = function (baseModel, cursor) { // ** this function should not modify baseModel ** this.division = cursor.segmentDivision; var measureStyle = cursor.staffAttributes.measureStyle; if (measureStyle.multipleRest && !measureStyle.multipleRestInitiatedHere) { // This is not displayed because it is part of a multirest. this.x = 0; this.expandPolicy = "none"; return; } this.model = this._detachModelWithContext(cursor, baseModel); this.satieStem = baseModel.satieStem; this.satieFlag = baseModel.satieFlag; this.boundingBoxes = this._captureBoundingBoxes(); var isWholeBar = baseModel.wholebar || baseModel.count === musicxml_interfaces_1.Count.Whole; this.expandPolicy = baseModel.satieMultipleRest || baseModel.rest && isWholeBar ? "centered" : "after"; lodash_1.forEach(this.model, function (note) { var staff = note.staff || 1; invariant(!!staff, "Expected the staff to be a non-zero number, got %s", staff); var paddingTop = cursor.lineMaxPaddingTopByStaff[staff] || 0; var paddingBottom = cursor.lineMaxPaddingBottomByStaff[staff] || 0; cursor.lineMaxPaddingTopByStaff[staff] = Math.max(paddingTop, note.defaultY - 50); cursor.lineMaxPaddingBottomByStaff[staff] = Math.max(paddingBottom, -note.defaultY - 25); }); var accidentalWidth = baseModel.calcAccidentalWidth(); var totalWidth = baseModel.calcWidth(cursor.lineShortest); invariant(isFinite(totalWidth), "Invalid width %s", totalWidth); var noteheads = baseModel.noteheadGlyph; var widths = lodash_1.map(noteheads, private_smufl_1.getWidth); this.renderedWidth = lodash_1.max(widths); if (baseModel.satieMultipleRest || baseModel.count === musicxml_interfaces_1.Count.Whole) { lodash_1.forEach(this.model, function (note) { return note.dots = []; }); } this.x = cursor.segmentX + accidentalWidth; this.minSpaceAfter = this._getMinWidthAfter(cursor); this.minSpaceBefore = this._getMinWidthBefore(cursor); cursor.segmentX += totalWidth; }; Layout.prototype._captureBoundingBoxes = function () { var _this = this; var bboxes = []; lodash_1.forEach(this.model, function (note) { var notations = private_chordUtil_1.notationObj(note); // TODO: detach this var bbn = implChord_notation_1.getBoundingRects(notations, note, _this); bboxes = bboxes.concat(bbn.bb); note.notations = [bbn.n]; }); return bboxes; }; Layout.prototype._getMinWidthBefore = function (cursor) { return this._getLyricWidth(cursor) / 2; }; Layout.prototype._getMinWidthAfter = function (cursor) { return this._getLyricWidth(cursor) / 2; }; Layout.prototype._getLyricWidth = function (cursor) { var factor = 40 * 25.4 / 96; // 40 tenths in staff * pixelFactor return implChord_lyrics_1.getChordLyricWidth(this.model, factor); }; Layout.prototype._detachModelWithContext = function (cursor, baseModel) { var _this = this; var model = lodash_1.map(baseModel, function (note, idx) { /* Here, we're extending each note to have the correct * default position. To do so, we use prototypical * inheritance. See Object.create. */ return Object.create(note, { defaultX: { get: function () { return (note.relativeX || 0) + (_this.overrideX || _this.x); } }, stem: { get: function () { return baseModel.stem || { type: baseModel.satieDirection }; } } }); }); model.stemX = function () { return (_this.overrideX || _this.x); }; model.staffIdx = baseModel.staffIdx; model.divCount = baseModel.divCount; model.satieLedger = baseModel.satieLedger; model.noteheadGlyph = baseModel.noteheadGlyph; model.satieMultipleRest = baseModel.satieMultipleRest; model.satieUnbeamedTuplet = baseModel.satieUnbeamedTuplet; return model; }; return Layout; }()); ChordModelImpl.Layout = Layout; Layout.prototype.expandPolicy = "after"; Layout.prototype.renderClass = document_1.Type.Chord; Layout.prototype.boundingBoxes = []; })(ChordModelImpl || (ChordModelImpl = {})); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = ChordModelImpl; var _a, _b;