UNPKG

satie

Version:

A sheet music renderer for the web

228 lines (227 loc) 10 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 invariant = require("invariant"); var lodash_1 = require("lodash"); var private_metre_checkRests_1 = require("./private_metre_checkRests"); var private_metre_getTSString_1 = require("./private_metre_getTSString"); var D = require("./private_metre_metreDurations"); var private_chordUtil_1 = require("./private_chordUtil"); function voiceToRestSpec(segment, attributes, factory) { var emptyRestSpec = { song: "", models: [], modelsToKill: [] }; var divsToSuppress = 0; var killIdx; var prevIdx = 0; var spec = segment.reduce(function (restSpec, model) { var divCount = model.newDivisions || 0; var oldDivCount = model.previousDivisions || 0; var restsAtEnd = model.previousDivisions > divCount ? "r" + lodash_1.times(model.previousDivisions - divCount - 1, function () { return "_"; }).join("") : ""; var modelsToKill = restSpec.modelsToKill; if (divsToSuppress > 0) { var extraSong = divCount > divsToSuppress ? "r" + lodash_1.times(divCount - divsToSuppress - 1, function () { return "_"; }).join("") : ""; divsToSuppress = Math.max(0, divsToSuppress - divCount); var newModelsToKill = modelsToKill.slice(); newModelsToKill[killIdx] = newModelsToKill[killIdx] || []; newModelsToKill[killIdx].push(model); return { song: restSpec.song + extraSong, models: restSpec.models.concat(lodash_1.times(extraSong.length, function () { return "killed"; })), modelsToKill: newModelsToKill, }; } else if (divCount === 0) { var newModelsToKill = modelsToKill.slice(); newModelsToKill[prevIdx] = newModelsToKill[prevIdx] || []; newModelsToKill[prevIdx].push(model); return { song: restSpec.song, models: restSpec.models, modelsToKill: newModelsToKill, }; } if (divCount) { prevIdx = restSpec.models.length + divCount; } var models = restSpec.models.concat(lodash_1.times(divCount, function () { return model; })).concat(restsAtEnd.split("").map(function () { return null; })); if (divCount > oldDivCount) { killIdx = models.length; divsToSuppress = divCount - oldDivCount; } if (model.rest && divCount) { return { song: restSpec.song + "r" + lodash_1.times(divCount - 1, function () { return "_"; }).join("") + restsAtEnd, models: models, modelsToKill: modelsToKill, }; } return { song: restSpec.song + lodash_1.times(divCount, function () { return "."; }).join("") + restsAtEnd, models: models, modelsToKill: modelsToKill, }; }, emptyRestSpec); invariant(spec.models.length === spec.song.length, "Invalid spec"); var totalDivisions = private_chordUtil_1.barDivisions(attributes); var restsToAdd = (totalDivisions - (spec.song.length % totalDivisions)) % totalDivisions; return { song: restsToAdd ? spec.song.concat("r" + lodash_1.times(restsToAdd - 1, function () { return "_"; }).join("")) : spec.song, models: spec.models, modelsToKill: spec.modelsToKill, }; } exports.voiceToRestSpec = voiceToRestSpec; function _cleanupRests(pattern, time) { // Now, call "checkRests" and apply the var ts = private_metre_getTSString_1.default(time); var next = function () { return private_metre_checkRests_1.default(ts, pattern.length, pattern, { dotsAllowed: true }); }; var operationsRemaining = 15; for (var status_1 = next(); status_1 !== "GOOD"; status_1 = next()) { if (!--operationsRemaining) { throw new Error("Rest cleanup timeout"); } // Apply patches until we're in a good state. var cmd = status_1.split(" "); invariant(cmd[0] === "apply", "Unexpected instruction '%s'", status_1); invariant(parseInt(cmd[1], 10) === pattern.length, "Unexpected length change from %s to %s", cmd[1], pattern.length); var patch = cmd[2]; var nextPattern = ""; for (var i = 0; i < pattern.length; ++i) { if (patch[i] === ".") { if (nextPattern[i] !== "." && i && patch[i - 1] !== ".") { nextPattern += "r"; } else { nextPattern += pattern[i]; } } else { nextPattern += patch[i]; } } pattern = nextPattern; } return pattern; } function simplifyRests(segment, factory, attributes) { var originalSpec = voiceToRestSpec(segment, attributes, factory); // Correct the rests. var totalDivisions = private_chordUtil_1.barDivisions(attributes); var cleanRestPattern = ""; for (var i = 0; i < originalSpec.song.length; i += totalDivisions) { cleanRestPattern += _cleanupRests(originalSpec.song.slice(i, i + totalDivisions), attributes.time); } // We now need to make patches to turn originalSpec.song into cleanRestPattern. var patches = []; var currIdx = -1; function killModel(model) { if (!model.newDivisions) { ++currIdx; } else { invariant(segment.indexOf(model) > -1, "Model must be present in segment"); patches.push({ ld: model.toSpec(), p: [currIdx + 1], }); --currIdx; } } for (var i = 0; i < cleanRestPattern.length; ++i) { var originalModel = originalSpec.models[i]; lodash_1.forEach(originalSpec.modelsToKill[i], killModel); if (originalModel && originalModel !== originalSpec.models[i - 1]) { ++currIdx; } if (cleanRestPattern[i] === "r") { var cleanRestEnd = i + 1; while (cleanRestPattern[cleanRestEnd] === "_") { ++cleanRestEnd; } var newDuration = D.makeDuration(totalDivisions / parseInt(attributes.time.beats[0], 10) * (4 / attributes.time.beatTypes[0]), attributes.time, cleanRestEnd - i); if (originalSpec.song[i] === "r" || originalModel === "killed" || !originalModel) { // Check if the length of the rest needs to be changed var originalRestEnd = i + 1; while (originalSpec.song[originalRestEnd] === "_") { ++originalRestEnd; } if (!originalModel || originalModel === "killed") { ++currIdx; newDuration[0].rest = newDuration[0].rest || {}; newDuration._class = "Chord"; patches.push({ li: newDuration, p: [currIdx], }); } else if (cleanRestEnd !== originalRestEnd) { if (!originalModel.rest) { throw new Error("Expected rest"); } var newDots = lodash_1.times(originalModel.newDots, function () { return ({}); }); if (JSON.stringify(newDots) !== JSON.stringify(newDuration[0].dots)) { patches.push({ od: newDots, oi: newDuration[0].dots, p: [currIdx, "notes", 0, "dots"], }); } if (originalModel.newCount !== newDuration[0].noteType.duration) { patches.push({ od: originalModel.newCount, oi: newDuration[0].noteType.duration, p: [currIdx, "notes", 0, "noteType", "duration"], }); } } } else { // New rest ++currIdx; newDuration[0].rest = newDuration[0].rest || {}; newDuration._class = "Chord"; patches.push({ li: newDuration, p: [currIdx], }); } } else if (cleanRestPattern[i] === "_" || cleanRestPattern[i] === ".") { if (originalSpec.song[i] === "r") { var model = originalSpec.models[i]; if (model === "killed") { throw new Error("Not reached"); } invariant(!!model, "Cannot remove undefined model"); invariant(segment.indexOf(model) > -1, "Model must be present in segment"); patches.push({ ld: model.toSpec(), p: [currIdx], }); --currIdx; } } } lodash_1.forEach(originalSpec.modelsToKill[originalSpec.models.length], killModel); return patches; } exports.simplifyRests = simplifyRests;