UNPKG

satie

Version:

A sheet music renderer for the web

259 lines (233 loc) 10.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/>. */ import {Time} from "musicxml-interfaces"; import {IAny} from "musicxml-interfaces/operations"; import * as invariant from "invariant"; import {times, forEach} from "lodash"; import {Document} from "./document"; import {ModelMetreMutationSpec} from "./engine_createPatch"; import {IFactory} from "./private_factory"; import {IAttributesSnapshot} from "./private_attributesSnapshot"; import checkRests from "./private_metre_checkRests"; import getTSString from "./private_metre_getTSString"; import * as D from "./private_metre_metreDurations"; import {barDivisions} from "./private_chordUtil"; export interface IRestSpec { readonly song: string; readonly models: (ModelMetreMutationSpec | "killed")[]; readonly modelsToKill: ModelMetreMutationSpec[][]; } export function voiceToRestSpec(segment: ModelMetreMutationSpec[], attributes: IAttributesSnapshot, factory: IFactory | Document) { const emptyRestSpec: IRestSpec = {song: "", models: [], modelsToKill: []}; let divsToSuppress = 0; let killIdx: number; let prevIdx: number = 0; let spec = segment.reduce((restSpec: IRestSpec, model: ModelMetreMutationSpec) => { let divCount = model.newDivisions || 0; let oldDivCount = model.previousDivisions || 0; const restsAtEnd = model.previousDivisions > divCount ? "r" + times(model.previousDivisions - divCount - 1, () => "_").join("") : ""; const modelsToKill = restSpec.modelsToKill; if (divsToSuppress > 0) { const extraSong = divCount > divsToSuppress ? "r" + times(divCount - divsToSuppress - 1, () => "_").join("") : ""; divsToSuppress = Math.max(0, divsToSuppress - divCount); let newModelsToKill = modelsToKill.slice(); newModelsToKill[killIdx] = newModelsToKill[killIdx] || []; newModelsToKill[killIdx].push(model); return { song: restSpec.song + extraSong, models: restSpec.models.concat(times(extraSong.length, () => "killed") as any), modelsToKill: newModelsToKill, }; } else if (divCount === 0) { let 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; } const models = restSpec.models.concat(times(divCount, () => model)).concat( restsAtEnd.split("").map(() => null)); if (divCount > oldDivCount) { killIdx = models.length; divsToSuppress = divCount - oldDivCount; } if (model.rest && divCount) { return { song: restSpec.song + "r" + times(divCount - 1, () => "_").join("") + restsAtEnd, models, modelsToKill, }; } return { song: restSpec.song + times(divCount, () => ".").join("") + restsAtEnd, models, modelsToKill, }; }, emptyRestSpec); invariant(spec.models.length === spec.song.length, "Invalid spec"); const totalDivisions = barDivisions(attributes); const restsToAdd = (totalDivisions - (spec.song.length % totalDivisions)) % totalDivisions; return { song: restsToAdd ? spec.song.concat("r" + times(restsToAdd - 1 , () => "_").join("")) : spec.song, models: spec.models, modelsToKill: spec.modelsToKill, }; } function _cleanupRests(pattern: string, time: Time) { // Now, call "checkRests" and apply the const ts = getTSString(time); const next = () => checkRests(ts, pattern.length, pattern, {dotsAllowed: true}); let operationsRemaining = 15; for (let status = next(); status !== "GOOD"; status = next()) { if (!--operationsRemaining) { throw new Error("Rest cleanup timeout"); } // Apply patches until we're in a good state. const cmd = status.split(" "); invariant(cmd[0] === "apply", "Unexpected instruction '%s'", status); invariant(parseInt(cmd[1], 10) === pattern.length, "Unexpected length change from %s to %s", cmd[1], pattern.length); const patch = cmd[2]; let nextPattern = ""; for (let 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; } export function simplifyRests( segment: (ModelMetreMutationSpec)[], factory: IFactory | Document, attributes: IAttributesSnapshot): IAny[] { const originalSpec = voiceToRestSpec(segment, attributes, factory); // Correct the rests. const totalDivisions = barDivisions(attributes); let cleanRestPattern = ""; for (let 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. let patches: IAny[] = []; let currIdx = -1; function killModel(model: ModelMetreMutationSpec) { if (!model.newDivisions) { ++currIdx; } else { invariant(segment.indexOf(model) > -1, "Model must be present in segment"); patches.push({ ld: model.toSpec(), p: [currIdx + 1], } as any); --currIdx; } } for (let i = 0; i < cleanRestPattern.length; ++i) { const originalModel = originalSpec.models[i]; forEach(originalSpec.modelsToKill[i], killModel); if (originalModel && originalModel !== originalSpec.models[i - 1]) { ++currIdx; } if (cleanRestPattern[i] === "r") { let cleanRestEnd = i + 1; while (cleanRestPattern[cleanRestEnd] === "_") { ++cleanRestEnd; } const 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 let 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"); } const newDots = times(originalModel.newDots, () => ({})); 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") { const 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], } as any); --currIdx; } } } forEach(originalSpec.modelsToKill[originalSpec.models.length], killModel); return patches; }