smoosic
Version:
<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i
982 lines (955 loc) • 35.3 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
/**
* Classes to support a {@link SmoSystemStaff}, which is a container for measures and
* staff modifiers.
* @module /smo/data/systemStaff
* **/
import { SmoObjectParams, SmoAttrs, MeasureNumber, getId,
ElementLike } from './common';
import { SmoMusic } from './music';
import { SmoMeasure, SmoMeasureParamsSer } from './measure';
import { SmoMeasureFormat, SmoRehearsalMark, SmoRehearsalMarkParams, SmoTempoTextParams, SmoVolta, SmoBarline } from './measureModifiers';
import { SmoInstrumentParams, StaffModifierBase, SmoInstrument, SmoInstrumentMeasure, SmoInstrumentStringParams, SmoInstrumentNumParams,
SmoTie, SmoStaffTextBracket, SmoStaffTextBracketParamsSer,
StaffModifierBaseSer, SmoTabStave, SmoTabStaveParamsSer } from './staffModifiers';
import { SmoPartInfo, SmoPartInfoParamsSer } from './partInfo';
import { SmoTextGroup } from './scoreText';
import { SmoSelector } from '../xform/selections';
import { SmoBeamer } from '../xform/beamers';
import { smoSerialize } from '../../common/serializationHelpers';
import { FontInfo } from '../../common/vex';
/**
* indicate that we need to serialize the key signature, etc.
* maps beause we are going to be deserializing again in a different score
* @category SmoObject
*/
export interface SmoStaffSerializationOptions {
skipMaps: boolean,
preserveIds: boolean
}
/**
* Constructor parameters for {@link SmoSystemStaff}.
* Usually you will call
* {@link SmoSystemStaff.defaults}, and modify the parameters you need to change,
* or get the defaults from an existing staff
* @param renumberingMap For alternate number, pickups, etc.
* @param keySignatureMap map of keys to measures
* @param measureInstrumentMap map of instruments to staves
* @param measures array of {@link SmoMeasure}
* @param modifiers slurs and such
* @param partInfo
* @category SmoObject
*/
export interface SmoSystemStaffParams {
/* the index of the staff in the score */
staffId: number,
/**
* For alternate number, pickups, etc.
* */
renumberingMap: Record<number, number>,
/**
* map of keys to measures
*/
keySignatureMap: Record<number, string>,
/* map of instruments to staves */
measureInstrumentMap: Record<number, SmoInstrumentParams>,
/**
* array of {@link SmoMeasure})
*/
measures: SmoMeasure[],
/**
* modifiers slurs and such
* */
modifiers: StaffModifierBase[],
/**
* information about the part
*/
partInfo?: SmoPartInfo;
/**
* text lines
*/
textBrackets?: SmoStaffTextBracket[];
/**
*guitar tablature
*/
tabStaves: SmoTabStave[]
}
/**
* Serialized components of a stave
* @category serialization
*/
export interface SmoSystemStaffParamsSer {
/**
* class name
*/
ctor: string,
/**
* index of the staff
*/
staffId: number,
/**
* map of measure numbers vs. indices of measures
*/
renumberingMap?: Record<number, number>,
/**
* locations of key signature changes
*/
keySignatureMap?: Record<number, string>,
/**
* map of measures to instruments (clef, transpose, sounds)
*/
measureInstrumentMap: Record<number, SmoInstrumentParams>,
/**
* measure container
*/
measures: SmoMeasureParamsSer[],
/**
* array of modifiers like slurs
*/
modifiers: StaffModifierBaseSer[],
/**
* Associated part information for this stave
*/
partInfo: SmoPartInfoParamsSer;
/**
* text brackets are another kind of modifier
*/
textBrackets: SmoStaffTextBracketParamsSer[];
/**
* guitar tablature
*/
tabStaves: SmoTabStave[];
}
function isSmoSystemStaffParamsSer(params: Partial<SmoSystemStaffParamsSer>):params is SmoSystemStaffParamsSer {
if (!(typeof(params.ctor) === 'string' && params.ctor === 'SmoSystemStaff')) {
return false;
}
if (!(Array.isArray(params.measures))) {
return false;
}
return true;
}
/**
* A staff is a line of music that can span multiple measures.
* A system is a line of music for each staff in the score. So a staff
* spans multiple systems.
* A staff modifier connects 2 points in the staff.
* @category SmoObject
* */
export class SmoSystemStaff implements SmoObjectParams {
/**
* Gets the instrument assigned to a given measure
* @param measureInstrumentMap
* @param measureIndex
* @returns
*/
static getStaffInstrument(measureInstrumentMap: Record<number, SmoInstrument>, measureIndex: number) {
const keyar: string[] = Object.keys(measureInstrumentMap);
let fit = 0;
keyar.forEach((key) => {
const numkey = parseInt(key, 10);
if (numkey <= measureIndex && numkey > fit) {
fit = numkey;
}
});
return measureInstrumentMap[fit];
}
static getStaffInstrumentArray(measureInstrumentMap: Record<number, SmoInstrumentParams>): SmoInstrumentMeasure[] {
const rv: SmoInstrumentMeasure[] = [];
const keyar: string[] = Object.keys(measureInstrumentMap);
keyar.forEach((key) => {
const measureIndex = parseInt(key, 10);
rv.push({ measureIndex, instrument: measureInstrumentMap[measureIndex] });
});
return rv;
}
staffId: number = 0;
renumberingMap: Record<number, number> = {};
keySignatureMap: Record<number, string> = {};
partInfo: SmoPartInfo;
measureInstrumentMap: Record<number, SmoInstrument> = {};
measures: SmoMeasure[] = [];
modifiers: StaffModifierBase[] = [];
textBrackets: SmoStaffTextBracket[] = [];
bracketMap: Record<number, ElementLike[]> = {};
tabStaves: SmoTabStave[] = [];
attrs: SmoAttrs = {
id: '',
type: 'SmoSystemStaff'
}
ctor: string = 'SmoSystemStaff';
_mappedStaffId: number = 0;
// ### defaults
// default values for all instances
static get defaults(): SmoSystemStaffParams {
return JSON.parse(JSON.stringify({
staffId: 0,
renumberingMap: {},
keySignatureMap: {},
measureInstrumentMap: {},
textBrackets: [],
measures: [],
modifiers: [],
tabStaves: []
}));
}
setMappedStaffId(value: number) {
this._mappedStaffId = value;
}
getMappedStaffId(): number {
return this._mappedStaffId;
}
constructor(params: SmoSystemStaffParams) {
this.staffId = params.staffId;
this.measures = params.measures;
this.modifiers = params.modifiers;
this.textBrackets = params.textBrackets ?? [];
this.renumberingMap = params.renumberingMap;
this.tabStaves = params.tabStaves;
if (!params.measureInstrumentMap) {
params.measureInstrumentMap = {};
const defs = SmoInstrument.defaults;
const ii: SmoInstrument = new SmoInstrument(defs);
params.measureInstrumentMap[0] = ii;
}
if (Object.keys(params.measureInstrumentMap).length === 0) {
const instrument = new SmoInstrument(SmoInstrument.defaults);
instrument.startSelector.staff = instrument.endSelector.staff = this.staffId;
instrument.endSelector.measure = this.measures.length - 1;
this.measureInstrumentMap[0] = instrument;
} else {
const keys = Object.keys(params.measureInstrumentMap);
keys.forEach((p, ix) => {
const pnum = parseInt(p, 10);
const instrument = new SmoInstrument(params.measureInstrumentMap[pnum]);
instrument.startSelector.staff = instrument.endSelector.staff = this.staffId;
// Make sure transposition goes to the end stave of the song.
if (ix === keys.length - 1) {
instrument.endSelector.measure = this.measures.length - 1;
}
this.measureInstrumentMap[pnum] = instrument;
});
}
this.consolidateInstruments();
if (this.measures.length) {
this.numberMeasures();
}
this.attrs = {
id: getId().toString(),
type: 'SmoSystemStaff'
};
if (params.partInfo) {
this.partInfo = params.partInfo;
} else {
const staveNo = this.staffId + 1;
const partDefs = SmoPartInfo.defaults;
partDefs.partName = 'Staff ' + staveNo;
partDefs.partAbbreviation = staveNo.toString() + '.';
this.partInfo = new SmoPartInfo(partDefs);
}
}
/**
* records need to be serialized separately from other elements in parameters
*
* @static
* @type {string[]}
* @memberof SmoSystemStaff
*/
static serializableElements: string[] = ['ctor', 'staffId'];
static recordElements: string[] = ['renumberingMap', 'keySignatureMap', 'measureInstrumentMap'];
// ### defaultParameters
// the parameters that get saved with the score.
static get defaultParameters() {
return [
'renumberingMap', 'keySignatureMap', 'instrumentInfo'];
}
get renderableModifiers() {
const rv: StaffModifierBase[] =
this.modifiers.concat(this.textBrackets);
return rv;
}
// ### serialize
// JSONify self.
serialize(options: SmoStaffSerializationOptions): SmoSystemStaffParamsSer {
const params: Partial<SmoSystemStaffParamsSer> = {
ctor: 'SmoSystemStaff',
tabStaves: []
};
if (!options.skipMaps) {
smoSerialize.serializedMerge(SmoSystemStaff.defaultParameters, this, params);
}
params.measures = [];
params.measureInstrumentMap = {};
const ikeys: string[] = Object.keys(this.measureInstrumentMap);
this.tabStaves.forEach((ts) => {
params.tabStaves?.push(ts.serialize());
});
ikeys.forEach((ikey: string) => {
params.measureInstrumentMap![parseInt(ikey, 10)] = this.measureInstrumentMap[parseInt(ikey, 10)].serialize();
});
this.measures.forEach((measure) => {
params.measures!.push(measure.serialize());
});
params.modifiers = [];
this.modifiers.forEach((modifier) => {
const ser = options.preserveIds ? modifier.serializeWithId() : modifier.serialize();
params.modifiers!.push(ser);
});
this.textBrackets.forEach((bracket) => {
const ser = options.preserveIds ? bracket.serializeWithId() : bracket.serialize();
params.modifiers!.push(ser);
});
params.partInfo = this.partInfo.serialize();
if (!isSmoSystemStaffParamsSer(params)) {
throw ('bad staff ' + JSON.stringify(params));
}
return params;
}
// ### deserialize
// parse formerly serialized staff.
static deserialize(jsonObj: SmoSystemStaffParamsSer): SmoSystemStaff {
const params: SmoSystemStaffParams = SmoSystemStaff.defaults;
params.staffId = jsonObj.staffId ?? 0;
params.measures = [];
params.modifiers = [];
params.textBrackets = [];
params.renumberingMap = jsonObj.renumberingMap ?? {};
if (jsonObj.tabStaves) {
jsonObj.tabStaves.forEach((ts) => {
params.tabStaves.push(StaffModifierBase.deserialize(ts));
});
}
if (jsonObj.partInfo) {
// Deserialize the text groups first
const tgs: SmoTextGroup[] = [];
jsonObj.partInfo.textGroups.forEach((tgSer: any) => {
tgs.push(SmoTextGroup.deserializePreserveId(tgSer));
});
jsonObj.partInfo.textGroups = tgs;
params.partInfo = SmoPartInfo.deserialize(jsonObj.partInfo);
}
// Up-convert legacy instrument info, which was split between different objects
if (!jsonObj.measureInstrumentMap) {
const jsonLegacy = jsonObj as any;
if (jsonLegacy.instrumentInfo) {
const defs = SmoInstrument.defaults;
defs.keyOffset = jsonLegacy.instrumentInfo.keyOffset;
defs.clef = jsonLegacy.instrumentInfo.clef;
defs.instrumentName = jsonLegacy.instrumentInfo.instrumentName;
const ii: SmoInstrument = new SmoInstrument(defs);
params.measureInstrumentMap = { 0: ii };
} else {
const ii: SmoInstrument = new SmoInstrument(SmoInstrument.defaults);
params.measureInstrumentMap = { 0: ii };
}
params.measureInstrumentMap[0].startSelector.staff = params.staffId;
params.measureInstrumentMap[0].endSelector.staff = params.staffId;
params.measureInstrumentMap[0].endSelector.measure = jsonObj.measures.length - 1;
params.measureInstrumentMap[0].keyOffset = jsonObj.measures[0].transposeIndex ?? 0;
} else {
const ikeys = Object.keys(jsonObj.measureInstrumentMap);
ikeys.forEach((ikey) => {
const ix = parseInt(ikey, 10);
const inst = jsonObj.measureInstrumentMap[ix];
const defs = SmoInstrument.defaults;
SmoInstrumentStringParams.forEach((str) => {
if (typeof(inst[str]) === 'string') {
defs[str] = inst[str] as any;
}
});
SmoInstrumentNumParams.forEach((str) => {
if (typeof(inst[str]) === 'number') {
defs[str] = inst[str];
}
});
if (typeof(inst.startSelector) !== 'undefined') {
defs.startSelector = inst.startSelector;
}
if (typeof(inst.endSelector) !== 'undefined') {
defs.endSelector = inst.endSelector;
}
params.measureInstrumentMap[ix] = new SmoInstrument(defs);
});
}
const instrumentAr = SmoSystemStaff.getStaffInstrumentArray(params.measureInstrumentMap);
let curInstrumentIndex = 0;
jsonObj.measures.forEach((measureObj: any) => {
const measure = SmoMeasure.deserialize(measureObj);
if (instrumentAr.length > (curInstrumentIndex + 1) && measure.measureNumber.measureIndex >=
instrumentAr[curInstrumentIndex + 1].measureIndex) {
curInstrumentIndex += 1;
}
measure.transposeIndex = instrumentAr[curInstrumentIndex].instrument.keyOffset;
measure.lines = instrumentAr[curInstrumentIndex].instrument.lines;
params.measures.push(measure);
});
if (jsonObj.modifiers) {
jsonObj.modifiers.forEach((modParams: any) => {
const mod = StaffModifierBase.deserialize(modParams);
if (modParams.attrs?.id) {
mod.attrs.id = modParams.attrs.id;
}
mod.associatedStaff = jsonObj.staffId;
if (mod.ctor === 'SmoStaffTextBracket') {
params.textBrackets!.push(mod as SmoStaffTextBracket);
} else {
params.modifiers.push(mod);
}
});
}
const rv = new SmoSystemStaff(params);
return rv;
}
/**
* We have created a score with staff mappings. Update the selectors in staff modifiers so that
* 'from' in the staff slot is 'to'
*/
mapStaffFromTo(from: number, to: number) {
if (from === to) {
return;
}
this.modifiers.forEach((mod) => {
if (mod.startSelector.staff === from) {
mod.startSelector.staff = to;
}
if (mod.endSelector.staff === from) {
mod.endSelector.staff = to;
}
mod.associatedStaff = to; // this.staffId will remap to 'to' value
});
this.textBrackets.forEach((mod: SmoStaffTextBracket) => {
if (mod.startSelector.staff === from) {
mod.startSelector.staff = to;
}
if (mod.endSelector.staff === from) {
mod.endSelector.staff = to;
}
mod.associatedStaff = to; // this.staffId will remap to 'to' value
});
}
updateMeasureFormatsForPart() {
this.measures.forEach((measure, mix) => {
if (this.partInfo.measureFormatting[mix]) {
measure.format = new SmoMeasureFormat(this.partInfo.measureFormatting[mix]);
} else {
measure.format = new SmoMeasureFormat(SmoMeasureFormat.defaults);
}
});
}
/**
* Get the active instrument at the given measure
* @param measureIndex
* @returns
*/
getStaffInstrument(measureIndex: number): SmoInstrument {
return SmoSystemStaff.getStaffInstrument(this.measureInstrumentMap, measureIndex);
}
getInstrumentList(): SmoInstrument[] {
const rv: SmoInstrument[] = [];
const keys = Object.keys(this.measureInstrumentMap);
keys.forEach((key) => {
rv.push(this.getStaffInstrument(parseInt(key)));
});
return rv;
}
consolidateInstruments() {
const keys = Object.keys(this.measureInstrumentMap);
const nrec:Record<number, SmoInstrument> = {};
let prevInst: SmoInstrument | null = null;
for (let i = 0;i < this.measures.length; ++i) {
if (this.measureInstrumentMap[i]) {
const cur = this.measureInstrumentMap[i];
if (prevInst !== null) {
const inst: SmoInstrument = prevInst;
if (inst.eq(cur)) {
nrec[inst.startSelector.measure] = inst;
inst.endSelector.measure = Math.max(inst.endSelector.measure, cur.endSelector.measure);
} else {
const inst: SmoInstrument = prevInst;
inst.endSelector.measure = Math.max(i - 1, 0);
nrec[inst.startSelector.measure] = inst;
prevInst = cur;
nrec[cur.startSelector.measure] = cur;
}
} else {
prevInst = cur;
}
}
}
// If there are no measures, and we never populated any records. Happens for new scores.
if (prevInst) {
const inst: SmoInstrument = prevInst;
nrec[inst.startSelector.measure] = inst;
} else {
return;
}
this.measureInstrumentMap = nrec;
}
updateInstrumentOffsets() {
const ar = SmoSystemStaff.getStaffInstrumentArray(this.measureInstrumentMap);
ar.forEach((entry) => {
let i = entry.instrument.startSelector.measure;
for (i; i <= entry.instrument.endSelector.measure && i < this.measures.length; ++i) {
const measure = this.measures[i];
const concertKey = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex);
const targetKey = SmoMusic.vexKeySigWithOffset(concertKey, entry.instrument.keyOffset);
const tabStave: SmoTabStave | undefined = this.getTabStaveForMeasure(SmoSelector.fromMeasure(measure));
measure.transposeToOffset(entry.instrument.keyOffset, targetKey, entry.instrument.clef);
measure.transposeIndex = entry.instrument.keyOffset;
measure.lines = entry.instrument.lines;
measure.keySignature = targetKey;
measure.setClef(entry.instrument.clef);
}
});
}
isRest(index: number) {
return this.measures[index].isRest();
}
/**
* for the purposes of breaking up multimeasure rests, isRepeat is true if
* the next bar has a start repeat, or the current bar has an end repeat.
* @param index
* @returns
*/
isRepeat(index: number) {
if (index < this.measures.length - 1) {
if (this.measures[index + 1].getStartBarline().barline !== SmoBarline.barlines.singleBar &&
this.measures[index + 1].getStartBarline().barline !== SmoBarline.barlines.noBar) {
return true;
}
}
const specialBar = this.measures[index].getEndBarline().barline !== SmoBarline.barlines.singleBar &&
this.measures[index].getStartBarline().barline !== SmoBarline.barlines.noBar;
return specialBar || this.measures[index].repeatSymbol;
}
isRepeatSymbol(index: number) {
return this.measures[index].repeatSymbol;
}
isRehearsal(index: number) {
return !(typeof(this.measures[index].getRehearsalMark()) === 'undefined');
}
findSimlarOverlap(modifier: StaffModifierBase) {
const overlap = this.modifiers.filter((ff) =>
SmoSelector.overlaps(ff.startSelector, ff.endSelector, modifier.startSelector, modifier.endSelector) &&
ff.ctor === modifier.ctor);
return overlap;
}
removeTabStaves(delList: SmoTabStave[]) {
if (delList.length < 1) {
return;
}
const newList: SmoTabStave[] = [];
this.tabStaves.forEach((ts) => {
if (delList.findIndex((xx:SmoTabStave) => xx.attrs.id === ts.attrs.id) < 0) {
newList.push(ts);
}
});
this.tabStaves = newList;
}
updateTabStave(ts: SmoTabStave) {
if (ts.allMeasures) {
ts.startSelector.measure = 0;
ts.endSelector.measure = this.measures.length - 1;
this.tabStaves = [ts];
return;
}
if (!this.tabStaves.length) {
this.tabStaves.push(ts);
return;
}
const toRemove: SmoTabStave[] = [];
for (var i = 0; i < this.tabStaves.length; ++i) {
const ex = this.tabStaves[i];
if (SmoTabStave.overlaps(ex, ts)) {
const starts = SmoSelector.order(ex.startSelector, ts.startSelector);
const ends = SmoSelector.order(ex.startSelector, ex.endSelector);
// If the tabs are the same type and overlap, then just merge them
if (SmoTabStave.featuresEqual(ex, ts)) {
ts.startSelector = starts[0];
ts.endSelector = ends[1];
}
toRemove.push(ex);
}
}
this.removeTabStaves(toRemove);
this.tabStaves.push(ts);
}
getTabStaveForMeasure(selector: SmoSelector): SmoTabStave | undefined {
return this.tabStaves.find((ts) =>
SmoSelector.sameStaff(ts.startSelector, selector) && ts.startSelector.measure <= selector.measure
&& ts.endSelector.measure >= selector.measure);
}
getTabStavesForMeasureRow(measures: SmoMeasure[]) {
const rv: SmoTabStave[] = [];
const added: Record<string, boolean> = {};
measures.forEach((mm) => {
const ts = this.getTabStaveForMeasure(SmoSelector.measureSelector(mm.measureNumber.staffId, mm.measureNumber.measureIndex));
if (ts && !added[ts.attrs.id]) {
added[ts.attrs.id] = true;
rv.push(ts);
}
});
return rv;
}
// ### addStaffModifier
// add a staff modifier, or replace a modifier of same type
// with same endpoints.
addStaffModifier(modifier: StaffModifierBase) {
this.removeStaffModifier(modifier);
this.modifiers.push(modifier);
modifier.associatedStaff = this.staffId;
}
// ### removeStaffModifier
// Remove a modifier of given type and location
removeStaffModifier(modifier: StaffModifierBase) {
const mods: StaffModifierBase[] = [];
const tbs: SmoStaffTextBracket[] = [];
this.renderableModifiers.forEach((mod: StaffModifierBase) => {
if (mod.attrs.type !== modifier.attrs.type ||
SmoSelector.neq(mod.startSelector, modifier.startSelector) ||
SmoSelector.neq(mod.endSelector, modifier.endSelector)) {
if (mod.ctor === 'SmoStaffTextBracket') {
tbs.push(mod as SmoStaffTextBracket);
} else {
mods.push(mod);
}
}
});
this.textBrackets = tbs;
this.modifiers = mods;
}
// ### getVoltaMap
getVoltaMap(startIndex: number, endIndex: number) {
const rv: SmoVolta[] = [];
this.measures.forEach((measure) => {
measure.getNthEndings().forEach((ending) => {
if (ending.startBar >= startIndex && ending.endBar <= endIndex) {
rv.push(ending);
}
});
});
return rv;
}
getVoltasForMeasure(ix: number) {
const rv: SmoVolta[] = [];
this.measures.forEach((measure) => {
measure.getNthEndings().forEach((ending) => {
if (ending.startSelector?.measure === ix || ending.endSelector?.measure === ix) {
rv.push(ending);
}
});
});
return rv;
}
// ### getModifiersAt
// get any modifiers at the selected location
getModifiersAt(selector: SmoSelector): StaffModifierBase[] {
const rv: StaffModifierBase[] = [];
this.modifiers.forEach((mod) => {
if (SmoSelector.sameNote(mod.startSelector, selector)) {
rv.push(mod);
}
});
return rv;
}
getModifier(modData: any) {
return this.renderableModifiers.find((mod) =>
SmoSelector.eq(mod.startSelector, modData.startSelector) && mod.attrs.type === modData.attrs.type);
}
setLyricFont(fontInfo: FontInfo) {
this.measures.forEach((measure) => {
measure.setLyricFont(fontInfo);
});
}
setLyricAdjustWidth(adjustNoteWidth: boolean) {
this.measures.forEach((measure) => {
measure.setLyricAdjustWidth(adjustNoteWidth);
});
}
setChordFont(fontInfo: FontInfo) {
this.measures.forEach((measure) => {
measure.setChordFont(fontInfo);
});
}
setChordAdjustWidth(adjustNoteWidth: boolean) {
this.measures.forEach((measure) => {
measure.setChordAdjustWidth(adjustNoteWidth);
});
}
addTextBracket(bracketParams: SmoStaffTextBracket) {
const nb = new SmoStaffTextBracket(bracketParams);
const brackets = this.textBrackets.filter((tb) => SmoSelector.lteq(tb.startSelector, nb.startSelector)
|| SmoSelector.gteq(tb.endSelector, nb.startSelector) || tb.position !== nb.position);
brackets.push(new SmoStaffTextBracket(bracketParams));
this.textBrackets = brackets;
}
removeTextBracket(bracketParams: SmoStaffTextBracket) {
const nb = new SmoStaffTextBracket(bracketParams);
const brackets = this.textBrackets.filter((tb) => SmoSelector.lteq(tb.startSelector, nb.startSelector)
|| SmoSelector.gteq(tb.endSelector, nb.startSelector) || tb.position !== nb.position);
brackets.push(new SmoStaffTextBracket(bracketParams));
this.textBrackets = brackets;
}
getTextBracketsStartingAt(selector: SmoSelector) {
return this.textBrackets.filter((tb) => SmoSelector.eq(tb.startSelector, selector));
}
// ### getSlursStartingAt
// like it says. Used by audio player to slur notes
getSlursStartingAt(selector: SmoSelector) {
return this.modifiers.filter((mod) =>
SmoSelector.sameNote(mod.startSelector, selector) && mod.attrs.type === 'SmoSlur'
);
}
// ### getSlursEndingAt
// like it says.
getSlursEndingAt(selector: SmoSelector) {
return this.modifiers.filter((mod) =>
SmoSelector.sameNote(mod.endSelector, selector) && mod.attrs.type === 'SmoSlur'
);
}
getTiesStartingAt(selector: SmoSelector): SmoTie[] {
return this.modifiers.filter((mod) =>
SmoSelector.sameNote(mod.startSelector, selector) && mod.attrs.type === 'SmoTie'
) as SmoTie[];
}
getTiesEndingAt(selector: SmoSelector) {
return this.modifiers.filter((mod) =>
SmoSelector.sameNote(mod.endSelector, selector) && mod.attrs.type === 'SmoTie'
);
}
getPedalMarkingsContaining(selector: SmoSelector) {
return this.modifiers.filter((mod) =>
mod.ctor === 'SmoPedalMarking' && SmoSelector.contains(selector, mod.startSelector, mod.endSelector)
);
}
// ### accesor getModifiers
getModifiers() {
return this.modifiers;
}
// ### applyBeams
// group all the measures' notes into beam groups.
applyBeams() {
for (let i = 0; i < this.measures.length; ++i) {
const measure = this.measures[i];
SmoBeamer.applyBeams(measure);
}
}
// ### addRehearsalMark
// for all measures in the system, and also bump the
// auto-indexing
addRehearsalMark(index: number, parameters: SmoRehearsalMarkParams) {
let i = 0;
let symbol = '';
var mark = new SmoRehearsalMark(parameters);
if (!mark.increment) {
this.measures[index].addRehearsalMark(mark);
return;
}
symbol = mark.symbol;
for (i = 0; i < this.measures.length; ++i) {
const mm = this.measures[i];
if (i < index) {
const rm: SmoRehearsalMark = (mm.getRehearsalMark() as SmoRehearsalMark);
if (rm && rm.cardinality === mark.cardinality && rm.increment) {
symbol = rm.getIncrement();
mark.symbol = symbol;
}
}
if (i === index) {
mm.addRehearsalMark(mark);
symbol = mark.getIncrement();
}
if (i > index) {
const rm: SmoRehearsalMark = (mm.getRehearsalMark() as SmoRehearsalMark);
if (rm && rm.cardinality === mark.cardinality && rm.increment) {
rm.symbol = symbol;
symbol = rm.getIncrement();
}
}
}
}
removeTempo(index: number) {
this.measures[index].resetTempo();
}
addTempo(tempo: SmoTempoTextParams, index: number) {
this.measures[index].setTempo(tempo);
}
// ### removeRehearsalMark
// for all measures in the system, and also decrement the
// auto-indexing
removeRehearsalMark(index: number) {
let ix: number = 0;
let symbol: string | null = null;
let card: string | null = null;
this.measures.forEach((measure) => {
if (ix === index) {
const mark: SmoRehearsalMark = measure.getRehearsalMark() as SmoRehearsalMark;
if (mark) {
symbol = mark.symbol;
card = mark.cardinality;
}
measure.removeRehearsalMark();
}
if (ix > index && symbol && card) {
const mark: SmoRehearsalMark = measure.getRehearsalMark() as SmoRehearsalMark;
if (mark && mark.increment) {
mark.symbol = symbol;
symbol = mark.getIncrement();
}
}
ix += 1;
});
}
/**
* Sync the staff modifier indices between the full score and the score view, which may
* have fewer staves
* @param measureIndex
* @param ostaff
*/
syncStaffModifiers(measureIndex: number, ostaff: SmoSystemStaff) {
const mods: StaffModifierBase[] = [];
// remove any modifiers in the stored score that aren't in the view score
this.modifiers.forEach((modifier) => {
if (modifier.startSelector.measure !== measureIndex) {
mods.push(modifier);
} else {
const omod = ostaff.modifiers.find((mm) => mm.attrs.id === modifier.attrs.id);
if (omod) {
mods.push(omod);
}
}
});
this.modifiers = mods;
const measureSelectors = ostaff.modifiers.filter((mm) => mm.startSelector.measure === measureIndex);
// Add any new modifiers from a copy operation
measureSelectors.forEach((modifier) => {
const dup = this.modifiers.find((mm) =>
mm.startSelector.measure === modifier.startSelector.measure &&
mm.endSelector.measure === modifier.endSelector.measure &&
mm.ctor === modifier.ctor);
if (dup ?? null === null) {
const ser = StaffModifierBase.cloneWithId(modifier);
const des = StaffModifierBase.deserialize(ser);
des.attrs = JSON.parse(JSON.stringify(ser.attrs));
des.startSelector.staff = this.staffId;
des.endSelector.staff = this.staffId;
this.modifiers.push(des);
}
});
}
// ### deleteMeasure
// delete the measure, and any staff modifiers that start/end there.
deleteMeasure(index: number) {
if (this.measures.length < 2) {
return; // don't delete last measure.
}
const nm: SmoMeasure[] = [];
this.measures.forEach((measure) => {
if (measure.measureNumber.measureIndex !== index) {
nm.push(measure);
}
});
this.tabStaves.forEach((ts) => {
ts.endSelector.measure = this.measures.length - 1;
});
const sm: StaffModifierBase[] = [];
this.modifiers.forEach((mod) => {
// Bug: if we are deleting a measure before the selector, change the measure number.
if (mod.startSelector.measure !== index && mod.endSelector.measure !== index) {
if (index < mod.startSelector.measure) {
mod.startSelector.measure -= 1;
}
if (index < mod.endSelector.measure) {
mod.endSelector.measure -= 1;
}
sm.push(mod);
}
});
const instMap: Record<number, SmoInstrument> = {};
SmoSystemStaff.getStaffInstrumentArray(this.measureInstrumentMap).forEach((mm) => {
if (mm.instrument.startSelector.measure > index || mm.instrument.startSelector.measure > this.measures.length - 1) {
mm.instrument.startSelector.measure -= 1;
}
if (mm.instrument.endSelector.measure > index || mm.instrument.endSelector.measure > this.measures.length - 1) {
mm.instrument.endSelector.measure -= 1;
}
instMap[mm.instrument.startSelector.measure] = new SmoInstrument(mm.instrument);
});
this.measures = nm;
this.modifiers = sm;
this.numberMeasures();
}
// ### addKeySignature
// Add key signature to the given measure and update map so we know
// when it changes, cancels etc.
addKeySignature(measureIndex: number, key: string) {
this.keySignatureMap[measureIndex] = key;
const target = this.measures[measureIndex];
target.keySignature = key;
}
_updateKeySignatures() {
let i = 0;
const currentSig = this.measures[0].keySignature;
for (i = 0; i < this.measures.length; ++i) {
const measure = this.measures[i];
const nextSig = this.keySignatureMap[i] ? this.keySignatureMap[i] : currentSig;
measure.setKeySignature(nextSig);
}
}
// ### numberMeasures
// After anything that might change the measure numbers, update them iteratively
numberMeasures() {
let i: number = 0;
let localIndex = 0;
for (i = 0; i < this.measures.length; ++i) {
const measure = this.measures[i];
if (typeof(this.renumberingMap[i]) === 'number') {
localIndex = this.renumberingMap[i];
} else {
localIndex += 1;
}
// since systemIndex is a render-time decision, we don't update it here.
const systemIndex = measure.measureNumber.systemIndex;
// If this is the first full measure, call it '1'
const numberObj: MeasureNumber = {
localIndex,
measureIndex: i,
systemIndex,
staffId: this.staffId
};
measure.setMeasureNumber(numberObj);
}
}
addDefaultMeasure(index: number, params: SmoMeasure) {
const measure = SmoMeasure.getDefaultMeasure(params);
this.addMeasure(index, measure);
}
// ## addMeasure
// ## Description:
// Add the measure at the specified index, splicing the array as required.
addMeasure(index: number, measure: SmoMeasure) {
if (index === 0 && this.measures.length) {
measure.setMeasureNumber(this.measures[0].measureNumber);
}
if (index >= this.measures.length) {
this.measures.push(measure);
} else {
this.measures.splice(index, 0, measure);
}
const modifiers = this.modifiers.filter((mod) => mod.startSelector.measure >= index);
modifiers.forEach((mod) => {
if (mod.startSelector.measure < this.measures.length) {
mod.startSelector.measure += 1;
}
if (mod.endSelector.measure < this.measures.length) {
mod.endSelector.measure += 1;
}
});
// If there is a tab stave, it should extend the length of the stave.
this.tabStaves.forEach((ts) => {
ts.endSelector.measure = this.measures.length - 1;
});
this.numberMeasures();
}
}