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
1,568 lines (1,498 loc) • 55.3 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
/**
* Contains definition and supporting classes for {@link SmoMeasure}.
* Most of the engraving is done at the measure level. Measure contains multiple (at least 1)
* voices, which in turn contain notes. Each measure also contains formatting information. This
* is mostly serialized outside of measure (in score), since columns and often an entire region
* share measure formatting. Measures also contain modifiers like barlines. Tuplets and beam groups
* are contained at the measure level.
* @module /smo/data/measure
*/
import { smoSerialize } from '../../common/serializationHelpers';
import { SmoMusic } from './music';
import {
SmoBarline, SmoMeasureModifierBase, SmoRepeatSymbol, SmoTempoText, SmoMeasureFormat,
SmoVolta, SmoRehearsalMarkParams, SmoRehearsalMark, SmoTempoTextParams, TimeSignature,
TimeSignatureParametersSer, SmoMeasureFormatParamsSer, SmoTempoTextParamsSer
} from './measureModifiers';
import { SmoNote, NoteType, SmoNoteParamsSer } from './note';
import { SmoTuplet, SmoTupletParamsSer, SmoTupletParams, SmoTupletTreeParamsSer, SmoTupletTree } from './tuplet';
import { layoutDebug } from '../../render/sui/layoutDebug';
import { SvgHelpers } from '../../render/sui/svgHelpers';
import { TickMap } from '../xform/tickMap';
import { MeasureNumber, SvgBox, SmoAttrs, Pitch, PitchLetter, Clef,
TickAccidental, AccidentalArray, getId } from './common';
import { SmoSelector } from '../xform/selections';
import { FontInfo } from '../../common/vex';
import { SmoTabStave } from './staffModifiers';
import { SmoFretPosition } from './noteModifiers';
/**
* Voice is just a container for {@link SmoNote}
* @category SmoObject
*/
export interface SmoVoice {
notes: SmoNote[]
}
/**
* @category SmoObject
*/
export interface SmoVoiceSer {
notes: SmoNoteParamsSer[]
}
/**
* TickMappable breaks up a circular dependency on modifiers
* like @SmoDuration
* @category SmoObject
*/
export interface TickMappable {
voices: SmoVoice[],
keySignature: string
}
/**
* @category SmoObject
*/
export interface MeasureTick {
voiceIndex: number,
tickIndex: number
}
/**
* Break up a circlar dependency with {@link SmoBeamGroup}
* @category SmoObject
*/
export interface ISmoBeamGroup {
notes: SmoNote[],
secondaryBeamBreaks: number[],
voice: number,
attrs: SmoAttrs
}
/**
* geometry information about the current measure for rendering and
* score layout.
* @internal
*/
export interface MeasureSvg {
staffWidth: number,
unjustifiedWidth: number,
adjX: number, // The start point of the music in the stave (after time sig, etc)
maxColumnStartX: number,
staffX: number, // The left-most x position of the staff
staffY: number,
logicalBox: SvgBox,
yTop: number,
adjRight: number,
history: string[],
lineIndex: number,
pageIndex: number,
rowInSystem: number,
forceClef: boolean,
forceKeySignature: boolean,
forceTimeSignature: boolean,
forceTempo: boolean,
hideEmptyMeasure: boolean,
hideMultimeasure: boolean,
multimeasureLength: number,
multimeasureEndBarline: number,
element: SVGSVGElement | null,
tabStaveBox?: SvgBox,
tabElement?: SVGSVGElement
}
/**
* Interface for a {@link TickMap} for each voice
* for formatting
* @category SmoObject
*/
export interface MeasureTickmaps {
tickmaps: TickMap[],
accidentalMap: Record<string | number, Record<string, TickAccidental>>,
accidentalArray: AccidentalArray[]
}
/**
* Column-mapped modifiers, managed by the {@link SmoScore}
* @category SmoObject
*/
export interface ColumnMappedParams {
// ['timeSignature', 'keySignature', 'tempo']
timeSignature: any,
keySignature: string,
tempo: any
}
// @internal
export type SmoMeasureNumberParam = 'transposeIndex' | 'activeVoice' | 'lines' | 'repeatCount';
// @internal
export const SmoMeasureNumberParams: SmoMeasureNumberParam[] = ['transposeIndex', 'activeVoice', 'lines', 'repeatCount'];
// @internal
export type SmoMeasureStringParam = 'keySignature';
// @internal
export const SmoMeasureStringParams: SmoMeasureStringParam[] = ['keySignature'];
/**
* constructor parameters for a {@link SmoMeasure}. Usually you will call
* {@link SmoMeasure.defaults}, and modify the parameters you need to change.
*
* @param timeSignature
* @param keySignature
* @param tuplets
* @param transposeIndex calculated from {@link SmoPartInfo} for non-concert-key instruments
* @param lines number of lines in the stave
* @param staffY Y coordinate (UL corner) of the measure stave
* @param measureNumber combination configured/calculated measure number
* @param clef
* @param voices
* @param activeVoice the active voice in the editor
* @param tempo
* @param format measure format, is managed by the score
* @param modifiers All measure modifiers that5 aren't format, timeSignature or tempo
* @category SmoObject
*/
export interface SmoMeasureParams {
timeSignature: TimeSignature,
keySignature: string,
tupletTrees: SmoTupletTree[],
transposeIndex: number,
lines: number,
// bars: [1, 1], // follows enumeration in VF.Barline
measureNumber: MeasureNumber,
clef: Clef,
voices: SmoVoice[],
activeVoice: number,
tempo: SmoTempoText,
format: SmoMeasureFormat | null,
modifiers: SmoMeasureModifierBase[],
repeatSymbol: boolean,
repeatCount: number
}
/**
* The serializeable bits of SmoMeasure. Some parameters are
* mapped by the stave if the don't change every measure, e.g.
* time signature.
* @category serialization
*/
export interface SmoMeasureParamsSer {
/**
* constructor
*/
ctor: string,
/**
* a list of tuplets (serialized)
*/
tupletTrees: SmoTupletTreeParamsSer[],
/**
* transpose the notes up/down. TODO: this should not be serialized
* as its part of the instrument parameters
*/
transposeIndex: number,
/**
* lines in the staff (e.g. percussion)
*/
lines: number,
/**
* measure number, absolute and relative/remapped
*/
measureNumber: MeasureNumber,
/**
* start clef
*/
clef: Clef,
/**
* voices contain notes
*/
voices: SmoVoiceSer[],
/**
* all other modifiers (barlines, etc)
*/
modifiers: SmoMeasureModifierBase[],
// the next 3 are not serialized as part of the measure in most cases, since they are
// mapped to specific measures in the score/system
/**
* key signature
*/
keySignature?: string,
/**
* time signature serialization
*/
timeSignature?: TimeSignatureParametersSer,
/**
* tempo at this point
*/
tempo: SmoTempoTextParamsSer
}
/**
* Only arrays and measure numbers are serilialized with default values.
* @param params - result of serialization
* @returns
*/
function isSmoMeasureParamsSer(params: Partial<SmoMeasureParamsSer>):params is SmoMeasureParamsSer {
if (!Array.isArray(params.voices) ||
!Array.isArray(params.tupletTrees) || !Array.isArray(params.modifiers) ||
typeof(params?.measureNumber?.measureIndex) !== 'number') {
return false;
}
return true;
}
/**
* Data for a measure of music. Many rules of musical engraving are
* enforced at a measure level: the duration of notes, accidentals, etc.
*
* Measures contain {@link SmoNote}, {@link SmoTuplet}, and {@link SmoBeamGroup}
* Measures are contained in {@link SmoSystemStaff}
* @category SmoObject
*/
export class SmoMeasure implements SmoMeasureParams, TickMappable {
static get timeSignatureDefault(): TimeSignature {
return new TimeSignature(TimeSignature.defaults);
}
static defaultDupleDuration: number = 4096;
static defaultTripleDuration: number = 2048 * 3;
// @internal
static readonly _defaults: SmoMeasureParams = {
timeSignature: SmoMeasure.timeSignatureDefault,
keySignature: 'C',
tupletTrees: [],
transposeIndex: 0,
modifiers: [],
// bars: [1, 1], // follows enumeration in VF.Barline
measureNumber: {
localIndex: 0,
systemIndex: 0,
measureIndex: 0,
staffId: 0
},
clef: 'treble',
lines: 5,
voices: [],
format: new SmoMeasureFormat(SmoMeasureFormat.defaults),
activeVoice: 0,
tempo: new SmoTempoText(SmoTempoText.defaults),
repeatSymbol: false,
repeatCount: 0
}
/**
* Default constructor parameters. Defaults are always copied so the
* caller can modify them to create a new measure.
* @returns constructor params for a new measure
*/
static get defaults(): SmoMeasureParams {
const proto: any = JSON.parse(JSON.stringify(SmoMeasure._defaults));
proto.format = new SmoMeasureFormat(SmoMeasureFormat.defaults);
proto.tempo = new SmoTempoText(SmoTempoText.defaults);
proto.modifiers.push(new SmoBarline({
position: SmoBarline.positions.start,
barline: SmoBarline.barlines.singleBar
}));
proto.modifiers.push(new SmoBarline({
position: SmoBarline.positions.end,
barline: SmoBarline.barlines.singleBar
}));
return proto;
}
// @ignore
static convertLegacyTimeSignature(ts: string) {
const rv = new TimeSignature(TimeSignature.defaults);
rv.timeSignature = ts;
return rv;
}
timeSignature: TimeSignature = SmoMeasure.timeSignatureDefault;
/**
* Overrides display of actual time signature, in the case of
* pick-up notes where the actual and displayed durations are different
*/
keySignature: string = '';
canceledKeySignature: string = '';
tupletTrees: SmoTupletTree[] = [];
repeatSymbol: boolean = false;
repeatCount: number = 0;
ctor: string='SmoMeasure';
/**
* Adjust for non-concert pitch intstruments
*/
transposeIndex: number = 0;
modifiers: SmoMeasureModifierBase[] = [];
/**
* Row, column, and custom numbering information about this measure.
*/
measureNumber: MeasureNumber = {
localIndex: 0,
systemIndex: 0,
measureIndex: 0,
staffId: 0
};
clef: Clef = 'treble';
voices: SmoVoice[] = [];
/**
* the active voice in the editor, if there are multiple voices
* */
activeVoice: number = 0;
tempo: SmoTempoText;
beamGroups: ISmoBeamGroup[] = [];
lines: number = 5;
/**
* Runtime information about rendering
*/
svg: MeasureSvg;
/**
* Measure-specific formatting parameters.
*/
format: SmoMeasureFormat;
/**
* Information for identifying this object
*/
id: string;
/**
* Fill in components. We assume the modifiers are already constructed,
* e.g. by deserialize or the calling function.
* @param params
*/
constructor(params: SmoMeasureParams) {
this.tempo = new SmoTempoText(SmoTempoText.defaults);
this.svg = {
staffWidth: 0,
unjustifiedWidth: 0,
staffX: 0,
staffY: 0,
logicalBox: {
x: 0, y: 0, width: 0, height: 0
},
yTop: 0,
adjX: 0,
maxColumnStartX: 0,
adjRight: 0,
history: [],
lineIndex: 0,
pageIndex: 0,
rowInSystem: 0,
forceClef: false,
forceKeySignature: false,
forceTimeSignature: false,
forceTempo: false,
hideEmptyMeasure: false,
hideMultimeasure: false,
multimeasureLength: 0,
multimeasureEndBarline: SmoBarline.barlines['singleBar'],
element: null
};
const defaults = SmoMeasure.defaults;
SmoMeasureNumberParams.forEach((param) => {
if (typeof (params[param]) !== 'undefined') {
this[param] = params[param];
}
});
SmoMeasureStringParams.forEach((param) => {
this[param] = params[param] ? params[param] : defaults[param];
});
this.clef = params.clef;
this.repeatSymbol = params.repeatSymbol;
this.measureNumber = JSON.parse(JSON.stringify(params.measureNumber));
if (params.tempo) {
this.tempo = new SmoTempoText(params.tempo);
}
// Handle legacy time signature format
if (params.timeSignature) {
const tsAny = params.timeSignature as any;
if (typeof (tsAny) === 'string') {
this.timeSignature = SmoMeasure.convertLegacyTimeSignature(tsAny);
} else {
this.timeSignature = TimeSignature.createFromPartial(tsAny);
}
}
this.voices = params.voices ? params.voices : [];
this.tupletTrees = params.tupletTrees ? params.tupletTrees : [];
this.modifiers = params.modifiers ? params.modifiers : defaults.modifiers;
this.setDefaultBarlines();
this.keySignature = SmoMusic.vexKeySigWithOffset(this.keySignature, this.transposeIndex);
if (!(params.format)) {
this.format = new SmoMeasureFormat(SmoMeasureFormat.defaults);
this.format.measureIndex = this.measureNumber.measureIndex;
} else {
this.format = new SmoMeasureFormat(params.format);
}
this.id = getId().toString();
this.updateClefChangeNotes();
}
// @internal
// used for serialization
static get defaultAttributes() {
return [
'keySignature',
'measureNumber',
'activeVoice', 'clef', 'transposeIndex',
'format', 'rightMargin', 'lines', 'repeatSymbol', 'repeatCount'
];
}
// @internal
// used for serialization
static get formattingOptions() {
return ['customStretch', 'customProportion', 'autoJustify', 'systemBreak',
'pageBreak', 'padLeft'];
}
// @internal
// used for serialization
static get columnMappedAttributes() {
return ['timeSignature', 'keySignature', 'tempo'];
}
static get serializableAttributes() {
const rv: any = [];
SmoMeasure.defaultAttributes.forEach((attr) => {
if (SmoMeasure.columnMappedAttributes.indexOf(attr) < 0 && attr !== 'format') {
rv.push(attr);
}
});
return rv;
}
/**
// Return true if the time signatures are the same, for display purposes (e.g. if a time sig change
// is required)
*/
static timeSigEqual(o1: TimeSignature, o2: TimeSignature) {
return o1.timeSignature === o2.timeSignature && o1.useSymbol === o2.useSymbol;
}
/**
* If there is a clef change mid-measure, update the actual clefs of the notes
* so they display correctly.
*/
updateClefChangeNotes() {
let changed = false;
let curTick = 0;
let clefChange = this.clef;
for (var i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
curTick = 0;
for (var j = 0; j < voice.notes.length; ++j) {
const smoNote = voice.notes[j];
smoNote.clef = this.clef;
if (smoNote.clefNote && smoNote.clefNote.clef !== this.clef) {
clefChange = smoNote.clefNote.clef;
curTick += smoNote.tickCount;
changed = true;
break;
}
curTick += smoNote.tickCount;
}
if (changed) {
break;
}
}
if (!changed) {
return;
}
// clefChangeTick is where the change goes. We only support
// one per measure, others are ignored.
const clefChangeTick = curTick;
for (var i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
curTick = 0;
for (var j = 0; j < voice.notes.length; ++j) {
const smoNote = voice.notes[j];
const noteTicks = smoNote.tickCount;
if (curTick + noteTicks >= clefChangeTick) {
smoNote.clef = clefChange;
}
// Remove any redundant clef changes later in the measure
if (curTick + noteTicks > clefChangeTick) {
if (smoNote.clefNote && smoNote.clefNote.clef === clefChange) {
smoNote.clefNote = null;
}
}
curTick += noteTicks;
}
}
}
/**
* @internal
* @returns column mapped parameters, serialized. caller will
* decide if the parameters need to be persisted
*/
serializeColumnMapped(): ColumnMappedParams {
//
return {
timeSignature: this.timeSignature.serialize(),
keySignature: this.keySignature,
tempo: this.tempo.serialize()
};
}
getColumnMapped(): ColumnMappedParams {
return {
timeSignature: this.timeSignature,
keySignature: this.keySignature,
tempo: this.tempo
};
}
/**
* Convert this measure object to a JSON object, recursively serializing all the notes,
* note modifiers, etc.
*/
serialize(): SmoMeasureParamsSer {
const params: Partial<SmoMeasureParamsSer> = { "ctor": "SmoMeasure" };
let ser = true;
smoSerialize.serializedMergeNonDefault(SmoMeasure.defaults, SmoMeasure.serializableAttributes, this, params);
// Don't serialize default things
const fmt = this.format.serialize();
// measure number can't be defaulted b/c tempos etc. can map to default measure
params.measureNumber = JSON.parse(JSON.stringify(this.measureNumber));
params.tupletTrees = [];
params.voices = [];
params.modifiers = [];
this.tupletTrees.forEach((tupletTree) => {
params.tupletTrees!.push(tupletTree.serialize());
});
this.voices.forEach((voice) => {
const obj: any = {
notes: []
};
voice.notes.forEach((note) => {
obj.notes.push(note.serialize());
});
params.voices!.push(obj);
});
this.modifiers.forEach((modifier) => {
ser = true;
/* don't serialize default modifiers */
if (modifier.ctor === 'SmoBarline' && (modifier as SmoBarline).position === SmoBarline.positions.start &&
(modifier as SmoBarline).barline === SmoBarline.barlines.singleBar) {
ser = false;
} else if (modifier.ctor === 'SmoBarline' && (modifier as SmoBarline).position === SmoBarline.positions.end
&& (modifier as SmoBarline).barline === SmoBarline.barlines.singleBar) {
ser = false;
} else if (modifier.ctor === 'SmoTempoText') {
// we don't save tempo text as a modifier anymore
ser = false;
} else if ((modifier as SmoRepeatSymbol).ctor === 'SmoRepeatSymbol' && (modifier as SmoRepeatSymbol).position === SmoRepeatSymbol.positions.start
&& (modifier as SmoRepeatSymbol).symbol === SmoRepeatSymbol.symbols.None) {
ser = false;
}
if (ser) {
params.modifiers!.push(modifier.serialize());
}
});
// ['timeSignature', 'keySignature', 'tempo']
if (!isSmoMeasureParamsSer(params)) {
throw 'invalid measure';
}
return params;
}
/**
* Due to a bug, some tuplets have incorrect ticks. Fix it when we are deserializing the measure
* @param voices
* @param tupletTree
*/
static fixTupletLengths(voices: SmoVoice[], tupletTree: SmoTupletTree) {
const voice = voices[tupletTree.voice];
const notear = [];
for (let i = tupletTree.startIndex; i <= tupletTree.endIndex; ++i) {
if (voice.notes.length > i) {
notear.push(voice.notes[i]);
}
}
if (notear.length === (tupletTree.endIndex - tupletTree.startIndex + 1)) {
const tickSum = notear.map((x) => x.tickCount).reduce((a,b) => a + b);
if (tupletTree.totalTicks > tickSum) {
notear[0].ticks.numerator += tupletTree.totalTicks - tickSum;
}
}
}
/**
* restore a serialized measure object. Usually called as part of deserializing a score,
* but can also be used to restore a measure due to an undo operation. Recursively
* deserialize all the notes and modifiers to construct a new measure.
* @param jsonObj the serialized SmoMeasure
* @returns
*/
static deserialize(jsonObj: SmoMeasureParamsSer): SmoMeasure {
let j = 0;
let i = 0;
const voices: SmoVoice[] = [];
for (j = 0; j < jsonObj.voices.length; ++j) {
const voice = jsonObj.voices[j];
const notes: SmoNote[] = [];
voices.push({
notes
});
for (i = 0; i < voice.notes.length; ++i) {
const noteParams = voice.notes[i];
const smoNote = SmoNote.deserialize(noteParams);
notes.push(smoNote);
}
}
const modifiers: SmoMeasureModifierBase[] = [];
jsonObj.modifiers.forEach((modParams: any) => {
const modifier: SmoMeasureModifierBase = SmoMeasureModifierBase.deserialize(modParams);
modifiers.push(modifier);
});
const params: SmoMeasureParams = SmoMeasure.defaults;
smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, jsonObj, params);
// explode column-mapped
if (jsonObj.tempo) {
params.tempo = SmoTempoText.deserialize(jsonObj.tempo);
} else {
params.tempo = new SmoTempoText(SmoTempoText.defaults);
}
// timeSignatureString is now part of timeSignature. upconvert old scores
let timeSignatureString = '';
const jsonLegacy = (jsonObj as any);
if (typeof(jsonLegacy.timeSignatureString) === 'string' && jsonLegacy.timeSignatureString.length > 0) {
timeSignatureString = jsonLegacy.timeSignatureString;
}
if (jsonObj.timeSignature) {
if (timeSignatureString.length) {
jsonObj.timeSignature.displayString = timeSignatureString;
}
params.timeSignature = TimeSignature.deserialize(jsonObj.timeSignature);
} else {
const tparams = TimeSignature.defaults;
if (timeSignatureString.length) {
tparams.displayString = timeSignatureString;
}
params.timeSignature = new TimeSignature(tparams);
}
params.keySignature = jsonObj.keySignature ?? 'C';
params.voices = voices;
if ((jsonObj as any).tupletTrees !== undefined) {
for (j = 0; j < jsonObj.tupletTrees.length; ++j) {
const tupletTreeJson = jsonObj.tupletTrees[j];
const tupletTree = SmoTupletTree.deserialize(tupletTreeJson);
params.tupletTrees.push(tupletTree);
SmoMeasure.fixTupletLengths(voices, tupletTree);
}
}
//deserialization of a legacy tuplets
//legacy schema had measure.tuplets, it is measure.tupletTrees now
if ((jsonObj as any).tuplets !== undefined) {
for (j = 0; j < (jsonObj as any).tuplets.length; ++j) {
const tupJson = (jsonObj as any).tuplets[j];
// Legacy schema had attrs.id, now it is just id
if ((tupJson as any).attrs && (tupJson as any).attrs.id) {
tupJson.id = (tupJson as any).attrs.id;
}
const tupletNotes: SmoNote[] = [];
let startIndex: number | null = null;
params.voices.forEach((voice) => {
voice.notes.forEach((note, index) => {
// backwards-compatibiliity, some scores don't have attrs on tuplet
if (typeof(tupJson.id) === 'string') {
tupJson.attrs = { type: 'SmoTuplet', id: tupJson.id };
}
const id = tupJson.attrs.id;
if (note.isTuplet && note.tupletId === id) {
tupletNotes.push(note);
//we cannot trust startIndex coming from legacy json
//we need to count index of the first note in the tuplet
if (startIndex === null) {
startIndex = index;
}
}
});
});
// Bug fix: A tuplet with no notes may be been overwritten
// in a copy/paste operation
if (tupletNotes.length > 0) {
tupJson.notes = tupletNotes;
tupJson.startIndex = startIndex;
tupJson.endIndex = tupJson.startIndex + tupletNotes.length - 1;
}
const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson);
const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet});
params.tupletTrees.push(tupletTree);
}
}
if (params.tupletTrees.length) {
SmoTupletTree.syncTupletIds(params.tupletTrees, voices)
}
params.modifiers = modifiers;
const measure = new SmoMeasure(params);
// Handle migration for measure-mapped parameters
measure.modifiers.forEach((mod) => {
if (mod.ctor === 'SmoTempoText') {
measure.tempo = (mod as SmoTempoText);
}
});
if (!measure.tempo) {
measure.tempo = new SmoTempoText(SmoTempoText.defaults);
}
return measure;
}
static clone(measure: SmoMeasure): SmoMeasure {
return SmoMeasure.deserialize(measure.serialize());
}
static cloneForPasteOrUndo(measure: SmoMeasure) {
const clonedMeasure = SmoMeasure.clone(measure);
clonedMeasure.svg = measure.svg;
// Ordinarily, the key/tempo/time is mapped to the stave, but since we are pasting measure-by
// measure here, we want to preserve it.
clonedMeasure.keySignature = measure.keySignature;
clonedMeasure.timeSignature = new TimeSignature(measure.timeSignature);
clonedMeasure.tempo = new SmoTempoText(measure.tempo);
return clonedMeasure;
}
/**
* When creating a new measure, the 'default' settings can vary depending on
* what comes before/after the measure. This determines the default pitch
* for a clef (appears on 3rd line)
*/
static get defaultPitchForClef(): Record<Clef, Pitch> {
return {
'treble': {
letter: 'b',
accidental: 'n',
octave: 4
},
'bass': {
letter: 'd',
accidental: 'n',
octave: 3
},
'tenor': {
letter: 'a',
accidental: 'n',
octave: 3
},
'alto': {
letter: 'c',
accidental: 'n',
octave: 4
},
'soprano': {
letter: 'b',
accidental: 'n',
octave: 4
},
'percussion': {
letter: 'b',
accidental: 'n',
octave: 4
},
'mezzo-soprano': {
letter: 'b',
accidental: 'n',
octave: 4
},
'baritone-c': {
letter: 'b',
accidental: 'n',
octave: 3
},
'baritone-f': {
letter: 'e',
accidental: 'n',
octave: 3
},
'subbass': {
letter: 'd',
accidental: '',
octave: 2
},
'french': {
letter: 'b',
accidental: '',
octave: 4
} // no idea
};
}
static _emptyMeasureNoteType: NoteType = 'r';
static set emptyMeasureNoteType(tt: NoteType) {
SmoMeasure._emptyMeasureNoteType = tt;
}
static get emptyMeasureNoteType(): NoteType {
return SmoMeasure._emptyMeasureNoteType;
}
static timeSignatureNotes(timeSignature: TimeSignature, clef: Clef) {
const pitch = SmoMeasure.defaultPitchForClef[clef];
const maxTicks = SmoMusic.timeSignatureToTicks(timeSignature.timeSignature);
const noteTick = 8192 / (timeSignature.beatDuration / 2);
let ticks = 0;
const pnotes: SmoNote[] = [];
while (ticks < maxTicks) {
const nextNote = SmoNote.defaults;
nextNote.pitches = [JSON.parse(JSON.stringify(pitch))];
nextNote.noteType = 'r';
nextNote.clef = clef;
nextNote.ticks.numerator = noteTick;
nextNote.stemTicks = noteTick;
pnotes.push(new SmoNote(nextNote));
ticks += noteTick;
}
if (timeSignature.beatDuration === 8 && (timeSignature.actualBeats % 3 === 0 || timeSignature.actualBeats % 2 !== 0)) {
let ix = 0;
pnotes.forEach((pnote) => {
if ((ix + 1) % 3 === 0) {
pnote.beamState = SmoNote.beamStates.end;
}
pnote.beamBeats = 2048 * 3;
ix += 1;
});
}
return pnotes;
}
/**
* Get a measure full of default notes for a given timeSignature/clef.
* returns 8th notes for triple-time meters, etc.
* @param params
* @returns
*/
static getDefaultNotes(params: SmoMeasureParams): SmoNote[] {
return SmoMeasure.timeSignatureNotes(new TimeSignature(params.timeSignature), params.clef);
}
/**
* When creating a new measure, the 'default' settings can vary depending on
* what comes before/after the measure. This determines the defaults from the
* parameters that are passed in, which could be another measure in the score.
* This version returns params with no notes, for callers that want to use their own notes.
* If you want the default notes, see {@link getDefaultMeasureWithNotes}
*
* @param params
* @returns
*/
static getDefaultMeasure(params: SmoMeasureParams): SmoMeasure {
const obj: any = {};
smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, SmoMeasure.defaults, obj);
smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, params, obj);
// Don't copy column-formatting options to new measure in new column
smoSerialize.serializedMerge(SmoMeasure.formattingOptions, SmoMeasure.defaults, obj);
obj.timeSignature = new TimeSignature(params.timeSignature);
// The measure expects to get concert KS in constructor and adjust for instrument. So do the
// opposite.
obj.keySignature = SmoMusic.vexKeySigWithOffset(obj.keySignature, -1 * obj.transposeIndex);
obj.lines = params.lines;
// Don't redisplay tempo for a new measure
const rv = new SmoMeasure(obj);
if (rv.tempo && rv.tempo.display) {
rv.tempo.display = false;
}
return rv;
}
/**
* When creating a new measure, the 'default' settings can vary depending on
* what comes before/after the measure. This determines the defaults from the
* parameters that are passed in, which could be another measure in the score.
*
* @param params
* @returns
*/
static getDefaultMeasureWithNotes(params: SmoMeasureParams): SmoMeasure {
var measure = SmoMeasure.getDefaultMeasure(params);
measure.voices.push({
notes: SmoMeasure.getDefaultNotes(params)
});
// fix a bug.
// new measures only have 1 voice, make sure active voice is 0
measure.activeVoice = 0;
return measure;
}
/**
* used by xml export
* @internal
* @param val
*/
getForceSystemBreak() {
return this.format.systemBreak;
}
// @internal
setDefaultBarlines() {
if (!this.getStartBarline()) {
this.modifiers.push(new SmoBarline({
position: SmoBarline.positions.start,
barline: SmoBarline.barlines.singleBar
}));
}
if (!this.getEndBarline()) {
this.modifiers.push(new SmoBarline({
position: SmoBarline.positions.end,
barline: SmoBarline.barlines.singleBar
}));
}
}
get containsSound(): boolean {
let i = 0;
for (i = 0; i < this.voices.length; ++i) {
let j = 0;
const voice = this.voices[i];
for (j = 0; j < this.voices.length; ++j) {
if (voice.notes[j].noteType === 'n') {
return true;
}
}
}
return false;
}
/**
* The rendered width of the measure, or estimate of same
*/
get staffWidth() {
return this.svg.staffWidth;
}
/**
* set the rendered width of the measure, or estimate of same
*/
setWidth(width: number, description: string) {
if (layoutDebug.flagSet(layoutDebug.values.measureHistory)) {
this.svg.history.push('setWidth ' + this.staffWidth + '=> ' + width + ' ' + description);
}
if (isNaN(width)) {
throw ('NAN in setWidth');
}
this.svg.staffWidth = width;
}
/**
* Get rendered or estimated start x
*/
get staffX(): number {
return this.svg.staffX;
}
/**
* Set rendered or estimated start x
*/
setX(x: number, description: string) {
if (isNaN(x)) {
throw ('NAN in setX');
}
layoutDebug.measureHistory(this, 'staffX', x, description);
this.svg.staffX = Math.round(x);
}
/**
* A time signature has possibly changed. add/remove notes to
* match the new length
*/
alignNotesWithTimeSignature() {
const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature);
let aligned = true;
for (let i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
if (this.getTicksFromVoice(i) !== tsTicks) {
aligned = false;
break;
}
}
if (aligned) {
return true;
}
const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => {
const fitNote = new SmoNote(SmoNote.defaults);
const duration = SmoMusic.closestDurationTickLtEq(target);
if (duration > 128) {
fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 };
fitNote.stemTicks = duration;
fitNote.pitches = note.pitches;
fitNote.noteType = note.noteType;
fitNote.clef = note.clef;
ar.push(fitNote);
}
}
const voices: SmoVoice[] = [];
const tuplets: SmoTuplet[] = [];
for (var i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
const newNotes: SmoNote[] = [];
let voiceTicks = 0;
for (var j = 0; j < voice.notes.length; ++j) {
const note = voice.notes[j];
// if a tuplet, make sure the whole tuplet fits.
if (note.isTuplet) {
const tupletTree = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletTrees, i, j);
if (tupletTree) {
// remaining notes of an approved tuplet, just add them
if (tupletTree.startIndex !== j) {
newNotes.push(note);
continue;
}
else if (tupletTree.totalTicks + voiceTicks <= tsTicks) {
// first note of the tuplet, it fits, add it
voiceTicks += tupletTree.totalTicks;
newNotes.push(note);
} else {
// tuplet will not fit. Replace tuplet with a note as close to remainder as possible and add it
// remove tuplet
note.tupletId = null
replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);
voiceTicks = tsTicks;
SmoTupletTree.removeTupletForNoteIndex(this, i, j);
break;
}
} else { // missing tuplet, now what?
console.warn('missing tuplet info');
replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);
voiceTicks = tsTicks;
}
} else {
if (note.tickCount + voiceTicks <= tsTicks) {
newNotes.push(note);
voiceTicks += note.tickCount;
} else {
replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);
voiceTicks = tsTicks;
break;
}
}
}
if (tsTicks - voiceTicks > 128) {
const np = SmoNote.defaults;
np.clef = this.clef;
const nnote = new SmoNote(np);
replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote);
}
voices.push({ notes: newNotes });
}
this.voices = voices;
}
get measureNumberDbg(): string {
return `${this.measureNumber.measureIndex}/${this.measureNumber.systemIndex}/${this.measureNumber.staffId}`;
}
/**
* Get rendered or estimated start y
*/
get staffY(): number {
return this.svg.staffY;
}
/**
* Set rendered or estimated start y
*/
setY(y: number, description: string) {
if (isNaN(y)) {
throw ('NAN in setY');
}
layoutDebug.measureHistory(this, 'staffY', y, description);
this.svg.staffY = Math.round(y);
}
/**
* Return actual or estimated highest point in score
*/
get yTop(): number {
return this.svg.yTop;
}
/**
* return the lowest y (highest value) in this measure svg
*
* @readonly
*/
get lowestY(): number {
if (this.svg.tabStaveBox) {
return this.svg.tabStaveBox.y + this.svg.tabStaveBox.height;
} else {
return this.svg.logicalBox.y + this.svg.logicalBox.height;
}
}
/**
* adjust the y for the render boxes to account for the page and margins
*/
adjustY(yOffset: number) {
this.svg.logicalBox.y += yOffset;
if (this.svg.tabStaveBox) {
this.svg.tabStaveBox.y += yOffset;
}
}
/**
* WHen setting an instrument, offset the pitches to match the instrument key
* @param offset
* @param newClef
*/
transposeToOffset(offset: number, targetKey: string, newClef?: Clef) {
const diff = offset - this.transposeIndex;
newClef = newClef ?? this.clef;
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
const pitches: number[] = [...Array(note.pitches.length).keys()];
// when the note is a rest, preserve the rest but match the new clef.
if (newClef !== this.clef && note.noteType === 'r') {
// @ts-ignore
const defp = JSON.parse(JSON.stringify(SmoMeasure.defaultPitchForClef[newClef]));
note.pitches = [defp];
} else {
note.transpose(pitches, diff, this.keySignature, targetKey);
note.getGraceNotes().forEach((gn) => {
const gpitch: number[] = [...Array(gn.pitches.length).keys()];
const xpose = SmoNote.transpose(gn, gpitch, diff, this.keySignature, targetKey);
gn.pitches = xpose.pitches;
});
}
});
});
}
/**
* Return actual or estimated highest point in score
*/
setYTop(y: number, description: string) {
layoutDebug.measureHistory(this, 'yTop', y, description);
this.svg.yTop = y;
}
/**
* Return actual or estimated bounding box
*/
setBox(box: SvgBox, description: string) {
layoutDebug.measureHistory(this, 'logicalBox', box, description);
this.svg.logicalBox = SvgHelpers.smoBox(box);
}
/**
* @returns the DOM identifier for this measure when rendered
*/
getClassId() {
return 'mm-' + this.measureNumber.staffId + '-' + this.measureNumber.measureIndex;
}
/**
*
* @param id
* @returns
*/
getRenderedNote(id: string) {
let j = 0;
let i = 0;
for (j = 0; j < this.voices.length; ++j) {
const voice = this.voices[j];
for (i = 0; i < voice.notes.length; ++i) {
const note = voice.notes[i];
if (note.renderId === id) {
return {
smoNote: note,
voice: j,
tick: i
};
}
}
}
return null;
}
getNotes() {
return this.voices[this.activeVoice].notes;
}
getActiveVoice() {
return this.activeVoice;
}
setActiveVoice(vix: number) {
if (vix >= 0 && vix < this.voices.length) {
this.activeVoice = vix;
}
}
getSwapVoicePairs() {
const rv = [];
for (let i = 0; i < this.voices.length; ++i) {
for (let j = i + 1; j < this.voices.length; ++j) {
rv.push([i, j]);
}
}
return rv;
}
swapVoices(voice1: number, voice2: number) {
if (this.voices.length > voice1 && this.voices.length > voice2) {
const v1 = this.voices[voice1];
const v2 = this.voices[voice2];
const nvoices: SmoVoice[] = [];
for (let i = 0; i < this.voices.length; ++i) {
if (i === voice1) {
nvoices.push(v2);
} else if (i === voice2) {
nvoices.push(v1);
} else {
nvoices.push(this.voices[i]);
}
}
this.voices = nvoices;
}
}
tickmapForVoice(voiceIx: number) {
return new TickMap(this, voiceIx);
}
// ### createMeasureTickmaps
// A tickmap is a map of notes to ticks for the measure. It is speciifc per-voice
// since each voice may have different numbers of ticks. The accidental map is
// overall since accidentals in one voice apply to accidentals in the other
// voices. So we return the tickmaps and the overall accidental map.
createMeasureTickmaps(): MeasureTickmaps {
let i = 0;
const tickmapArray: TickMap[] = [];
const accidentalMap: Record<string | number, Record<string, TickAccidental>> =
{} as Record<string | number, Record<PitchLetter, TickAccidental>>;
for (i = 0; i < this.voices.length; ++i) {
tickmapArray.push(this.tickmapForVoice(i));
}
for (i = 0; i < this.voices.length; ++i) {
const tickmap: TickMap = tickmapArray[i];
const durationKeys: string[] = Object.keys((tickmap.durationAccidentalMap));
durationKeys.forEach((durationKey: string) => {
if (!accidentalMap[durationKey]) {
accidentalMap[durationKey] = tickmap.durationAccidentalMap[durationKey];
} else {
const amap = accidentalMap[durationKey];
const tickable: Record<PitchLetter, TickAccidental> = tickmap.durationAccidentalMap[durationKey];
const letterKeys: PitchLetter[] = Object.keys(tickable) as Array<PitchLetter>;
letterKeys.forEach((pitchKey) => {
if (!amap[pitchKey]) {
amap[pitchKey] = tickmap.durationAccidentalMap[durationKey][pitchKey];
}
});
}
});
}
// duration: duration, pitches: Record<PitchLetter,TickAccidental>
const accidentalArray: AccidentalArray[] = [];
Object.keys(accidentalMap).forEach((durationKey) => {
accidentalArray.push({ duration: durationKey, pitches: accidentalMap[durationKey] });
});
return {
tickmaps: tickmapArray,
accidentalMap,
accidentalArray
};
}
// ### createRestNoteWithDuration
// pad some duration of music with rests.
static createRestNoteWithDuration(duration: number, clef: Clef): SmoNote {
const pitch: Pitch = JSON.parse(JSON.stringify(
SmoMeasure.defaultPitchForClef[clef]));
const note = new SmoNote(SmoNote.defaults);
note.pitches = [pitch];
note.noteType = 'r';
note.hidden = true;
note.ticks = { numerator: duration, denominator: 1, remainder: 0 };
return note;
}
/**
* Count the number of ticks in each voice and return max
* @returns
*/
getMaxTicksVoice() {
let i = 0;
let max = 0;
for (i = 0; i < this.voices.length; ++i) {
const voiceTicks = this.getTicksFromVoice(i);
max = Math.max(voiceTicks, max);
}
return max;
}
/**
* For pasting, paste into the target measure if the voice exists, else paste into
* voice 0
* @param voiceIndex
* @returns
*/
getTicksFromThisOrAnyVoice(voiceIndex: number): number {
if (this.voices.length > voiceIndex) {
return this.getTicksFromVoice(voiceIndex);
} else {
return this.getTicksFromVoice(0);
}
}
/**
* Count the number of ticks in a specific voice
* @param voiceIndex
* @returns
*/
getTicksFromVoice(voiceIndex: number): number {
let ticks = 0;
this.voices[voiceIndex].notes.forEach((note) => {
ticks += note.tickCount;
});
return ticks;
}
/**
* Count all the ticks up to the provided tickIndex
* @param voiceIndex
* @param tickIndex
*/
getNotePositionInTicks(voiceIndex: number, tickIndex: number): number {
let rv = 0;
for (let i = 0; i < tickIndex; i++) {
const note = this.voices[voiceIndex].notes[i];
rv += note.tickCount;
}
return rv;
}
/**
* Count all the ticks up to the provided tickIndex
* @param voiceIndex
* @param tickIndex
*/
getTickCountForNote(voiceIndex: number, note: SmoNote): number {
let rv = 0;
for (let i = 0; i < this.voices[voiceIndex].notes.length; i++) {
const currentNote = this.voices[voiceIndex].notes[i];
rv += note.tickCount;
}
return rv;
}
getClosestIndexFromTickCount(voiceIndex: number, tickCount: number): number {
let i = 0;
let rv = 0;
for (i = 0; i < this.voices[voiceIndex].notes.length; ++i) {
const note = this.voices[voiceIndex].notes[i];
if (note.tickCount + rv >= tickCount) {
return i;
}
rv += note.tickCount;
}
return i;
}
isPickup(): boolean {
const ticks = this.getTicksFromVoice(0);
const goal = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature);
return (ticks < goal);
}
clearBeamGroups() {
this.beamGroups = [];
}
// ### updateLyricFont
// Update the lyric font, which is the same for all lyrics.
setLyricFont(fontInfo: FontInfo) {
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
note.setLyricFont(fontInfo);
});
});
}
setLyricAdjustWidth(adjustNoteWidth: boolean) {
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
note.setLyricAdjustWidth(adjustNoteWidth);
});
});
}
setChordAdjustWidth(adjustNoteWidth: boolean) {
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
note.setChordAdjustWidth(adjustNoteWidth);
});
});
}
// ### updateLyricFont
// Update the lyric font, which is the same for all lyrics.
setChordFont(fontInfo: FontInfo) {
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
note.setChordFont(fontInfo);
});
});
}
tupletNotes(smoTuplet: SmoTuplet): SmoNote[] {
let tupletNotes: SmoNote[] = [];
for (let i = smoTuplet.startIndex; i <= smoTuplet.endIndex; i++) {
const note = this.voices[smoTuplet.voice].notes[i];
tupletNotes.push(note);
}
return tupletNotes;
}
getStemDirectionForTuplet(smoTuplet: SmoTuplet) {
let note: SmoNote | null = null;
for (let currentNote of this.tupletNotes(smoTuplet)) {
if (currentNote.noteType === 'n') {
note = currentNote;
break;
}
}
if (!note) {
return SmoNote.flagStates.down;
}
if (note.flagState !== SmoNote.flagStates.auto) {
return note.flagState;
}
return SmoMusic.pitchToLedgerLine(this.clef, note.pitches[0])
>= 2 ? SmoNote.flagStates.up : SmoNote.flagStates.down;
}
getNoteById(id: string): SmoNote | null {
for (var i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
for (var j = 0; j < voice.notes.length; ++j) {
const note = voice.notes[j];
if (note.attrs.id === id) {
return note;
}
}
}
return null;
}
setClef(clef: Clef) {
const oldClef = this.clef;
this.clef = clef;
this.voices.forEach((voice) => {
voice.notes.forEach((note) => {
note.clef = clef;
});
});
}
/**
* Get the clef that this measure ends with.
* @returns
*/
getLastClef() {
for (var i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
for (var j = 0; j < voice.notes.length; ++j) {
const note = voice.notes[j];
if (note.clefNote && note.clefNote.clef !== this.clef) {
return note.clefNote.clef;
}
}
}
return this.clef;
}
isRest() {
let i = 0;
for (i = 0; i < this.voices.length; ++i) {
const voice = this.voices[i];
for (var j = 0; j < voice.notes.length; ++j) {
if (!voice.notes[j].isRest()) {
return false;
}
}
}
return true;
}
// ### populateVoice
// Create a new voice in this measure, and populate it with the default note
// for this measure/key/clef
populateVoice(index: number) {
if (index !== this.voices.length) {
return;
}
this.voices.push({ notes: SmoMeasure.getDefaultNotes(this) });
this.activeVoice = index;
}
private _removeSingletonModifier(name: string) {
const ar = this.modifiers.filter(obj => obj.attrs.type !== name);
this.modifiers = ar;
}
addRehearsalMark(parameters: SmoRehearsalMarkParams) {
this._removeSingletonModifier('SmoRehearsalMark');
this.modifiers.push(new SmoRehearsalMark(parameters));
}
removeRehearsalMark() {
this._removeSingletonModifier('SmoRehearsalMark');
}
getRehearsalMark(): SmoMeasureModifierBase | undefined {
return this.modifiers.find(obj => obj.attrs.type === 'SmoRehearsalMark');
}
getModifiersByType(type: string) {
return this.modifiers.filter((mm) => type === mm.attrs.type);
}
setTempo(params: SmoTempoTextParams) {
this.tempo = new SmoTempoText(params);
}
/**
* Set measure tempo to the default {@link SmoTempoText}
*/
resetTempo() {
this.tempo = new SmoTempoText(SmoTempoText.defaults);
}
getTempo() {
if (typeof (this.tempo) === 'undefined') {
this.tempo = new SmoTempoText(SmoTempoText.defaults);
}
return this.tempo;
}
/**
* Measure text is deprecated, and may not be supported in the future.
* Better to use SmoTextGroup and attach to the measure.
* @param mod
* @returns
*/
addMeasureText(mod: SmoMeasureModifierBase) {
var exist = this.modifiers.filter((mm) =>
mm.attrs.id === mod.attrs.id
);
if (exist.length) {
return;
}
this.modifiers.push(mod);
}
getMeasureText() {
return this.modifiers.filter(obj => obj.ctor === 'SmoMeasureText');
}
removeMeasureText(id: string) {
var ar = this.modifiers.filter(obj => obj.attrs.id !== id);
this.modifiers = ar;
}
se