UNPKG

satie

Version:

A sheet music renderer for the web

492 lines (424 loc) 17.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 {Note, Chord, Rest, Dot, Type, TimeModification, Pitch, Unpitched, NoteheadText, Accidental, Instrument, Lyric, Notations, Stem, Cue, Tie, Play, Grace, Notehead, Beam, NormalBold, NormalItalic, Level, Footnote, Articulations, AccidentalMark, Arpeggiate, Dynamics, Fermata, Glissando, NonArpeggiate, Ornaments, OtherNotation, Slide, Slur, Technical, Tied, Tuplet, MxmlAccidental, serializeNote} from "musicxml-interfaces"; import {forEach, reduce, map, isEqual} from "lodash"; import * as invariant from "invariant"; import {IReadOnlyValidationCursor} from "./private_cursor"; import {accidentalGlyphs, onLedger, InvalidAccidental, lineForClef} from "./private_chordUtil"; import {bboxes as glyphBBoxes} from "./private_smufl"; import {cloneObject} from "./private_util"; import ChordModelImpl from "./implChord_chordImpl"; /** * Represents a note in a ChordImpl. * * Gotchas: * - You need to set a a noteType, not a noteType.duration. Setting noteType.duration * has no effect. */ class NoteImpl implements Note { _class = "Note"; _parent: ChordModelImpl; _idx: number; constructor(parent: ChordModelImpl, idx: number, note: Note, updateParent: boolean = true) { let self: {[key: string]: any} = this as any; /* Link to parent */ Object.defineProperty(this, "_parent", { enumerable: false, value: parent }); this._idx = idx; /* Properties owned by NoteImpl */ let properties = [ "pitch", "unpitched", "noteheadText", "accidental", "instrument", "attack", "endDynamics", "lyrics", "notations", "stem", "cue", "ties", "dynamics", "duration", "play", "staff", "grace", "notehead", "release", "pizzicato", "beams", "voice", "footnote", "level", "relativeY", "defaultY", "relativeX", "fontFamily", "fontWeight", "fontStyle", "fontSize", "color", "printDot", "printLyric", "printObject", "printSpacing", "timeOnly", "dots", "noteType", "timeModificiation", "rest", ]; forEach(properties, setIfDefined); function setIfDefined(property: string) { if (note.hasOwnProperty(property) && (<any>note)[property] !== undefined) { self[property] = <any> (<any>note)[property]; } } } /*---- Note -----------------------------------------------------------------------------*/ /*---- Note > Core ----------------------------------------------------------------------*/ chord: Chord; rest: Rest; dots: Dot[]; noteType: Type; timeModification: TimeModification; pitch: Pitch; /*---- Extended -------------------------------------------------------------------------*/ unpitched: Unpitched; noteheadText: NoteheadText; accidental: Accidental; instrument: Instrument; attack: number; endDynamics: number; lyrics: Lyric[]; /** * Do not modify notations. Instead use notationObj and articulationObj */ notations: Notations[]; get stem(): Stem { return this._parent.stem; } set stem(stem: Stem) { this._parent.stem = stem; } cue: Cue; duration: number; /** * This applies to the sound only. * s.a. notationObj.tieds */ ties: Tie[]; dynamics: number; play: Play; staff: number; // See prototype. grace: Grace; notehead: Notehead; release: number; pizzicato: boolean; beams: Beam[]; /*---- PrintStyle -----------------------------------------------------------------------*/ /*---- PrintStyle > EditorialVoice ------------------------------------------------------*/ voice: number; footnote: Footnote; level: Level; /*---- PrintStyle > Position ------------------------------------------------------------*/ defaultX: number; // ignored for now relativeY: number; defaultY: number; relativeX: number; /*---- PrintStyle > Font ----------------------------------------------------------------*/ fontFamily: string; fontWeight: NormalBold; fontStyle: NormalItalic; fontSize: string; /*---- PrintStyle > Color ---------------------------------------------------------------*/ color: string; /*---- Printout -------------------------------------------------------------------------*/ printDot: boolean; printLyric: boolean; /*---- Printout > PrintObject -----------------------------------------------------------*/ printObject: boolean; /*---- Printout > PrintSpacing ----------------------------------------------------------*/ printSpacing: boolean; /*---- TimeOnly -------------------------------------------------------------------------*/ timeOnly: string; /*---- Implementation -------------------------------------------------------------------*/ toXML() { return serializeNote(this); } toJSON() { let { pitch, unpitched, noteheadText, accidental, instrument, attack, endDynamics, lyrics, notations, stem, cue, ties, dynamics, duration, play, staff, grace, notehead, release, pizzicato, beams, voice, footnote, level, relativeY, defaultY, relativeX, fontFamily, fontWeight, fontStyle, fontSize, color, printDot, printLyric, printObject, printSpacing, timeOnly, dots, noteType, timeModification, rest, } = this; return { pitch, unpitched, noteheadText, accidental, instrument, attack, endDynamics, lyrics, notations, stem, cue, ties, dynamics, duration, play, staff, grace, notehead, release, pizzicato, beams, voice, footnote, level, relativeY, defaultY, relativeX, fontFamily, fontWeight, fontStyle, fontSize, color, printDot, printLyric, printObject, printSpacing, timeOnly, noteType, dots, timeModification, rest, _class: "Note", }; } inspect() { return this.toXML(); } refresh(cursor: IReadOnlyValidationCursor) { this.cleanNotations(cursor); if (this.pitch && this.pitch.step !== this.pitch.step.toUpperCase()) { cursor.patch(voice => voice.note(this._idx, note => note.pitch( pitch => pitch.step(this.pitch.step.toUpperCase()) ) )); } if (this.grace && this.cue) { cursor.patch(voice => voice.note(this._idx, note => note.cue(null) )); } if (this.unpitched && (this.rest || this.pitch)) { cursor.patch(voice => voice.note(this._idx, note => note.unpitched(null) )); } if (this.pitch && this.rest) { cursor.patch(voice => voice.note(this._idx, note => note.pitch(null) )); } invariant(cursor.segmentInstance.ownerType === "voice", "Expected to be in voice's context during validation"); if (this.voice !== cursor.segmentInstance.owner) { cursor.patch(partBuilder => partBuilder .note(this._idx, note => note .voice(cursor.segmentInstance.owner), ) ); } const defaultY = (lineForClef(this, cursor.staffAttributes.clef) - 3) * 10; if (defaultY !== this.defaultY) { cursor.patch(voice => voice .note(this._idx, note => note.defaultY(defaultY)) ); } const dotOffset = this.defaultY % 10 === 0 ? 5 : 0; if (!this.dots) { cursor.patch(voice => voice .note(this._idx, note => note.dots([])) ); } if (this.dots.some(n => n.defaultY !== dotOffset)) { cursor.patch(voice => voice .note(this._idx, note => reduce(this.dots, (note, _dot, idx) => note.dotsAt(idx, dot => dot.defaultY(dotOffset)), note), ) ); } if (!this.staff) { cursor.patch(partBuilder => partBuilder .note(this._idx, note => note .staff(1), ) ); } this.updateAccidental(cursor); } /*---- Util -----------------------------------------------------------------------------*/ /** * Flattens notations. * All of the following are valid and equivalent in MusicXML: * * 1. <notations> * <articulations> * <staccato placement="above"/> * </articulations> * </notations> * <notations> * <articulations> * <accent placement="above"/> * </articulations> * </notations> * * 2. <notations> * <articulations> * <staccato placement="above"/> * </articulations> * <articulations> * <accent placement="above"/> * </articulations> * </notations> * * 3. <notations> * <articulations> * <staccato placement="above"/> * <accent placement="above"/> * </articulations> * </notations> * * This function makes the structure like the third version. So there's only ever 0 or * 1 notations and 0 or 1 articulations. This makes the notationObj and articualtionObj * function above fast. * * In practice, different groups of notations could have different editorials and print-object * attributes. I'm not willing to put up with that, yet. */ cleanNotations(cursor: IReadOnlyValidationCursor) { let notations = cloneObject(this.notations); if (notations) { let notation: Notations = { accidentalMarks: combine<AccidentalMark> ("accidentalMarks"), arpeggiates: combine<Arpeggiate> ("arpeggiates"), articulations: combineArticulations ("articulations"), dynamics: combine<Dynamics> ("dynamics"), fermatas: combine<Fermata> ("fermatas"), footnote: last<Footnote> ("footnote"), glissandos: combine<Glissando> ("glissandos"), level: last<Level> ("level"), nonArpeggiates: combine<NonArpeggiate> ("nonArpeggiates"), ornaments: combine<Ornaments> ("ornaments"), otherNotations: combine<OtherNotation> ("otherNotations"), printObject: last<boolean> ("printObject"), slides: combine<Slide> ("slides"), slurs: combine<Slur> ("slurs"), technicals: combine<Technical> ("technicals"), tieds: combine<Tied> ("tieds"), tuplets: combine<Tuplet> ("tuplets") }; forEach(notation.tieds, tied => { if (!tied.number) { tied.number = 1; } }); forEach(notation.tuplets, tuplet => { if (!tuplet.tupletActual) { tuplet.tupletActual = {}; } if (!tuplet.tupletNormal) { tuplet.tupletNormal = {}; } if (!tuplet.tupletActual.tupletNumber) { tuplet.tupletActual.tupletNumber = { text: String(this.timeModification.actualNotes) }; } if (!tuplet.tupletNormal.tupletNumber) { tuplet.tupletNormal.tupletNumber = { text: String(this.timeModification.normalNotes) }; } if (!tuplet.tupletNormal.tupletDots) { tuplet.tupletNormal.tupletDots = map(this.timeModification.normalDots, () => ({})); } }); cursor.patch(voice => voice.note(this._idx, note => note.notations([notation]) )); } function combine<T>(key: string): T[] { return reduce(notations, (memo: any, n: any) => n[key] ? (memo || <T[]>[]).concat(n[key]) : memo, null); } function combineArticulations(key: string): Articulations[] { let array = combine<Articulations>(key); if (!array) { return null; } let articulations: Articulations = <any> {}; for (let i = 0; i < array.length; ++i) { for (let akey in array[i]) { if (array[i].hasOwnProperty(akey)) { (<any>articulations)[akey] = (<any>array[i])[akey]; } } } return [articulations]; } function last<T>(key: string): T { return reduce(notations, (memo: any, n: any) => n[key] ? n[key] : memo, []); } } updateAccidental(cursor: IReadOnlyValidationCursor) { let pitch = this.pitch; if (!pitch) { return; } let actual = pitch.alter || 0; let accidentals = cursor.staffAccidentals; invariant(!!accidentals, "Accidentals must already have been setup. Is there an Attributes element?"); // TODO: this is no longer sufficient if multiple voices share a staff. let generalTarget = accidentals[pitch.step] || null; let target = accidentals[pitch.step + pitch.octave]; if (isNaN(target) && generalTarget !== InvalidAccidental) { target = generalTarget; } let acc = cloneObject(this.accidental); if (!acc && (actual || 0) !== (target || 0)) { let accType: MxmlAccidental = null; switch (actual) { case 2: accType = MxmlAccidental.DoubleSharp; break; case 1.5: accType = MxmlAccidental.ThreeQuartersSharp; break; case 1: accType = MxmlAccidental.Sharp; break; case 0.5: accType = MxmlAccidental.QuarterSharp; break; case 0: accType = MxmlAccidental.Natural; break; case -0.5: accType = MxmlAccidental.QuarterFlat; break; case -1: accType = MxmlAccidental.Flat; break; case -1.5: accType = MxmlAccidental.ThreeQuartersFlat; break; case -2: accType = MxmlAccidental.DoubleFlat; break; default: invariant(false, "Not implemented: unknown accidental for offset %s", actual); } acc = { accidental: accType }; } if (acc) { let glyphName = accidentalGlyphs[acc.accidental]; invariant(glyphName in glyphBBoxes, "Expected a known glyph, got %s", glyphName); let width = glyphBBoxes[glyphName][0] * 10; let {clef} = cursor.staffAttributes; // TODO: `let clef = cursor.part.attributes.clefs[cursor.staffIdx]` if (onLedger(this, clef)) { acc.defaultX = -4.1; } else { acc.defaultX = -2.04; } acc.defaultX -= width; acc.defaultY = 0; if (acc.editorial && !acc.parentheses || acc.bracket) { // We don't allow an accidental to be editorial but not have parentheses. acc.parentheses = true; // XXX: do not mutate } if (acc.parentheses) { acc.defaultX -= 10; } } if (!isEqual(cloneObject(this.accidental), acc) && cursor.patch) { cursor.patch(part => part.note(this._idx, note => note.accidental(acc))); } } } export default NoteImpl;