UNPKG

satie

Version:

A sheet music renderer for the web

348 lines (347 loc) 16.9 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"; /** * @file Creates beams and tuplets */ var musicxml_interfaces_1 = require("musicxml-interfaces"); var lodash_1 = require("lodash"); var invariant = require("invariant"); var document_1 = require("./document"); var private_chordUtil_1 = require("./private_chordUtil"); /** * Lays out measures within a bar & justifies. * * @returns new end of line */ function beam(options, bounds, measures) { lodash_1.forEach(measures, function (measure) { // Note that the `number` property of beams does NOT differentiate between sets of beams, // as it does with e.g., ties. See `note.mod`. var activeBeams = {}; var activeUnbeamedTuplets = {}; var activeAttributes = null; // Invariant: measure.elements[i].length == measure.elements[j].length for all valid i, j. lodash_1.times(measure.elements[0].length, function (i) { lodash_1.forEach(measure.elements, function (elements) { var layout = elements[i]; var model = layout.model; if (model && layout.renderClass === document_1.Type.Attributes) { activeAttributes = model; } if (!model || layout.renderClass !== document_1.Type.Chord) { return; } var chord = model; var targetNote = lodash_1.find(chord, function (note) { return !!note.beams; }); var startTuplet; var stopTuplet; lodash_1.forEach(chord, function (note) { lodash_1.forEach(note.notations, function (notation) { lodash_1.forEach(notation.tuplets, function (aTuplet) { targetNote = targetNote || note; if (aTuplet.type === musicxml_interfaces_1.StartStop.Start) { startTuplet = aTuplet; } else { stopTuplet = aTuplet; } }); }); }); if (targetNote && targetNote.grace) { // TODO: grace notes return; } if (!targetNote) { lodash_1.forEach(chord, function (note) { if (!note || note.grace) { // TODO: grace notes return; } lodash_1.forEach(activeBeams[note.voice], function (beam, idx) { if (!beam) { return; } console.warn("Beam in voice %s, level %s was not explicitly closed " + "before another note was added.", note.voice, idx); }); }); return; } var beams = targetNote.beams, voice = targetNote.voice; if (!beams) { beams = []; } var toTerminate = []; var anyInvalid = lodash_1.some(lodash_1.sortBy(beams), function (beam, idx) { var expected = idx + 1; var actual = beam.number; if (expected !== actual) { console.warn("Invalid beam number"); // TODO: fix it return true; } return false; }); if (anyInvalid) { return; } if (!beams.length && startTuplet) { activeUnbeamedTuplets[voice] = activeUnbeamedTuplets[voice] || []; activeUnbeamedTuplets[voice][startTuplet.number || 1] = { number: startTuplet.number || 1, elements: [layout], initial: null, attributes: activeAttributes._snapshot, counts: [1], tuplet: startTuplet }; } else { lodash_1.forEach(activeUnbeamedTuplets[voice], function (unbeamedTuplet) { if (unbeamedTuplet) { unbeamedTuplet.elements.push(layout); } }); } if (stopTuplet && activeUnbeamedTuplets[voice] && activeUnbeamedTuplets[voice][stopTuplet.number || 1]) { toTerminate.push({ voice: voice, isUnbeamedTuplet: true, idx: stopTuplet.number || 1, beamSet: activeUnbeamedTuplets }); } lodash_1.chain(beams).sortBy("number").forEach(function (beam) { var idx = beam.number; invariant(!!idx, "A beam's number must be defined in MusicXML."); invariant(!!voice, "A beam's voice must be defined in MusicXML."); activeBeams[voice] = activeBeams[voice] || []; switch (beam.type) { case musicxml_interfaces_1.BeamType.Begin: case musicxml_interfaces_1.BeamType.BackwardHook: case musicxml_interfaces_1.BeamType.ForwardHook: activeBeams[voice] = activeBeams[voice] || []; if (activeBeams[voice][idx]) { console.warn("Beam at level %s in voice %s should have " + "been closed before being opened again.", idx, voice); terminateBeam$(voice, idx, activeBeams, false); } activeBeams[voice][idx] = { number: idx, elements: [layout], initial: beam, attributes: activeAttributes._snapshot, counts: [1], tuplet: startTuplet }; var counts = activeBeams[voice][1].counts; if (idx !== 1) { counts[counts.length - 1]++; } if (beam.type === musicxml_interfaces_1.BeamType.Begin) { break; } // Passthrough for BackwardHook and ForwardHook, which are single note things case musicxml_interfaces_1.BeamType.End: invariant(voice in activeBeams, "Cannot end non-existant beam " + "(no beam at all in current voice %s)", voice); invariant(idx in activeBeams[voice], "Cannot end non-existant " + "beam (no beam at level %s in voice %s)", idx, voice); activeBeams[voice][idx].elements.push(layout); counts = activeBeams[voice][1].counts; if (beam.type === musicxml_interfaces_1.BeamType.End) { if (idx === 1) { counts.push(1); } else { counts[counts.length - 1]++; } } toTerminate.push({ voice: voice, idx: idx, isUnbeamedTuplet: false, beamSet: activeBeams }); var groupTuplet = activeBeams[voice][idx].tuplet; if (groupTuplet && !stopTuplet) { // We optimisticly attached the tuplet to the beam, but it extends // beyond the beam. Detach the tuplet from the beam, and create an // unbeamed tuplet. activeBeams[voice][idx].tuplet = null; activeUnbeamedTuplets[voice] = activeUnbeamedTuplets[voice] || []; activeUnbeamedTuplets[voice][groupTuplet.number || 1] = { number: groupTuplet.number || 1, elements: activeBeams[voice][idx].elements.slice(), initial: null, attributes: activeBeams[voice][idx].attributes, counts: activeBeams[voice][idx].counts.slice(), tuplet: groupTuplet }; } break; case musicxml_interfaces_1.BeamType.Continue: invariant(voice in activeBeams, "Cannot continue non-existant beam (no beam at all " + "in current voice %s)", voice); invariant(idx in activeBeams[voice], "Cannot continue non-existant " + "beam (no beam at level %s in voice %s)", idx, voice); activeBeams[voice][idx].elements.push(layout); counts = activeBeams[voice][1].counts; if (idx === 1) { counts.push(1); } else { counts[counts.length - 1]++; } break; default: throw new Error("Unknown type " + beam.type); } }).value(); lodash_1.forEach(toTerminate, function (t) { return terminateBeam$(t.voice, t.idx, t.beamSet, t.isUnbeamedTuplet); }); }); }); lodash_1.forEach(activeBeams, function (beams, voice) { lodash_1.forEach(beams, function (beam, idx) { if (!beam) { return; } console.warn("Beam in voice %s, level %s was not closed before the " + "end of the measure.", voice, idx); terminateBeam$(parseInt(voice, 10), idx, activeBeams, false); }); }); }); return measures; } function terminateBeam$(voice, idx, beamSet, isUnbeamedTuplet) { if (isUnbeamedTuplet || idx === 1) { layoutBeam$(voice, idx, beamSet, isUnbeamedTuplet); } delete beamSet[voice][idx]; } function layoutBeam$(voice, idx, beamSet, isUnbeamedTuplet) { var beam = beamSet[voice][idx]; var chords = lodash_1.map(beam.elements, function (eLayout) { return eLayout.model; }); var firstChord = lodash_1.first(chords); var lastChord = lodash_1.last(chords); var clef = beam.attributes.clef; var firstAvgLine = private_chordUtil_1.averageLine(firstChord, clef); var lastAvgLine = private_chordUtil_1.averageLine(lastChord, clef); var avgLine = (firstAvgLine + lastAvgLine) / 2; var direction = avgLine >= 3 ? -1 : 1; // TODO: StemType should match this!! var Xs = []; var lines = []; lodash_1.forEach(beam.elements, function (layout, idx) { Xs.push(layout.x); lines.push(private_chordUtil_1.linesForClef(chords[idx], clef)); }); var line1 = private_chordUtil_1.startingLine(firstChord, direction, clef); var line2 = private_chordUtil_1.startingLine(lastChord, direction, clef); var slope = (line2 - line1) / (lodash_1.last(Xs) - lodash_1.first(Xs)) * 10; var stemHeight1 = 35; // Limit the slope to the range (-50, 50) if (slope > 0.5) { slope = 0.5; } if (slope < -0.5) { slope = -0.5; } var intercept = line1 * 10 + stemHeight1; function getStemHeight(direction, idx, line) { return intercept * direction + (direction === 1 ? 0 : 69) + slope * (Xs[idx] - lodash_1.first(Xs)) * direction - direction * line * 10; } // When the slope causes near-collisions, eliminate the slope. var minStemHeight = 1000; var incrementalIntercept = 0; lodash_1.forEach(chords, function (chord, idx) { var currHeightDeterminingLine = private_chordUtil_1.heightDeterminingLine(chord, direction, clef); var stemHeight = getStemHeight(direction, idx, currHeightDeterminingLine); if (stemHeight < minStemHeight) { minStemHeight = stemHeight; incrementalIntercept = direction * (30 - minStemHeight) + slope * (Xs[idx] - lodash_1.first(Xs)); } }); if (minStemHeight < 30) { intercept += incrementalIntercept; slope = 0; } if (slope === 0 && intercept >= 0 && intercept <= 50) { intercept += direction * (10 - (intercept % 10)); } var layouts = beam.elements; if (isUnbeamedTuplet) { var offsetY = direction > 0 ? -13 : -53; lodash_1.forEach(layouts, function (chordLayout, idx) { var stemStart = private_chordUtil_1.startingLine(chordLayout.model, direction, clef); var stemHeight = getStemHeight(direction, idx, stemStart); invariant(chords.length === 1 || isFinite(stemHeight), "stemHeight must be defined for 2+ notes"); chordLayout.satieStem = { direction: direction, stemStart: stemStart, stemHeight: stemHeight, tremolo: lodash_1.first(layouts).satieStem ? lodash_1.first(layouts).satieStem.tremolo : null, }; }); var tuplet = Object.create(beam.tuplet); tuplet.placement = direction > 0 ? musicxml_interfaces_1.AboveBelow.Above : musicxml_interfaces_1.AboveBelow.Below; var firstStem = lodash_1.first(layouts).satieStem; var lastStem = lodash_1.last(layouts).satieStem; firstChord.satieUnbeamedTuplet = { beamCount: null, direction: direction, x: Xs, y1: firstStem.stemStart * 10 + direction * firstStem.stemHeight + offsetY, y2: lastStem.stemStart * 10 + direction * lastStem.stemHeight + offsetY, tuplet: tuplet }; } else { lodash_1.forEach(layouts, function (chordLayout, idx) { var stemStart = private_chordUtil_1.startingLine(chordLayout.model, direction, clef); var stemHeight = getStemHeight(direction, idx, stemStart); chordLayout.satieStem = { direction: direction, stemStart: stemStart, stemHeight: stemHeight, tremolo: layouts[0].satieStem ? layouts[0].satieStem.tremolo : null, }; chordLayout.satieFlag = null; }); var firstStem = lodash_1.first(layouts).satieStem; var lastStem = lodash_1.last(layouts).satieStem; var firstLayout = lodash_1.first(beam.elements); firstLayout.satieBeam = { beamCount: lodash_1.times(Xs.length, function (idx) { return beam.counts[idx]; }), direction: direction, x: Xs, y1: firstStem.stemStart * 10 + direction * firstStem.stemHeight - 30, y2: lastStem.stemStart * 10 + direction * lastStem.stemHeight - 30, tuplet: beam.tuplet }; } } Object.defineProperty(exports, "__esModule", { value: true }); exports.default = beam;