UNPKG

satie

Version:

A sheet music renderer for the web

452 lines (451 loc) 18.5 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 _stretchRestRuleMemo = {}; function _stretchRestRule(rule, quantization) { var key = rule + String(quantization); if (!_stretchRestRuleMemo[key]) { var newRule = []; for (var i = 0; i < rule.length; ++i) { newRule.push(rule[i]); for (var j = 1; j < quantization; ++j) { newRule.push(rule[i] === "r" ? "_" : rule[i]); } } _stretchRestRuleMemo[key] = newRule.join(""); } return _stretchRestRuleMemo[key]; } function _getValidSubBeatLengths(quantumPerBeats, beatsPerMeasure, dotsAllowed) { var validLengths = []; for (var i = 5; i > 0; --i) { var pow2 = Math.pow(2, i); if (quantumPerBeats % pow2 === 0) { validLengths.push(quantumPerBeats / pow2); } var toAdd = quantumPerBeats / pow2; var dottedLength = quantumPerBeats / pow2; var dots = 0; if (dotsAllowed) { while ((toAdd % 2 === 0) && dots + 1 <= 3) { toAdd /= 2; dottedLength += toAdd; ++dots; validLengths.push(dottedLength); } } } // It's currently from smallest to largest, we want it from largest to smallest. validLengths.reverse(); return validLengths; } var RestSolver = (function () { function RestSolver(ruleBeats, ruleBeatType, restRules) { this._ruleBeats = ruleBeats; this._ruleBeatType = ruleBeatType; this._restRules = restRules; } RestSolver.prototype.isSimple = function () { return this._ruleBeats === 1 || this._ruleBeats === 2 || this._ruleBeats === 3 || this._ruleBeats === 4; }; RestSolver.prototype.checkRests = function (divisions, song, options) { var MATCH_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; var quantization = divisions * 4 / this._ruleBeatType; var myRestRules = this._restRules .filter(function (rule) { return options.dotsAllowed || (rule.search(RestSolver.dotRule) === -1); }) .map(function (rule) { return _stretchRestRule(rule, quantization); }); song = _stretchRestRule(song, 1 / quantization); var len = myRestRules[0].length; var quantumPerBeats = len / this._ruleBeats; var validSubBeatLengths = _getValidSubBeatLengths(quantumPerBeats, this._ruleBeats, options.dotsAllowed); var validNonDotSubBeatLengths = _getValidSubBeatLengths(quantumPerBeats, this._ruleBeats, false); if (song.length > len) { return "split " + len + " " + myRestRules[0].length; } else if (song.length < myRestRules[0].length) { return "apply " + len + " " + (song.split("").map(function () { return "."; }).join("") + Array(myRestRules[0].length - song.length + 1).join("r")); } else if (song.length !== len) { return "ERR: " + song.length + " !== " + this._ruleBeats; } // Ensure that rests that do not start on beats do not cross beat boundaries. for (var i = 0; i < song.length; ++i) { if (song[i] === "r" && (i % quantumPerBeats) > 0) { var j = i; while (song[j + 1] === "_") { ++j; } var beats = ((j - i + 1) / quantumPerBeats); if (beats >= 1) { var startOfNextBeat = i + 1; while (startOfNextBeat % quantumPerBeats !== 0) { ++startOfNextBeat; } var fillUntil = startOfNextBeat = startOfNextBeat + 1; while (fillUntil % quantumPerBeats !== 0 && song[fillUntil] && song[fillUntil] !== ".") { ++fillUntil; } var patch = Array(startOfNextBeat).join(".") + "r" + Array(fillUntil - startOfNextBeat + 1).join("_") + Array(song.length - fillUntil + 1).join("."); return "apply " + len + " " + patch; } } } // Apply rules var matches = new Array(song.length + 1).join(" ").split(""); for (var ruleNum = 0; ruleNum < myRestRules.length; ++ruleNum) { var rule = myRestRules[ruleNum]; for (var i = 0; i < rule.length; ++i) { if (rule[i] === "r" && song[i] === "r" && matches[i] === " ") { var ruleMatches = true; var ruleIsApplied = true; ruleMatch: for (var j = i + 1; j < rule.length; ++j) { if (rule[j] === "_" && song[j] === "_") { continue ruleMatch; } if (rule[j] !== "_") { if (song[j] === "_") { // New rule must start here. var k = j; while (song[k + 1] === "_") { ++k; } return "apply " + len + " " + (Array(j + 1).join(".") + "r" + Array(k - j + 1).join("_") + Array(song.length - k).join(".")); } break ruleMatch; } if (matches[j] !== " ") { ruleMatches = false; ruleIsApplied = false; break ruleMatch; } if (song[j] === "r") { ruleIsApplied = false; } else if (song[j] === ".") { ruleMatches = false; break ruleMatch; } else if (song[j] === "_") { throw new Error("Not reached"); } } // end ruleMatch if (ruleMatches && !ruleIsApplied) { return "apply " + len + " " + rule; } if (ruleMatches) { for (var j = 0; j < rule.length; ++j) { if (rule[j] !== ".") { if (matches[j] !== " ") { throw new Error("Double match."); } matches[j] = MATCH_CHARS[ruleNum]; } } } } else if (rule[i] === "*" && song[i] === "r" && matches[i] === " ") { if (this.isSimple()) { var bestSubbeatReplacement = ""; subbeatOptions: for (var j = 0; j < validSubBeatLengths.length; ++j) { var subbeatLength = validSubBeatLengths[j]; var k = i; while (rule[k + 1] === "*" && matches[k + 1] === " " && song[k + 1] !== ".") { ++k; } var matchedLength = k - i + 1; if (matchedLength >= subbeatLength) { // When it is preferable to show more clearly how the beat is divided, // divide the rest into half-beats. // Improvised extension: we apply this to quarters and eighths of a // beat as well. var isDotted = validNonDotSubBeatLengths.indexOf(subbeatLength) === -1; var atEnd = rule[i + subbeatLength] !== "*"; // XXX: make this a lint warning, instead of a requirement. var ruleSize = rule.split("*").length - 1; var ruleStart = rule.indexOf("*"); var dividesMiddle = false; for (var divisions_1 = 2; divisions_1 <= 16; divisions_1 *= 2) { for (var l = 1; l < divisions_1; ++l) { var rulePoint = ruleStart + l * ruleSize / divisions_1; if (i < rulePoint && (i + subbeatLength) > rulePoint) { dividesMiddle = true; } if (i < rulePoint && i > (rulePoint - ruleSize / divisions_1) && (i + subbeatLength) > rulePoint && (i + subbeatLength) < rulePoint + ruleSize / divisions_1) { continue subbeatOptions; } } } // Allow, but do not prefer, dotted rests at the end of a beat. // XXX: we should have a "lint warning" suggesting non-dotted at end. var isOptional = isDotted && (atEnd || dividesMiddle); for (var l = i + 1; l < i + subbeatLength; ++l) { if (song[l] === "r") { var pattern = bestSubbeatReplacement || (Array(i + 1).join(".") + "r" + Array(i + subbeatLength - i).join("_") + Array(song.length - (i + subbeatLength) + 1).join(".")); if (isOptional) { bestSubbeatReplacement = pattern; continue subbeatOptions; } return "apply " + len + " " + pattern; } } // Make sure the subbeat actually ends where it is supposed to. if (song[i + subbeatLength] === "_") { var l = i + subbeatLength; while (song[l] === "_") { ++l; } return "apply " + len + " " + (Array((i + subbeatLength) + 1).join(".") + "r" + Array(l - (i + subbeatLength)).join("_") + Array(song.length - l + 1).join(".")); } // Subbeat match. for (var l = i; l < i + subbeatLength; ++l) { matches[l] = MATCH_CHARS[ruleNum]; } break subbeatOptions; } } } else { subbeatOptions: for (var j = 0; j < validSubBeatLengths.length; ++j) { var subbeatLength = validSubBeatLengths[j]; var k = i; while (rule[k + 1] === "*" && matches[k + 1] === " " && song[k + 1] !== ".") { ++k; } var matchedLength = k - i + 1; if (matchedLength >= subbeatLength) { // Make sure the subbeat actually ends where it is supposed to. if (song[i + subbeatLength] === "_") { var l = i + subbeatLength; while (song[l] === "_") { ++l; } return "apply " + len + " " + (Array((i + subbeatLength) + 1).join(".") + "r" + Array(l - (i + subbeatLength)).join("_") + Array(song.length - l + 1).join(".")); } // Subbeat match. for (var l = i; l < i + subbeatLength; ++l) { matches[l] = MATCH_CHARS[ruleNum]; } break subbeatOptions; } } } // end subbeatOptions } } } ; // Continuations that aren't matched should become rests for (var i = 0; i < song.length; ++i) { if (matches[i] === " " && song[i] !== ".") { var j = i; while (song[j + 1] === "_") { ++j; } return "apply " + len + " " + (Array(i + 1).join(".") + "r" + Array(j - i + 1).join("_") + Array(song.length - j).join(".")); } } return "GOOD"; }; return RestSolver; }()); RestSolver.dotRule = /r__(\.|$)/; var REST_RULES_1 = Object.freeze([ "r", "*", ]); var REST_RULES_2 = Object.freeze([ "r_", "r.", ".r", "*.", ".*", ]); var REST_RULES_3 = Object.freeze([ "r__", "r..", ".r.", "..r", "*..", ".*.", "..*", ]); var REST_RULES_4 = Object.freeze([ "r___", "r_..", "..r_", "r...", ".r..", "..r.", "...r", "*...", ".*..", "..* ", "...*", ]); var REST_RULES_6 = Object.freeze([ "r_____", "r__...", "...r__", "r_....", "...r_.", "r.....", ".r....", "..r...", "...r..", "....r.", ".....r", "*.....", ".*....", "..*...", "...*..", "....*.", ".....*", ]); var REST_RULES_9 = Object.freeze([ "r________", "r__......", "...r__...", "......r__", "r_.......", "...r_....", "......r_.", "r........", ".r.......", "..r......", "...r.....", "....r....", ".....r...", "......r..", ".......r.", "........r", "*........", ".*.......", "..*......", "...*.....", "....*....", ".....*...", "......*..", ".......*.", "........*", ]); var REST_RULES_12 = Object.freeze([ "r___________", "r_____......", "......r_____", "r__.........", "...r__......", "......r__...", ".........r__", "r_..........", "...r_.......", "......r_....", ".........r_.", "r...........", ".r..........", "..r.........", "...r........", "....r.......", ".....r......", "......r.....", ".......r....", "........r...", ".........r..", "..........r.", "...........r", "*...........", ".*..........", "..*.........", "...*........", "....*.......", ".....*......", "......*.....", ".......*....", "........*...", ".........*..", "..........*.", ".......... *", ]); var TIME_SIGNATURES = Object.freeze({ "4/8": new RestSolver(4, 8, REST_RULES_4), "2/4": new RestSolver(2, 4, REST_RULES_2), "1/2": new RestSolver(1, 2, REST_RULES_1), "4/4": new RestSolver(4, 4, REST_RULES_4), "2/2": new RestSolver(2, 2, REST_RULES_2), "1/1": new RestSolver(1, 1, REST_RULES_1), "6/16": new RestSolver(6, 16, REST_RULES_6), "6/8": new RestSolver(6, 8, REST_RULES_6), "6/4": new RestSolver(6, 4, REST_RULES_6), "12/8": new RestSolver(12, 8, REST_RULES_12), "3/4": new RestSolver(3, 4, REST_RULES_3), "3/8": new RestSolver(3, 8, REST_RULES_3), "9/8": new RestSolver(9, 8, REST_RULES_9), }); /** * $timeSignatureName is a string like "4/4" or "6/8". * * A $song is a string in a song where $barLength divisions make up a bar. * A full $song is made up of $barLength characters. * The string contains three kinds of characters. * - 'r': The start of a beat * - '_': The continuation of a beat * - '.': A note * * See README.md for examples / tests. */ function checkRests(timeSignatureName, barLength, song, options) { var ts = TIME_SIGNATURES[timeSignatureName]; if (!ts) { return "ERR: No such time signature " + timeSignatureName + "."; } var numerator = parseInt(timeSignatureName.split("/")[0], 10); if (isNaN(numerator)) { return "ERR: No such time signature " + timeSignatureName; } var denominator = parseInt(timeSignatureName.split("/")[1], 10); if (isNaN(denominator)) { return "ERR: No such time signature " + timeSignatureName; } var divisions = barLength / numerator / 4 * denominator; if (divisions !== Math.round(divisions)) { return "ERR: Invalid bar length " + barLength + ". Divisions per quarter note must be an integer."; } return ts.checkRests(divisions, song, options); } Object.defineProperty(exports, "__esModule", { value: true }); exports.default = checkRests;