UNPKG

satie

Version:

A sheet music renderer for the web

540 lines (539 loc) 21.2 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 private_util_1 = require("./private_util"); var EMPTY_FROZEN = Object.freeze({}); function hasAccidental(chord, cursor) { return lodash_1.some(chord, function (c) { if (!c.pitch) { return false; } var accidental = (cursor.staffAccidentals[c.pitch.alter] || 0); return ((c.pitch.alter || 0) !== accidental && (c.pitch.alter || 0) !== (cursor.staffAccidentals[c.pitch.alter + c.pitch.octave] || 0) || !!c.accidental); }); } exports.hasAccidental = hasAccidental; function _isIDurationDescription(chord) { return !isNaN(chord.count); } function _isIChord(chord) { return !isNaN(chord.length); } function count(chord) { if (_isIChord(chord)) { var target = lodash_1.find(chord, function (note) { return note.noteType; }); return target ? target.noteType.duration : NaN; } else if (_isIDurationDescription(chord)) { return chord.count; } else { invariant(false, "count() expected a chord or duration."); } } exports.count = count; function dots(chord) { if (_isIChord(chord)) { return (lodash_1.find(chord, function (note) { return note.dots; }) || { dots: [] }).dots.length || 0; } else if (_isIDurationDescription(chord)) { return chord.dots || 0; } else { invariant(false, "dots() expected a chord or duration"); } } exports.dots = dots; function timeModification(chord) { if (_isIChord(chord)) { return (lodash_1.find(chord, function (note) { return note.timeModification; }) || { timeModification: null }) .timeModification || null; } else if (_isIDurationDescription(chord)) { return chord.timeModification || null; } else { invariant(false, "timeModification() expected a chord or duration"); } } exports.timeModification = timeModification; function ties(chord) { var ties = lodash_1.map(chord, function (note) { return note.ties && note.ties.length ? note.ties[0] : null; }); return lodash_1.filter(ties, function (t) { return !!t; }).length ? ties : null; } exports.ties = ties; function beams(chord) { var target = lodash_1.find(chord, function (note) { return !!note.beams; }); if (target) { return target.beams; } return null; } exports.beams = beams; function hasFlagOrBeam(chord) { // TODO: check if flag/beam forcefully set to "off" return lodash_1.some(chord, function (note) { return note.noteType.duration <= musicxml_interfaces_1.Count.Eighth; }); } exports.hasFlagOrBeam = hasFlagOrBeam; /** * Returns the mean of all the lines, in SMuFL coordinates, where * 3 is the middle line. (SMuFL coordinates are 10x MusicXML coordinates) */ function averageLine(chord, clef) { return lodash_1.reduce(linesForClef(chord, clef), function (memo, line) { return memo + line; }, 0) / chord.length; } exports.averageLine = averageLine; /** * Returns the minimum of all the lines, in SMuFL coordinates, where * 3 is the middle line. (SMuFL coordinates are 10x MusicXML coordinates) */ function lowestLine(chord, clef) { return lodash_1.reduce(linesForClef(chord, clef), function (memo, line) { return Math.min(memo, line); }, 10000); } exports.lowestLine = lowestLine; /** * Returns the highest of all the lines, in SMuFL coordinates, where * 3 is the middle line. (SMuFL coordinates are 10x MusicXML coordinates) */ function highestLine(chord, clef) { return lodash_1.reduce(linesForClef(chord, clef), function (memo, line) { return Math.max(memo, line); }, -10000); } exports.highestLine = highestLine; /** * Returns the position where the line starts. For single notes, this is where * the notehead appears. For chords, this is where the furthest notehead appears. */ function startingLine(chord, direction, clef) { if (direction !== -1 && direction !== 1) { throw new Error("Direction was not a number"); } return direction === 1 ? lowestLine(chord, clef) : highestLine(chord, clef); } exports.startingLine = startingLine; /** * The line of the notehead closest to the dangling end of the stem. For single notes, * startingLine and heightDeterminingLine are equal. * * Note: The minimum size of a stem is determinted by this value. */ function heightDeterminingLine(chord, direction, clef) { if (direction !== -1 && direction !== 1) { throw new Error("Direction was not a number"); } return direction === 1 ? highestLine(chord, clef) : lowestLine(chord, clef); } exports.heightDeterminingLine = heightDeterminingLine; function linesForClef(chord, clef) { if (!clef) { throw new Error("Exepected a valid clef"); } return lodash_1.map(chord, function (note) { return lineForClef(note, clef); }); } exports.linesForClef = linesForClef; ; function lineForClef(note, clef) { if (!clef) { throw new Error("Exepected a valid clef"); } if (!note) { return 3; } else if (!!note.rest) { if (note.rest.displayStep) { return lineForClef_(note.rest.displayStep, note.rest.displayOctave, clef); } else if (note.noteType.duration === musicxml_interfaces_1.Count.Whole) { return 4; } else { return 3; } } else if (!!note.unpitched) { return lineForClef_(note.unpitched.displayStep, note.unpitched.displayOctave, clef); } else if (!!note.pitch) { return lineForClef_(note.pitch.step, note.pitch.octave, clef); } else { throw new Error("Invalid note"); } } exports.lineForClef = lineForClef; exports.offsetToPitch = { 0: "C", 0.5: "D", 1: "E", 1.5: "F", 2: "G", 2.5: "A", 3: "B" }; exports.pitchOffsets = { C: 0, D: 0.5, E: 1, F: 1.5, G: 2, A: 2.5, B: 3 }; function pitchForClef(relativeY, clef) { var line = relativeY / 10 + 3; var clefOffset = getClefOffset(clef); var offset2x = Math.round((line - clefOffset) * 2); var octave = Math.floor(offset2x / 7) + 3; var stepQuant = (Math.round(offset2x + 7 * 1000) % 7) / 2; if (stepQuant === 3.5) { octave = octave + 1; stepQuant = 0; } var step = exports.offsetToPitch[stepQuant]; return { octave: octave, step: step }; } exports.pitchForClef = pitchForClef; function lineForClef_(step, octave, clef) { var octaveNum = (parseInt(octave, 10) || 0); return getClefOffset(clef) + (octaveNum - 3) * 3.5 + exports.pitchOffsets[step]; } exports.lineForClef_ = lineForClef_; /** * Returns true if a ledger line is needed, and false otherwise. * Will be changed once staves with > 5 lines are available. */ function onLedger(note, clef) { if (!note || note.rest || note.unpitched) { return false; } var line = lineForClef(note, clef); return line < 0.5 || line > 5.5; } exports.onLedger = onLedger; function ledgerLines(chord, clef) { var low = lowestLine(chord, clef); var high = highestLine(chord, clef); var lines = []; for (var i = 6; i <= high; ++i) { lines.push(i); } for (var i = 0; i >= low; --i) { lines.push(i); } return lines; } exports.ledgerLines = ledgerLines; function rest(chord) { return !chord.length || chord[0].rest; } exports.rest = rest; exports.defaultClefLines = { G: 2, F: 4, C: 3, PERCUSSION: 3, TAB: 5, NONE: 3 }; exports.clefOffsets = { G: -3.5, F: 2.5, C: -0.5, PERCUSSION: -0.5, TAB: -0.5, NONE: -0.5 }; function getClefOffset(clef) { return exports.clefOffsets[clef.sign] + clef.line - exports.defaultClefLines[clef.sign.toUpperCase()] - 3.5 * parseInt(clef.clefOctaveChange || "0", 10); } exports.getClefOffset = getClefOffset; function barDivisionsDI(time, divisions) { invariant(!!divisions, "Expected divisions to be set before calculating bar divisions."); if (time.senzaMisura != null) { return 1000000 * divisions; } var quarterNotes = lodash_1.reduce(time.beats, function (memo, timeStr, idx) { return memo + lodash_1.reduce(timeStr.split("+"), function (memo, timeStr) { return memo + parseInt(timeStr, 10) * 4 / time.beatTypes[idx]; }, 0); }, 0); return quarterNotes * divisions || NaN; } exports.barDivisionsDI = barDivisionsDI; function barDivisions(_a) { var time = _a.time, divisions = _a.divisions; return barDivisionsDI(time, divisions); } exports.barDivisions = barDivisions; exports.IDEAL_STEM_HEIGHT = 35; exports.MIN_STEM_HEIGHT = 25; exports.chromaticScale = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; // c:12 exports.countToHasStem = { 0.25: true, 0.5: false, 1: false, 2: true, 4: true, 8: true, 16: true, 32: true, 64: true, 128: true, 256: true, 512: true, 1024: true }; exports.countToIsBeamable = { 8: true, 16: true, 32: true, 64: true, 128: true, 256: true, 512: true, 1024: true }; exports.countToFlag = { 8: "flag8th", 16: "flag16th", 32: "flag32nd", 64: "flag64th", 128: "flag128th", 256: "flag256th", 512: "flag512th", 1024: "flag1024th" }; exports.accidentalGlyphs = (_a = {}, _a[musicxml_interfaces_1.MxmlAccidental.NaturalFlat] = "accidentalNaturalSharp", _a[musicxml_interfaces_1.MxmlAccidental.SharpUp] = "accidentalThreeQuarterTonesSharpArrowUp", _a[musicxml_interfaces_1.MxmlAccidental.ThreeQuartersFlat] = "accidentalThreeQuarterTonesFlatZimmermann", _a[musicxml_interfaces_1.MxmlAccidental.ThreeQuartersSharp] = "accidentalThreeQuarterTonesSharpStein", _a[musicxml_interfaces_1.MxmlAccidental.QuarterFlat] = "accidentalQuarterToneFlatStein", _a[musicxml_interfaces_1.MxmlAccidental.Flat] = "accidentalFlat", _a[musicxml_interfaces_1.MxmlAccidental.TripleSharp] = "accidentalTripleSharp", _a[musicxml_interfaces_1.MxmlAccidental.Flat1] = null, _a[musicxml_interfaces_1.MxmlAccidental.Flat2] = null, _a[musicxml_interfaces_1.MxmlAccidental.Flat3] = null, _a[musicxml_interfaces_1.MxmlAccidental.Flat4] = null, _a[musicxml_interfaces_1.MxmlAccidental.Flat5] = null, _a[musicxml_interfaces_1.MxmlAccidental.Sharp1] = null, _a[musicxml_interfaces_1.MxmlAccidental.Sharp2] = null, _a[musicxml_interfaces_1.MxmlAccidental.Sharp3] = null, _a[musicxml_interfaces_1.MxmlAccidental.Sharp4] = null, _a[musicxml_interfaces_1.MxmlAccidental.Sharp5] = null, _a[musicxml_interfaces_1.MxmlAccidental.SlashQuarterSharp] = null, _a[musicxml_interfaces_1.MxmlAccidental.DoubleSlashFlat] = null, _a[musicxml_interfaces_1.MxmlAccidental.TripleFlat] = "accidentalTripleFlat", _a[musicxml_interfaces_1.MxmlAccidental.Sharp] = "accidentalSharp", _a[musicxml_interfaces_1.MxmlAccidental.QuarterSharp] = "accidentalQuarterToneSharpStein", _a[musicxml_interfaces_1.MxmlAccidental.SlashFlat] = "accidentalTavenerFlat", _a[musicxml_interfaces_1.MxmlAccidental.FlatDown] = "accidentalFlatJohnstonDown", _a[musicxml_interfaces_1.MxmlAccidental.NaturalDown] = "accidentalQuarterToneFlatNaturalArrowDown", _a[musicxml_interfaces_1.MxmlAccidental.SharpSharp] = "accidentalSharpSharp", _a[musicxml_interfaces_1.MxmlAccidental.FlatUp] = "accidentalFlatJohnstonUp", _a[musicxml_interfaces_1.MxmlAccidental.DoubleSharp] = "accidentalDoubleSharp", _a[musicxml_interfaces_1.MxmlAccidental.Sori] = "accidentalSori", _a[musicxml_interfaces_1.MxmlAccidental.SharpDown] = "accidentalQuarterToneSharpArrowDown", _a[musicxml_interfaces_1.MxmlAccidental.Koron] = "accidentalKoron", _a[musicxml_interfaces_1.MxmlAccidental.NaturalUp] = "accidentalQuarterToneSharpNaturalArrowUp", _a[musicxml_interfaces_1.MxmlAccidental.SlashSharp] = "accidentalTavenerSharp", _a[musicxml_interfaces_1.MxmlAccidental.NaturalSharp] = "accidentalNaturalSharp", _a[musicxml_interfaces_1.MxmlAccidental.FlatFlat] = "accidentalDoubleFlat", _a[musicxml_interfaces_1.MxmlAccidental.Natural] = "accidentalNatural", _a[musicxml_interfaces_1.MxmlAccidental.DoubleFlat] = "accidentalDoubleFlat", _a); exports.InvalidAccidental = -999; var CUSTOM_NOTEHEADS = (_b = {}, _b[musicxml_interfaces_1.NoteheadType.ArrowDown] = [ "noteheadLargeArrowDownBlack", "noteheadLargeArrowDownHalf", "noteheadLargeArrowDownWhole", "noteheadLargeArrowDownDoubleWhole" ], _b[musicxml_interfaces_1.NoteheadType.ArrowUp] = ["noteheadLargeArrowUpBlack", "noteheadLargeArrowUpHalf", "noteheadLargeArrowUpWhole", "noteheadLargeArrowUpDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.BackSlashed] = ["noteheadSlashedBlack2", "noteheadSlashedHalf2", "noteheadSlashedWhole2", "noteheadSlashedDoubleWhole2"], _b[musicxml_interfaces_1.NoteheadType.CircleDot] = ["noteheadRoundWhiteWithDot", "noteheadCircledHalf", "noteheadCircledWhole", "noteheadCircledDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.CircleX] = ["noteheadCircledXLarge", "noteheadCircledXLarge", "noteheadCircledXLarge", "noteheadCircledXLarge"], _b[musicxml_interfaces_1.NoteheadType.Cluster] = ["noteheadNull", "noteheadNull", "noteheadNull", "noteheadNull"], _b[musicxml_interfaces_1.NoteheadType.Cross] = ["noteheadPlusBlack", "noteheadPlusHalf", "noteheadPlusWhole", "noteheadPlusDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.InvertedTriangle] = [ "noteheadTriangleDownBlack", "noteheadTriangleDownHalf", "noteheadTriangleDownWhole", "noteheadTriangleDownDoubleWhole" ], _b[musicxml_interfaces_1.NoteheadType.LeftTriangle] = [ "noteheadTriangleRightBlack", "noteheadTriangleRightHalf", "noteheadTriangleRightWhole", "noteheadTriangleRightDoubleWhole" ], // Finale has a different idea about what left means _b[musicxml_interfaces_1.NoteheadType.None] = [ "noteheadNull", "noteheadNull", "noteheadNull", "noteheadNull" ], _b[musicxml_interfaces_1.NoteheadType.Slash] = ["noteheadSlashHorizontalEnds", "noteheadSlashWhiteHalf", "noteheadSlashWhiteWhole", "noteheadDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Slashed] = ["noteheadSlashedBlack1", "noteheadSlashedHalf1", "noteheadSlashedWhole1", "noteheadSlashedDoubleWhole1"], _b[musicxml_interfaces_1.NoteheadType.X] = ["noteheadXBlack", "noteheadXHalf", "noteheadXWhole", "noteheadXDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Do] = ["noteheadTriangleUpBlack", "noteheadTriangleUpHalf", "noteheadTriangleUpWhole", "noteheadTriangleUpDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Triangle] = ["noteheadTriangleUpBlack", "noteheadTriangleUpHalf", "noteheadTriangleUpWhole", "noteheadTriangleUpDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Re] = ["noteheadMoonBlack", "noteheadMoonWhite", "noteheadMoonWhite", "noteheadMoonWhite"], _b[musicxml_interfaces_1.NoteheadType.Mi] = ["noteheadDiamondBlack", "noteheadDiamondHalf", "noteheadDiamondWhole", "noteheadDiamondDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Diamond] = ["noteheadDiamondBlack", "noteheadDiamondHalf", "noteheadDiamondWhole", "noteheadDiamondDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.Fa] = ["noteheadTriangleUpRightBlack", "noteheadTriangleUpRightWhite", "noteheadTriangleUpRightWhite", "noteheadTriangleUpRightWhite"], _b[musicxml_interfaces_1.NoteheadType.FaUp] = [ "noteheadTriangleUpRightBlack", "noteheadTriangleUpRightWhite", "noteheadTriangleUpRightWhite", "noteheadTriangleUpRightWhite" ], _b[musicxml_interfaces_1.NoteheadType.So] = ["noteheadBlack", "noteheadHalf", "noteheadWhole", "noteheadDoubleWhole"], _b[musicxml_interfaces_1.NoteheadType.La] = ["noteheadSquareBlack", "noteheadSquareWhite", "noteheadSquareWhite", "noteheadSquareWhite"], _b[musicxml_interfaces_1.NoteheadType.Square] = ["noteheadSquareBlack", "noteheadSquareWhite", "noteheadSquareWhite", "noteheadSquareWhite"], _b[musicxml_interfaces_1.NoteheadType.Rectangle] = ["noteheadSquareBlack", "noteheadSquareWhite", "noteheadSquareWhite", "noteheadSquareWhite"], _b[musicxml_interfaces_1.NoteheadType.Ti] = [ "noteheadTriangleRoundDownBlack", "noteheadTriangleRoundDownWhite", "noteheadTriangleRoundDownWhite", "noteheadTriangleRoundDownWhite" ], _b); function getNoteheadGlyph(notehead, stdGlyph) { var type = notehead ? notehead.type : musicxml_interfaces_1.NoteheadType.Normal; if (type === musicxml_interfaces_1.NoteheadType.Normal) { return stdGlyph; } else { var noteheads = CUSTOM_NOTEHEADS[type]; if (noteheads) { if (noteheads[0] && stdGlyph === "noteheadBlack") { return noteheads[0]; } else if (noteheads[1] && stdGlyph === "noteheadHalf") { return noteheads[1]; } else if (noteheads[2] && stdGlyph === "noteheadWhole") { return noteheads[2]; } else if (noteheads[3] && stdGlyph === "noteheadDoubleWhole") { return noteheads[3]; } } } console.warn("The custom notehead with ID " + type + " cannot replace " + (this.props.notehead + ", probably because it's not implemented.")); return this.props.notehead; } exports.getNoteheadGlyph = getNoteheadGlyph; function notationObj(n) { invariant(!n.notations || n.notations.length === 1, "Deprecated notations format"); return n.notations ? n.notations[0] : EMPTY_FROZEN; } exports.notationObj = notationObj; function articulationObj(n) { return notationObj(n).articulations ? notationObj(n).articulations[0] : Object.freeze({}); } exports.articulationObj = articulationObj; function tieds(n) { return lodash_1.chain(n) .map(function (n) { return notationObj(n).tieds; }) .map(function (t) { return t && t.length ? t[0] : null; }) .value(); } exports.tieds = tieds; var FractionalDivisionsException = (function () { function FractionalDivisionsException(requiredDevisions) { this.requiredDivisions = requiredDevisions; } return FractionalDivisionsException; }()); exports.FractionalDivisionsException = FractionalDivisionsException; function divisions(chord, attributes, allowFractional) { if (_isIChord(chord) && lodash_1.some(chord, function (note) { return note.grace; })) { return 0; } var chordCount = count(chord); var chordDots = dots(chord); var chordTM = timeModification(chord); var attributesTime = attributes.time.senzaMisura === undefined ? attributes.time : { beatTypes: [4], beats: ["1000"], }; var attributeDivisions = attributes.divisions; invariant(!!attributesTime, "A time signature must be specified."); if (chordCount === -1 || chordCount <= 1) { // TODO: What if beatType isn't consistent? return attributeDivisions * lodash_1.reduce(attributesTime.beats, function (memo, durr) { return memo + lodash_1.reduce(durr.split("+"), function (m, l) { return m + parseInt(l, 10); }, 0); }, 0); } if ((attributeDivisions * 4) % chordCount > 0 && !allowFractional) { var newDivisions = private_util_1.lcm(attributeDivisions * 4, chordCount) / 4; throw new FractionalDivisionsException(newDivisions); } var base = (attributeDivisions * 4) / chordCount; var tmFactor = chordTM ? chordTM.normalNotes / chordTM.actualNotes : 1.0; var dotFactor = lodash_1.times(chordDots, function (d) { return 1 / Math.pow(2, d + 1); }) .reduce(function (m, i) { return m + i; }, 1); var total = base * tmFactor * dotFactor; invariant(!isNaN(total), "calcDivisions must return a number. %s is not a number.", total); return total; } exports.divisions = divisions; var _a, _b;