satie
Version:
A sheet music renderer for the web
714 lines (607 loc) • 25.7 kB
text/typescript
/**
* @source: https://github.com/jnetterf/satie/
*
* @license
* (C) Josh Netterfield <joshua@nettek.ca> 2015.
* Part of the Satie music engraver <https://github.com/jnetterf/satie>.
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Clef, PartSymbol, MeasureStyle, StaffDetails, Transpose, Directive,
Time, Key, Footnote, Level, Attributes, KeyOctave, PartSymbolType, SymbolSize,
TimeSymbolType, serializeAttributes} from "musicxml-interfaces";
import {find, forEach, times, isEqual} from "lodash";
import * as invariant from "invariant";
import {IModel, Type, ILayout} from "./document";
import {IBoundingRect} from "./private_boundingRect";
import {IAttributesSnapshot} from "./private_attributesSnapshot";
import {IReadOnlyValidationCursor, LayoutCursor} from "./private_cursor";
import {hasAccidental} from "./private_chordUtil";
import {groupsForPart} from "./private_part";
import {createAttributesSnapshot as createSnapshot} from "./private_attributesSnapshot";
import {standardClefs} from "./implAttributes_clefData";
import {CLEF_INDENTATION, clefWidth, keyWidth, timeWidth,
clefsEqual, timesEqual, keysEqual} from "./implAttributes_attributesData";
class AttributesModel implements Export.IAttributesModel {
_class = "Attributes";
/*---- I.1 IModel ---------------------------------------------------------------------------*/
/** @prototype only */
private _divCount = 0;
private _layout: AttributesModel.Layout[];
get divCount() {
return this._divCount;
}
set divCount(count: number) {
invariant(isFinite(count), "Count must be finite.");
this._divCount = count;
}
/** defined externally */
staffIdx: number;
_snapshot: IAttributesSnapshot;
/*---- I.2 Attributes -----------------------------------------------------------------------*/
_parent: IAttributesSnapshot;
divisions: number;
partSymbol: PartSymbol;
measureStyles: MeasureStyle[];
staffDetails: StaffDetails[];
transposes: Transpose[];
staves: number;
instruments: string;
directives: Directive[];
clefs: Clef[];
times: Time[];
keySignatures: Key[];
/*---- I.3 Editorial ------------------------------------------------------------------------*/
footnote: Footnote;
level: Level;
/*---- Implementation -----------------------------------------------------------------------*/
refresh(cursor: IReadOnlyValidationCursor): void {
this._parent = cursor.staffAttributes;
if (!this._parent || !this._parent.divisions) {
this.divisions = this.divisions || 1;
}
this._validateClef(cursor);
this._validateTime(cursor);
this._validateKey(cursor);
this._validateStaves(cursor);
this._validateStaffDetails(cursor);
this._validateMeasureStyles(cursor);
this._snapshot = createSnapshot({
before: cursor.staffAttributes || <IAttributesSnapshot> {},
current: this,
staff: cursor.staffIdx,
measure: cursor.measureInstance.idx
});
}
getLayout(cursor: LayoutCursor): Export.IAttributesLayout {
if (!this._layout) {
this._layout = [];
}
if (!this._layout[cursor.segmentInstance.owner]) {
this._layout[cursor.segmentInstance.owner] = new AttributesModel.Layout();
}
let layout = this._layout[cursor.segmentInstance.owner];
layout._refresh(this, this._snapshot, this._parent, cursor);
return layout;
}
constructor({divisions, partSymbol, measureStyles, staffDetails, transposes, staves, instruments,
directives, clefs, times, keySignatures, footnote, level}: Attributes = {divisions: 0}) {
this.divisions = divisions;
this.partSymbol = partSymbol;
this.measureStyles = measureStyles;
this.staffDetails = staffDetails;
this.transposes = transposes;
this.staves = staves;
this.instruments = instruments;
this.directives = directives;
this.clefs = clefs;
this.times = times;
this.keySignatures = keySignatures;
this.footnote = footnote;
this.level = level;
}
toXML(): string {
let j = this.toJSON();
// Hack: we index staffDetails by 1-index staff, leaving a null at index 0, with MXML doesn't handle.
j.staffDetails = j.staffDetails.filter(a => !!a);
j.clefs = j.clefs.filter(a => !!a);
j.keySignatures = j.keySignatures.filter(a => !!a);
return `${serializeAttributes(j)}\n<forward><duration>${this.divCount}</duration></forward>\n`;
}
toJSON(): Attributes {
const {
_class,
divisions,
partSymbol,
measureStyles,
staffDetails,
transposes,
staves,
instruments,
directives,
clefs,
times,
keySignatures,
footnote,
level,
} = this;
return {
_class,
divisions,
partSymbol,
measureStyles,
staffDetails,
transposes,
staves,
instruments,
directives,
clefs,
times,
keySignatures,
footnote,
level,
};
}
inspect() {
return this.toXML();
}
calcWidth() {
return 0; // TODO
}
private _validateClef(cursor: IReadOnlyValidationCursor) {
const staffIdx = cursor.staffIdx;
// Clefs must be an array
if (!(this.clefs instanceof Array)) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.clefs([])
));
}
// Clefs must have a staff number and be sorted by staff number
this.clefs.forEach((clef, clefIdx) => {
if (!clef) {
return;
}
if (clef.number !== clefIdx) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.clefsAt(clefIdx, clef =>
clef.number(clefIdx)
)
));
}
});
// A clef is mandatory (we haven't implemented clef-less staves yet)
if ((!this._parent || !this._parent.clef) && !this.clefs[staffIdx]) {
cursor.patch(staff => staff.attributes(attributes =>
attributes
.clefsAt(0, null) // XXX: HACK to fix splice
.clefsAt(staffIdx, clef =>
clef
.number(staffIdx)
.sign("G")
.line(2)
)
));
}
// Validate the given clef
let clef = this.clefs[staffIdx];
if (clef) {
if (clef.sign !== clef.sign.toUpperCase()) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.clefsAt(staffIdx, clefb =>
clefb.sign(clef.sign.toUpperCase())
)
));
}
if (clef.line && clef.line !== parseInt("" + clef.line, 10)) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.clefsAt(staffIdx, clefb =>
clefb.line(parseInt("" + clef.line, 10))
)
));
}
// Clef lines can be inferred.
if (!clef.line) {
let {sign} = clef;
let standardClef = find(standardClefs, {sign});
cursor.patch(staff => staff.attributes(attributes =>
attributes.clefsAt(staffIdx, clefb =>
clefb.line(standardClef ? standardClef.line : 2)
)
));
}
}
}
private _validateTime(cursor: IReadOnlyValidationCursor) {
// Times must be an array
this.times = this.times || [];
// A time signature is mandatory.
if ((!this._parent || !this._parent.time) && !this.times[0]) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.timesAt(0, time => time
.symbol(TimeSymbolType.Common)
.beats(["4"])
.beatTypes([4])
)
));
}
}
private _validateKey(cursor: IReadOnlyValidationCursor) {
// Key signatures must be an array
this.keySignatures = this.keySignatures || [];
if ((!this._parent || !this._parent.keySignature) && !this.keySignatures[0]) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.keySignaturesAt(0, key => key
.fifths(0)
.mode("major")
)
));
}
let ks = this.keySignatures[0];
if (ks && (ks.keySteps || ks.keyAlters || ks.keyOctaves)) {
if (ks.keySteps.length !== ks.keyAlters.length) {
console.warn(
"Expected the number of steps to equal the number of alterations. " +
"Ignoring key.");
cursor.patch(staff => staff.attributes(attributes =>
attributes.keySignaturesAt(0, key => key
.fifths(0)
.keySteps(null)
.keyAccidentals(null)
.keyAlters(null)
)
));
}
if (ks.keyAccidentals && ks.keyAccidentals.length !== ks.keySteps.length) {
if (ks.keyAccidentals.length) {
console.warn(
"Currently, if `key-accidentals` are specified, they must be " +
"specified for all steps in a key signature due to a limitation " +
"in musicxml-interfaces. Ignoring `key-accidentals`");
}
cursor.patch(staff => staff.attributes(attributes =>
attributes.keySignaturesAt(0, key => key
.keyAccidentals(null)
)
));
}
if (ks.keyOctaves) {
// Let's sort them (move to prefilter?)
let keyOctaves: KeyOctave[] = [];
forEach(ks.keyOctaves, octave => {
keyOctaves[octave.number - 1] = octave;
});
if (!isEqual(ks.keyOctaves, keyOctaves)) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.keySignaturesAt(0, key => key
.keyOctaves(keyOctaves)
)
));
}
}
}
}
private _validateStaffDetails(cursor: IReadOnlyValidationCursor) {
// Staff details must be an array
this.staffDetails = this.staffDetails || [];
// Staff details must have a staff number
let sSoFar = 0;
this.staffDetails.forEach((staffDetails, i) => {
if (staffDetails) {
++sSoFar;
if (!staffDetails.number) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.staffDetailsAt(i, sd =>
sd.number(sSoFar)
)
));
}
}
});
// Staff details must be indexed by staff
const staffDetailsByNumber: StaffDetails[] = this.staffDetails.reduce((staffDetails, staffDetail) => {
if (staffDetail) {
staffDetails[staffDetail.number] = staffDetail;
};
return staffDetails;
}, []);
let needsSorting = this.staffDetails.length !== staffDetailsByNumber.length ||
this.staffDetails.some((s, i) => {
if (!s && !staffDetailsByNumber[i]) {
return false;
}
return !isEqual(s, staffDetailsByNumber[i]);
});
if (needsSorting) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.staffDetails(staffDetailsByNumber)
));
}
// Staff details are required. Staff lines are required
if (!this.staffDetails[cursor.staffIdx]) {
cursor.patch(staff => staff.attributes(attributes =>
attributes
.staffDetailsAt(0, null) // XXX: HACK
.staffDetailsAt(cursor.staffIdx, {
number: cursor.staffIdx,
})
));
}
if ((!this._parent || !this._parent.staffDetails ||
!this._parent.staffDetails[cursor.staffIdx] ||
!this._parent.staffDetails[cursor.staffIdx].staffLines) &&
(!this.staffDetails[cursor.staffIdx] ||
!this.staffDetails[cursor.staffIdx].staffLines)) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.staffDetailsAt(cursor.staffIdx, l => l.staffLines(5))
));
}
}
private _validateStaves(cursor: IReadOnlyValidationCursor) {
this.staves = this.staves || 1; // FIXME!
let currentPartId = cursor.segmentInstance.part;
let currentPart = cursor.measureInstance.parts[currentPartId];
times(this.staves, staffMinusOne => {
let staff = staffMinusOne + 1;
if (!currentPart.staves[staff]) {
throw new Error("A staff is missing. The code to add it is not implemented.");
}
});
if (this.staves > 1 && (!this._parent || !this._parent.partSymbol) && !this.partSymbol) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.partSymbol({
bottomStaff: 1,
topStaff: this.staves,
type: PartSymbolType.Brace,
})
));
}
// HACK: Convert part group symbols to part symbols.
// Obviously, this won't fly when we have multiple part groups
let groups = groupsForPart(cursor.header.partList, cursor.segmentInstance.part);
if (groups.length && (!this._parent || !this._parent.partSymbol) && !this.partSymbol) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.partSymbol({
bottomStaff: 1,
topStaff: 1,
type: PartSymbolType.Bracket
})
));
}
}
private _validateMeasureStyles(cursor: IReadOnlyValidationCursor): void {
if (!this.measureStyles) {
cursor.patch(staff => staff.attributes(attributes =>
attributes.measureStyles([])
));
}
}
}
module AttributesModel {
export class Layout implements Export.IAttributesLayout {
_refresh(model: IModel, attributes: Attributes, prevAttributes: Attributes, cursor: LayoutCursor) {
this.model = model;
invariant(!!attributes, "Layout must be passed a model");
this.clef = null;
this.snapshotClef = null;
this.clefSpacing = null;
this.time = null;
this.tsSpacing = null;
this.keySignature = null;
this.ksSpacing = null;
this.measureNumberVisible = null;
this.partSymbol = null;
this.staffDetails = null;
this.x = cursor.segmentX;
this.division = cursor.segmentDivision;
this.staffIdx = cursor.staffIdx;
let isFirstInLine = cursor.lineBarOnLine === 0 && !this.division;
let next = cursor.segmentInstance[cursor.segmentPosition + 1];
let ksVisible = !keysEqual(attributes, prevAttributes) || isFirstInLine;
let tsVisible = !timesEqual(attributes, prevAttributes);
let clefVisible = !clefsEqual(attributes, prevAttributes, cursor.segmentInstance.owner) || isFirstInLine;
let partSymbolVisible = isFirstInLine && attributes.partSymbol &&
attributes.partSymbol.bottomStaff === cursor.staffIdx;
// Measure number
if (!cursor.measureInstance.implicit && parseInt(cursor.measureInstance.number, 10) !== 1) {
let measureNumbering = cursor.print ?
cursor.print.measureNumbering.data : "system";
let firstInMeasure = cursor.segmentDivision === 0;
let showNumberBecauseOfSystem = isFirstInLine && measureNumbering === "system";
let showNumberBecauseOfMeasure =
this.division === 0 && measureNumbering === "measure" && firstInMeasure;
let shouldShowNumber = showNumberBecauseOfSystem || showNumberBecauseOfMeasure;
if (shouldShowNumber) {
this.measureNumberVisible = cursor.measureInstance.number;
}
}
/*---- Clef layout ------------------------------------*/
const nextChord = cursor.factory.modelHasType(next, Type.Chord) ? next : null;
this.snapshotClef = cursor.staffAttributes.clef;
if (clefVisible) {
let clef = attributes.clefs[cursor.staffIdx];
this.x += CLEF_INDENTATION;
cursor.segmentX = this.x;
let contextualSpacing = 0;
this.clef = Object.create(clef, {
"defaultX": {
get: () => {
if (isFirstInLine) {
return this.overrideX;
} else {
return this.overrideX - 10.5;
}
}
}
});
this.clef.defaultY = this.clef.defaultY || 0;
this.clef.size = isFirstInLine ? SymbolSize.Full : SymbolSize.Cue;
if (nextChord && !ksVisible && !tsVisible) {
if (hasAccidental(nextChord, cursor)) {
// TODO: what if there are more than 1 accidental?
contextualSpacing = 15;
} else {
contextualSpacing = 25;
}
} else {
contextualSpacing = 12.5;
}
if (!isFirstInLine) {
contextualSpacing -= 19.8;
}
this.clefSpacing = clefWidth(attributes) + contextualSpacing;
} else {
this.clefSpacing = 0;
}
/*---- KS layout --------------------------------------*/
if (ksVisible) {
let keySignature = attributes.keySignatures[0];
let contextualSpacing = 0;
this.keySignature = Object.create(keySignature, {
defaultX: {
get: () => {
return this.overrideX + this.clefSpacing;
}
}
});
this.keySignature.defaultY = 0;
if (nextChord && !tsVisible) {
if (hasAccidental(nextChord, cursor)) {
// TODO: what if there are more than 1 accidental?
contextualSpacing = 25;
} else {
contextualSpacing = 15;
}
} else {
contextualSpacing = 10;
}
this.ksSpacing = contextualSpacing + keyWidth(attributes);
} else {
this.ksSpacing = 0;
}
/*---- TS layout --------------------------------------*/
if (tsVisible) {
let time = attributes.times[0];
let contextualSpacing = 0;
this.time = Object.create(time, {
defaultX: {
get: () => {
return this.overrideX + this.clefSpacing + this.ksSpacing;
}
}
});
this.time.defaultY = 0;
if (nextChord) {
if (hasAccidental(nextChord, cursor)) {
// TODO: what if there are more than 1 accidental?
contextualSpacing = 25;
} else {
contextualSpacing = 15;
}
} else {
contextualSpacing = 12.5;
}
if (!attributes.times[0].beatTypes) {
contextualSpacing = 0;
}
this.tsSpacing = contextualSpacing + timeWidth(attributes);
} else {
this.tsSpacing = 0;
}
/*---- Part symbol ------------------------------------*/
if (partSymbolVisible) {
let {partSymbol} = cursor.staffAttributes;
this.partSymbol = Object.create(partSymbol, {
defaultX: {
get: () => {
return 0;
}
}
});
}
this.staffDetails = cursor.staffAttributes.staffDetails[this.staffIdx];
/*---- Geometry ---------------------------------------*/
cursor.segmentX += this.clefSpacing + this.tsSpacing + this.ksSpacing;
this.renderedWidth = cursor.segmentX - this.x - 8;
}
/*---- ILayout ------------------------------------------------------*/
// Constructed:
model: IModel;
x: number;
division: number;
staffIdx: number;
/**
* Set by layout engine.
*/
overrideX: number;
// Prototype:
boundingBoxes: IBoundingRect[];
renderClass: Type;
expandPolicy: "none";
renderedWidth: number;
/*---- AttributesModel ----------------------------------------------*/
clef: Clef;
snapshotClef: Clef;
clefSpacing: number;
time: Time;
tsSpacing: number;
keySignature: Key;
ksSpacing: number;
/** undefined if no measure number should be displayed. */
measureNumberVisible: string;
partSymbol: PartSymbol;
staffDetails: StaffDetails;
}
Layout.prototype.expandPolicy = "none";
Layout.prototype.renderClass = Type.Attributes;
Layout.prototype.boundingBoxes = [];
Object.freeze(Layout.prototype.boundingBoxes);
};
/**
* Registers Attributes in the factory structure passed in.
*/
function Export(constructors: { [key: number]: any }) {
constructors[Type.Attributes] = AttributesModel;
}
module Export {
export interface IAttributesModel extends Attributes, IModel {
divisions: number;
}
export interface IAttributesLayout extends ILayout {
model: IModel;
clef: Clef;
snapshotClef: Clef;
clefSpacing: number;
time: Time;
tsSpacing: number;
keySignature: Key;
ksSpacing: number;
measureNumberVisible: string;
partSymbol: PartSymbol;
staffIdx: number;
staffDetails: StaffDetails;
}
export function createWarningLayout(cursor: LayoutCursor, prevAttributes: Attributes,
nextAttributes: Attributes): IAttributesLayout {
let warningLayout = new AttributesModel.Layout();
console.log("Creating warning layout for ", nextAttributes);
warningLayout._refresh(
null,
nextAttributes,
prevAttributes,
cursor,
);
return warningLayout;
}
}
export default Export;