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,296 lines (1,248 loc) • 36.1 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
/**
* A note modifier is anything that is mapped to the note, but not part of the
* pitch itself. This includes grace notes, and note-text like lyrics.
* @module /smo/data/noteModifiers
*/
import { SmoAttrs, Ticks, Pitch, getId, SmoObjectParams, Transposable, SvgBox, SmoModifierBase,
Clef, IsClef, SmoDynamicCtor,
IsPitchLetter, ElementLike} from './common';
import { smoSerialize } from '../../common/serializationHelpers';
import { SmoMusic } from './music';
import { defaultNoteScale, FontInfo, getChordSymbolGlyphFromCode } from '../../common/vex';
/**
* A note modifier is anything that is mapped to the note, but not part of the
* pitch itself. This includes grace notes, and note-text like lyrics.
* All note modifiers have a serialize method and a 'ctor' parameter or deserialization
* @category SmoObject
*/
export abstract class SmoNoteModifierBase implements SmoModifierBase {
attrs: SmoAttrs;
ctor: string;
logicalBox: SvgBox | null = null;
element: ElementLike = null;
constructor(ctor: string) {
this.attrs = {
id: getId().toString(),
type: ctor
};
this.ctor = ctor;
}
static deserialize(jsonObj: SmoObjectParams) {
// Handle backwards-compatibility thing
if (jsonObj.ctor === 'SmoMicrotone' && typeof ((jsonObj as any).pitch) === 'number') {
(jsonObj as any).pitchIndex = (jsonObj as any).pitch;
}
if (jsonObj.ctor === 'SmoLyric') {
if (typeof((jsonObj as any)._text) === 'string') {
(jsonObj as any).text = (jsonObj as any)._text;
}
}
if (typeof (SmoDynamicCtor[jsonObj.ctor]) === 'undefined') {
console.log('ouch bad ctor for ' + jsonObj.ctor);
}
try {
const rv = SmoDynamicCtor[jsonObj.ctor](jsonObj);
return rv;
} catch (exp) {
console.error('no ctor for ' + jsonObj.ctor);
throw(exp);
}
}
abstract serialize(): any;
}
export function isClefChangeParamsSer(params: Partial<SmoClefChangeParamsSer>): params is SmoClefChangeParamsSer {
if (typeof(params.clef) === 'string' && params.ctor === 'SmoClefChange') {
return true;
}
return false;
}
/**
* @category SmoObject
*/
export interface SmoClefChangeParams {
clef: string
}
/**
* @category serialization
*/
export interface SmoClefChangeParamsSer extends SmoClefChangeParams {
/**
* constructor
*/
ctor: string;
/**
* attributes for ID
*/
attrs: SmoAttrs;
}
/**
* @category SmoObject
*/
export class SmoClefChange extends SmoNoteModifierBase {
clef: Clef;
static get defaults() {
const rv: SmoClefChangeParamsSer = JSON.parse(JSON.stringify({
clef: 'treble',
ctor: 'SmoClefChange',
attrs: {
id: getId(),
type: 'SmoClefChange'
}
}));
return rv;
}
constructor(clefParams: SmoClefChangeParams) {
super('SmoClefChange');
const clef = clefParams.clef;
if (!IsClef(clef)) {
this.clef = 'treble';
} else {
this.clef = clef as Clef;
}
}
serialize(): SmoClefChangeParamsSer {
const params: Partial<SmoClefChangeParamsSer> = { ctor: 'SmoClefChange' };
params.clef = this.clef;
if (!isClefChangeParamsSer(params)) {
throw('corrupt clef change');
}
return params;
}
}
/**
* used to construct {@link SmoGraceNote}
* beam group.
* @category SmoObject
*/
export interface GraceNoteParams extends SmoModifierBase {
/**
* up, down, or auto
*/
flagState: number,
/**
* same as for {@link SmoNote}
*/
noteType: string,
/**
* same as for {@link SmoNote}
*/
beamBeats: number,
/**
* same as for {@link SmoNote}. Indicates break in beam group
*/
endBeam: boolean,
/**
* should be same as note?
*/
clef: string,
/**
* there's probably a name for this...
*/
slash: boolean,
/**
* only used for beaming
*/
ticks: Ticks,
/**
* Pitch, same as for {@link SmoNote}
*/
pitches: Pitch[],
}
/**
* serialized grace note
* @category serialization
*/
export interface GraceNoteParamsSer extends GraceNoteParams {
/**
* constructor
*/
ctor: string;
/**
* attributes for ID
*/
attrs: SmoAttrs;
}
function isGraceNoteParamsSer(params: Partial<GraceNoteParamsSer>): params is GraceNoteParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoGraceNote') {
return false;
}
return true;
}
/**
* A grace notes has many of the things an 'actual' note can have, but it doesn't take up
* time against the time signature
* @category SmoObject
*/
export class SmoGraceNote extends SmoNoteModifierBase implements Transposable {
static get flagStates() {
return { auto: 0, up: 1, down: 2 };
}
static get defaults(): GraceNoteParams {
return JSON.parse(JSON.stringify({
flagState: SmoGraceNote.flagStates.auto,
noteType: 'n',
beamBeats: 4096,
endBeam: false,
clef: 'treble',
slash: false,
ticks: {
numerator: 4096,
denominator: 1,
remainder: 0
},
pitches: [{
letter: 'b',
octave: 4,
accidental: ''
}]
}));
}
// TODO: Matches SmoNote - move to SmoMusic?
static get parameterArray() {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoGraceNote.defaults) {
rv.push(key);
}
return rv;
}
ticks: Ticks = SmoGraceNote.defaults.ticks;
pitches: Pitch[] = [];
slash: boolean = false;
clef: string = 'treble';
noteType: string = 'n';
renderId: string | null = null;
hasTabNote: boolean = false;
tickCount() {
return this.ticks.numerator / this.ticks.denominator + this.ticks.remainder;
}
toVexGraceNote() {
const p = SmoMusic.smoPitchesToVex(this.pitches);
const rv = { duration: SmoMusic.closestVexDuration(this.tickCount()), keys: p, slash: this.slash };
return rv;
}
serialize(): GraceNoteParamsSer {
const params: Partial<GraceNoteParamsSer> = { ctor: 'SmoGraceNote' };
smoSerialize.serializedMergeNonDefault(SmoGraceNote.defaults,
SmoGraceNote.parameterArray, this, params);
if (!isGraceNoteParamsSer(params)) {
throw 'bad grace note ' + JSON.stringify(params);
}
return params;
}
constructor(parameters: Partial<GraceNoteParams>) {
super('SmoGraceNote');
smoSerialize.serializedMerge(SmoGraceNote.parameterArray, SmoGraceNote.defaults, this);
smoSerialize.serializedMerge(SmoGraceNote.parameterArray, parameters, this);
}
}
export type SmoArpeggioType = 'directionless' | 'rasquedo_up' | 'rasquedo_down'
| 'roll_up' | 'roll_down' | 'brush_up' | 'brush_down' | 'none';
export const SmoArpeggioTypes = ['directionless', 'rasquedo_up', 'rasquedo_down',
'roll_up', 'roll_down', 'brush_up', 'brush_down', 'none'];
/**
* @category SmoObject
*/
export interface SmoArpeggioParams {
type: SmoArpeggioType
}
/**
* @category serialization
*/
export interface SmoArpeggioParamsSer {
ctor: string;
/**
* stringified arpeggion enumeration
*/
type: string;
}
function isSmoArpeggionParamsSer(params: Partial<SmoArpeggioParamsSer>): params is SmoArpeggioParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoArpeggio') {
return false;
}
return true;
}
export function isArpeggioType(tp: SmoArpeggioType | string): tp is SmoArpeggioType {
return SmoArpeggioTypes.indexOf(tp) >= 0;
}
/**
* A 'splatter' symbol next to a chord.
* @category SmoObject
*/
export class SmoArpeggio extends SmoNoteModifierBase {
static _types: Record<string, number> = {};
static get types() {
if (typeof(SmoArpeggio._types['directionless']) === 'undefined') {
SmoArpeggio._types['directionless'] = 7;
SmoArpeggio._types['rasquedo_up'] = 6;
SmoArpeggio._types['rasquedo_down'] = 5;
SmoArpeggio._types['roll_up'] = 4;
SmoArpeggio._types['roll_down'] = 3;
SmoArpeggio._types['brush_up'] = 2;
SmoArpeggio._types['brush_down'] = 1;
SmoArpeggio._types['none'] = 0;
}
return SmoArpeggio._types;
}
typeCode: number;
constructor(params: SmoArpeggioParams) {
super('SmoArpeggio');
this.typeCode = SmoArpeggio.types[params.type];
}
get typeString(): SmoArpeggioType {
const str = SmoArpeggioTypes.find((x) => SmoArpeggio.types[x] === this.typeCode);
const type = str ? str : 'none';
return type as SmoArpeggioType;
}
serialize(): SmoArpeggioParamsSer {
const rv: Partial<SmoArpeggioParamsSer> = { ctor: 'SmoArpeggio' }
const str = SmoArpeggioTypes.find((x) => SmoArpeggio.types[x] === this.typeCode);
rv.type = str ? str : 'none';
if (!isSmoArpeggionParamsSer(rv)) {
throw 'bad arpeggio ' + JSON.stringify(rv);
}
return rv;
}
}
/**
* Constructor parameters for {@link SmoMicrotone}
* @category SmoObject
*/
export interface SmoMicrotoneParams extends SmoObjectParams {
/**
* indicates which modifier to alter the tone (e.g. 1/4 sharp)
*/
tone: string,
/**
* the index of the pitch to alter
*/
pitch: number
}
/**
* serialized microtones.
* @category serialization
*/
export interface SmoMicrotoneParamsSer extends SmoMicrotoneParams {
ctor: string,
attrs: SmoAttrs
}
function isSmoMicrotoneParamsSer(params: Partial<SmoMicrotoneParamsSer>): params is SmoMicrotoneParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoMicrotone') {
return false;
}
return true;
}
/**
* Microtones are treated similarly to ornaments. There are not
* rules for persisting throughout a measure, cancel etc.
* @category SmoObject
*/
export class SmoMicrotone extends SmoNoteModifierBase {
tone: string = 'flat75sz';
pitchIndex: number = 0;
// This is how VexFlow notates them
static readonly smoToVex: Record<string, string> = {
flat75sz: 'db',
flat25sz: 'd',
flat25ar: 'bs',
flat125ar: 'afhf',
sharp75: '++',
sharp125: 'ashs',
sharp25: '+',
sori: 'o',
koron: 'k'
}
// The audio frequency offsets
static readonly pitchCoeff: Record<string, number> = {
flat75sz: -1.5,
flat25sz: -0.5,
flat25ar: -0.5,
flat125ar: -2.5,
sharp75: 1.5,
sharp125: 2.5,
sharp25: 0.5,
sori: 0.5,
koron: -0.5
}
get toPitchCoeff(): number {
return SmoMicrotone.pitchCoeff[this.tone];
}
get toVex(): string {
return SmoMicrotone.smoToVex[this.tone];
}
static get defaults(): SmoMicrotoneParams {
return JSON.parse(JSON.stringify({
ctor: 'SmoMicrotone',
tone: 'flat25sz',
pitch: 0
}));
}
static get parameterArray() {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoMicrotone.defaults) {
rv.push(key);
}
return rv;
}
serialize(): SmoMicrotoneParamsSer {
const params: Partial<SmoMicrotoneParamsSer> = { ctor: 'SmoMicrotone' };
smoSerialize.serializedMergeNonDefault(SmoMicrotone.defaults,
SmoMicrotone.parameterArray, this, params);
if (!isSmoMicrotoneParamsSer(params)) {
throw 'bad microtone ' + JSON.stringify(params);
}
return params;
}
constructor(parameters: SmoMicrotoneParams) {
super(parameters.ctor);
smoSerialize.serializedMerge(SmoMicrotone.parameterArray, SmoMicrotone.defaults, this);
smoSerialize.serializedMerge(SmoMicrotone.parameterArray, parameters, this);
}
}
/**
* Constructor for {@link SmoOrnament}
* @category SmoObject
*/
export interface SmoOrnamentParams {
/**
* postition, above or below
*/
position?: string,
/**
* horizontal offset from note head
*/
offset?: string,
/**
* accidental above/below
*/
accidentalAbove?: string,
accidentalBelow?: string,
/**
* code for the ornament
*/
ornament: string,
}
/**
* serializable ornament
* @category serialization
*/
export interface SmoOrnamentParamsSer extends SmoOrnamentParams {
/**
* constructor
*/
ctor: string;
}
function isSmoOrnamentParamsSer(params: Partial<SmoOrnamentParamsSer>): params is SmoOrnamentParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoOrnament') {
return false;
}
return true;
}
/**
* Ornaments map to vex ornaments. articulations vs. ornaments
* is kind of arbitrary
* @category SmoObject
*/
export class SmoOrnament extends SmoNoteModifierBase {
static readonly ornaments: Record<string, string> = {
mordent: 'mordent',
mordent_inverted: 'mordent_inverted',
turn: 'turn',
turn_inverted: 'turn_inverted',
trill: 'tr',
upprall: 'upprall',
prallup: 'prallup',
pralldown: 'pralldown',
upmordent: 'upmordent',
downmordent: 'downmordent',
caesura: 'caesura',
lineprall: 'lineprall',
prallprall: 'prallprall',
scoop: 'scoop',
fall: 'fall',
fallLong: 'fallLong',
breath: 'breath',
doit: 'doit',
doitLong: 'doitLong',
flip: 'flip',
smear: 'smear',
bend: 'bend',
plungerClosed: 'plungerClosed',
plungerOpen: 'plungerOpen'
}
static readonly xmlOrnaments: Record<string, string> = {
mordent: 'mordent',
mordent_inverted: 'inverted-mordent',
turn: 'turn',
turn_inverted: 'inverted-turn',
upmordent: 'mordent',
downmordent: 'mordent',
lineprall: 'schleifer',
prallprall: 'schleifer',
prallup: 'schleifer',
tr: 'trill-mark'
}
static readonly textNoteOrnaments: Record<string, string> = {
breath: 'breath',
caesura: 'caesura_straight'
}
// jazz ornaments in vex are articulations in music xml
static readonly xmlJazz: Record<string, string> = {
doit: 'doit',
scoop: 'scoop',
dropLong: 'falloff',
drop: 'plop'
}
static get jazzOrnaments(): string[] {
return ['scoop', 'fallLong', 'doit', 'doitLong', 'flip', 'smear', 'scoop', 'plungerOpen', 'plungerClosed', 'bend'];
}
static get legacyJazz(): Record<string, string> {
return {'SCOOP': SmoOrnament.ornaments.scoop ,
'FALL_SHORT': SmoOrnament.ornaments.fall,
'FALL_LONG': SmoOrnament.ornaments.fallLong,
'DOIT': SmoOrnament.ornaments.doit,
'LIFT': SmoOrnament.ornaments.lift,
'FLIP': SmoOrnament.ornaments.flip,
'SMEAR': SmoOrnament.ornaments.smear
};
}
toVex() {
return SmoOrnament.ornaments[this.ornament];
}
isJazz() {
return SmoOrnament.jazzOrnaments.indexOf(this.ornament) >= 0;
}
position: string = SmoOrnament.positions.above;
offset: string = 'on';
ornament: string = SmoOrnament.ornaments.mordent;
static get parameterArray() {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoOrnament.defaults) {
rv.push(key);
}
return rv;
}
static get positions() {
return {
above: 'above',
below: 'below',
auto: 'auto'
};
}
static get offsets() {
return {
on: 'on',
after: 'after'
};
}
static get defaults(): SmoOrnamentParams {
return JSON.parse(JSON.stringify({
ctor: 'SmoOrnament',
ornament: SmoOrnament.ornaments.mordent,
position: SmoOrnament.positions.auto,
offset: SmoOrnament.offsets.on
}));
}
serialize(): SmoOrnamentParamsSer {
var params: Partial<SmoOrnamentParamsSer> = { ctor: 'SmoOrnament' };
smoSerialize.serializedMergeNonDefault(SmoOrnament.defaults,
SmoOrnament.parameterArray, this, params);
if (!isSmoOrnamentParamsSer(params)) {
throw 'bad ornament ' + JSON.stringify(params);
}
return params;
}
constructor(parameters: SmoOrnamentParams) {
super('SmoOrnament');
smoSerialize.serializedMerge(SmoOrnament.parameterArray, SmoOrnament.defaults, this);
smoSerialize.serializedMerge(SmoOrnament.parameterArray, parameters, this);
// handle some legacy changes
if (typeof(SmoOrnament.legacyJazz[this.ornament]) === 'string') {
this.ornament = SmoOrnament.legacyJazz[this.ornament];
}
}
}
/**
* Constructor parameters for {@link SmoArticulation}
* @category SmoObject
*/
export interface SmoArticulationParameters {
/**
* position, above or below
*/
position?: string,
/**
* x offset
*/
offset?: number,
/**
* articulation code
*/
articulation: string
}
/**
* @category serialization
*/
export interface SmoArticulationParametersSer extends SmoArticulationParameters {
ctor: string;
}
function isSmoArticulationParametersSer(params: Partial<SmoArticulationParametersSer>): params is SmoArticulationParametersSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoArticulation') {
return false;
}
return true;
}
/**
* Articulations map to notes, can be placed above/below
* @category SmoObject
*/
export class SmoArticulation extends SmoNoteModifierBase {
static get articulations(): Record<string, string> {
return {
accent: 'accent',
staccato: 'staccato',
marcato: 'marcato',
tenuto: 'tenuto',
upStroke: 'upStroke',
downStroke: 'downStroke',
pizzicato: 'pizzicato',
bowUp: 'bowUp',
bowDown: 'bowDown',
fermata: 'fermata'
};
}
static readonly xmlArticulations: Record<string, string> = {
accent: 'accent',
staccato: 'staccato',
tenuto: 'tenuto',
marcato: 'strong-accent'
}
static get positions() {
return {
above: 'above',
below: 'below',
auto: 'auto'
};
}
static get articulationToVex(): Record<string, string> {
return {
accent: 'a>',
staccato: 'a.',
marcato: 'a^',
tenuto: 'a-',
upStroke: 'a|',
downStroke: 'am',
pizzicato: 'ao',
fermata: 'a@a'
};
}
static get vexToArticulation(): Record<string, string> {
return {
'a>': 'accent',
'a.': 'staccato',
'a^': 'marcato',
'a-': 'tenuto',
'a|': 'upStroke',
'am': 'downStroke',
'ao': 'pizzicato',
'a@a': 'fermata'
};
}
static get parameterArray(): string[] {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoArticulation.defaults) {
rv.push(key);
}
return rv;
}
static get positionToVex(): Record<string, number> {
return {
'above': 3,
'below': 4
};
}
static get defaults(): SmoArticulationParameters {
return JSON.parse(JSON.stringify({
ctor: 'SmoArticulation',
position: SmoArticulation.positions.above,
articulation: SmoArticulation.articulations.accent
}));
}
position: string = SmoOrnament.positions.above;
offset: number = 0;
articulation: string = SmoArticulation.articulations.accent;
adjX: number = 0;
serialize(): SmoArticulationParametersSer {
var params: Partial<SmoArticulationParametersSer> = { ctor: 'SmoArticulation'};
smoSerialize.serializedMergeNonDefault(SmoArticulation.defaults,
SmoArticulation.parameterArray, this, params);
if (!isSmoArticulationParametersSer(params)) {
throw 'bad articulation ' + JSON.stringify(params);
}
return params;
}
constructor(parameters: SmoArticulationParameters) {
super('SmoArticulation');
smoSerialize.serializedMerge(SmoArticulation.parameterArray, SmoArticulation.defaults, this);
smoSerialize.serializedMerge(SmoArticulation.parameterArray, parameters, this);
// this.selector = parameters.selector;
}
}
/**
* @internal
*/
export interface VexAnnotationParams {
glyph?: string,
symbolModifier?: number,
text?: string
}
/**
* The persist-y parts of {@link SmoLyricParams}. We don't persist the selector
* since that can change based on the position of the parent note
* @category serialization
*/
export interface SmoLyricParamsSer extends SmoObjectParams {
/**
* constructor
*/
ctor: string,
/**
* attributes for ID
*/
attrs: SmoAttrs,
/**
* the lyric font
*/
fontInfo: FontInfo,
/**
* classes for styling
*/
classes: string,
/**
* which verse the lyric goes with
*/
verse: number,
/**
* lyrics are used for chord changes or annotations, parser is different for each
*/
parser: number,
/**
* indicates we should format for the width of the lyric
*/
adjustNoteWidthLyric: boolean,
/**
* indicates we should format for the width of the chord
*/
adjustNoteWidthChord: boolean,
/**
* fill color for text
*/
fill: string,
/**
* translate to align lyrics. Possibly this should not be serialized
*/
translateX: number,
/**
* translate to align lyrics. Possibly this should not be serialized
*/
translateY: number,
/**
* the actual text
*/
text: string | null
}
function isSmoLyricPersist(params: Partial<SmoLyricParamsSer>): params is SmoLyricParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoLyric') {
return false;
}
return true;
}
/**
* Used to construct a {@link SmoLyric} for both chords and lyrics
* @category SmoObject
*/
export interface SmoLyricParams {
/**
* the lyric font
*/
fontInfo: FontInfo,
/**
* classes for styling
*/
classes: string,
/**
* which verse the lyric goes with
*/
verse: number,
/**
* lyrics are used for chord changes or annotations, parser is different for each
*/
parser: number,
/**
* indicates we should format for the width of the lyric
*/
adjustNoteWidthLyric: boolean,
/**
* indicates we should format for the width of the chord
*/
adjustNoteWidthChord: boolean,
/**
* fill color for text
*/
fill: string,
/**
* translate to align lyrics. Possibly this should not be serialized
*/
translateX: number,
/**
* translate to align lyrics. Possibly this should not be serialized
*/
translateY: number,
/**
* the actual text
*/
text: string | null
}
/**
* SmoLyric covers both chords and lyrics. The parser tells you which
* one you get.
* @category SmoObject
*/
export class SmoLyric extends SmoNoteModifierBase {
static readonly parsers: Record<string, number> = {
lyric: 0, anaylysis: 1, chord: 2
}
static get defaults(): SmoLyricParams {
return JSON.parse(JSON.stringify({
ctor: 'SmoLyric',
text: '\xa0',
endChar: '',
verse: 0,
fontInfo: {
size: 12,
family: 'times',
style: 'normal',
weight: 'normal'
},
fill: 'black',
classes: 'score-text',
translateX: 0,
translateY: 0,
adjustNoteWidthLyric: true,
adjustNoteWidthChord: false,
parser: SmoLyric.parsers.lyric
}));
}
static get symbolPosition() {
return {
SUPERSCRIPT: 1,
SUBSCRIPT: 2,
NORMAL: 3
};
}
static get persistArray(): string[] {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoLyric.defaults) {
rv.push(key);
}
return rv;
}
static get parameterArray(): string[] {
const rv = SmoLyric.persistArray;
rv.push('selector', 'text');
return rv;
}
ctor: string = 'SmoLyric';
text: string = '';
fontInfo: FontInfo = {
size: 12,
family: 'Merriweather',
style: 'normal',
weight: 'normal'
};
parser: number = SmoLyric.parsers.lyric;
selector: string | null = null; // used by UI
adjustNoteWidthLyric: boolean = true;
adjustNoteWidthChord: boolean = false;
verse: number = 0;
skipRender: boolean = false;
fill: string = '';
translateX: number = 0;
translateY: number = 0;
classes: string = '';
// used by the renderer to calculate offsets for aligning lyrics
adjX: number = 0;
adjY: number = 0;
// used by the renderer to calculate the y offset for music that goes below the staff
musicYOffset: number = 0;
hyphenX: number = 0;
deleted: boolean = false;
serialize(): SmoLyricParamsSer {
var params: Partial<SmoLyricParamsSer> = { ctor: 'SmoLyric' };
smoSerialize.serializedMergeNonDefault(SmoLyric.defaults,
SmoLyric.persistArray, this, params);
if (!isSmoLyricPersist(params)) {
throw 'bad lyric ' + JSON.stringify('params');
}
return params;
}
// For lyrics, we default to adjust note width on lyric size. For chords, this is almost never what
// you want, so it is off by default.
get adjustNoteWidth() {
return (this.parser === SmoLyric.parsers.lyric) ? this.adjustNoteWidthLyric : this.adjustNoteWidthChord;
}
set adjustNoteWidth(val) {
if (this.parser === SmoLyric.parsers.lyric) {
this.adjustNoteWidthLyric = val;
} else {
this.adjustNoteWidthChord = val;
}
}
static transposeChordToKey(chord: SmoLyric, offset: number, srcKey: string, destKey: string): SmoLyric {
if (chord.parser !== SmoLyric.parsers.chord || offset === 0) {
return new SmoLyric(chord);
}
const nchord = new SmoLyric(chord);
let srcIx = 0;
const maxLen = chord.text.length - 1;
let destString = '';
while (srcIx < chord.text.length) {
let symbolBlock = false;
const nchar = chord.text[srcIx];
let lk = srcIx < maxLen ? chord.text[srcIx + 1] : null;
// make sure this chord start witha VEX pitch letter (A-G upper case)
if (IsPitchLetter(nchar.toLowerCase()) && nchar == nchar.toUpperCase()) {
if (lk === '@') {
symbolBlock = true;
srcIx += 1;
lk = srcIx < maxLen ? chord.text[srcIx + 1] : null;
}
const pitch: Pitch = {letter: nchar.toLowerCase() as any, accidental: 'n', octave: 4 };
if (lk !== null && (lk === 'b' || lk === '#')) {
pitch.accidental = lk;
srcIx += 1;
}
const npitch = SmoMusic.transposePitchForKey(pitch, srcKey, destKey, offset);
destString += npitch.letter.toUpperCase();
if (symbolBlock) {
destString += '@';
}
if (npitch.accidental !== 'n') {
destString += npitch.accidental;
}
} else {
destString += nchar;
}
srcIx += 1;
}
nchord.text = destString;
return nchord;
}
// ### getClassSelector
// returns a selector used to find this text block within a note.
getClassSelector(): string {
var parser = (this.parser === SmoLyric.parsers.lyric ? 'lyric' : 'chord');
return 'g.' + parser + '-' + this.verse;
}
setText(text: string) {
// For chords, trim all whitespace
if (this.parser !== SmoLyric.parsers.lyric) {
if (text.trim().length) {
text.replace(/\s/g, '');
}
}
this.text = text;
}
isHyphenated() {
const text = this.text.trim();
return this.parser === SmoLyric.parsers.lyric &&
text.length &&
text[text.length - 1] === '-';
}
getText() {
const text = this.text.trim();
if (this.isHyphenated()) {
return smoSerialize.tryParseUnicode(text.substr(0, text.length - 1)).trim();
}
return smoSerialize.tryParseUnicode(text);
}
isDash() {
return this.getText().length === 0 && this.isHyphenated();
}
static _chordGlyphFromCode(code: string) {
return getChordSymbolGlyphFromCode(code);
}
static _tokenizeChordString(str: string) {
// var str = this._text;
const reg = /^([A-Z|a-z|0-9|]+)/g;
let mmm = str.match(reg);
let tokeType: string = '';
let toke: string = '';
const tokens: string[] = [];
while (str.length) {
if (!mmm) {
tokeType = str[0];
tokens.push(tokeType);
str = str.slice(1, str.length);
} else {
toke = mmm[0].substr(0, mmm[0].length);
str = str.slice(toke.length, str.length);
tokens.push(toke);
tokeType = '';
toke = '';
}
mmm = str.match(reg);
}
return tokens;
}
constructor(parameters: SmoLyricParams) {
super('SmoLyric');
smoSerialize.serializedMerge(SmoLyric.parameterArray, SmoLyric.defaults, this);
smoSerialize.serializedMerge(SmoLyric.parameterArray, parameters, this);
if (typeof(this.fontInfo.size) !== 'number') {
this.fontInfo.size = SmoLyric.defaults.fontInfo.size;
}
// backwards-compatibility for lyric text
if (parameters.text) {
this.text = parameters.text;
}
// calculated adjustments for alignment purposes
this.adjY = 0;
this.adjX = 0;
// this.verse = parseInt(this.verse, 10);
if (!this.attrs) {
this.attrs = {
id: getId().toString(),
type: 'SmoLyric'
};
}
}
}
/**
* Used to create a {@link SmoBarline}
* @category SmoObject
*/
export interface SmoNoteBarParams {
barline: number
}
export interface SmoNoteBarParamsSer extends SmoNoteBarParams {
ctor: string,
barline: number
}
export class SmoNoteBar extends SmoNoteModifierBase {
barline: number = SmoNoteBar.defaults.barline;
static get defaults(): SmoNoteBarParams {
return { barline: SmoNoteBar.barlines.noBar }
}
static get parameterArray() {
return ['barline'];
}
static readonly barlines: Record<string, number> = {
singleBar: 0,
doubleBar: 1,
endBar: 2,
startRepeat: 3,
endRepeat: 4,
noBar: 5
}
constructor(parameters: SmoNoteBarParams) {
super('SmoNoteBar');
smoSerialize.serializedMerge(SmoNoteBar.parameterArray, SmoNoteBar.defaults, this);
smoSerialize.serializedMerge(SmoLyric.parameterArray, parameters, this);
}
serialize(): SmoNoteBarParamsSer {
const parameters: Partial<SmoNoteBarParamsSer> = {};
smoSerialize.serializedMergeNonDefault(SmoNoteBar.defaults,
SmoNoteBar.parameterArray, this, parameters);
parameters.ctor = 'SmoNoteBar';
return parameters as SmoNoteBarParamsSer;
}
}
/**
* The persisted bits of {@link SmoDynamicTextParams}
* @category serialization
*/
export interface SmoDynamicTextSer extends SmoObjectParams {
ctor: string,
xOffset: number,
fontSize: number,
yOffsetLine: number,
yOffsetPixels: number,
text: string
}
/**
* Constructor parameters for {@link SmoDynamicText}
* @category SmoObject
*/
export interface SmoDynamicTextParams extends SmoDynamicTextSer {
ctor: string,
xOffset: number,
fontSize: number,
yOffsetLine: number,
yOffsetPixels: number,
text: string
}
/**
* Dynamic text tells you how loud not to play.
* @category SmoObject
*/
export class SmoDynamicText extends SmoNoteModifierBase {
static get dynamics(): Record<string, string> {
// matches VF.modifier
return {
PP: 'pp',
P: 'p',
MP: 'mp',
MF: 'mf',
F: 'f',
FF: 'ff',
SFZ: 'sfz'
};
}
static get defaults(): SmoDynamicTextParams {
return JSON.parse(JSON.stringify({
ctor: 'SmoDynamicText',
xOffset: 0,
fontSize: defaultNoteScale,
yOffsetLine: 11,
yOffsetPixels: 0,
text: SmoDynamicText.dynamics.MP,
}));
}
static get persistArray(): string[] {
const rv: string[] = [];
// eslint-disable-next-line
for (const key in SmoDynamicText.defaults) {
rv.push(key);
}
return rv;
}
static get parameterArray(): string[] {
const rv = SmoDynamicText.persistArray;
rv.push('selector');
return rv;
}
text: string = '';
yOffsetLine: number = 11;
yOffsetPixels: number = 0;
xOffset: number = 0;
fontSize: number = defaultNoteScale;
serialize(): object {
var params = {};
smoSerialize.serializedMergeNonDefault(SmoDynamicText.defaults,
SmoDynamicText.persistArray, this, params);
return params;
}
constructor(parameters: SmoDynamicTextParams) {
super('SmoDynamicText');
smoSerialize.vexMerge(this, SmoDynamicText.defaults);
smoSerialize.filteredMerge(SmoDynamicText.parameterArray, parameters, this);
if (!this.attrs) {
this.attrs = {
id: getId().toString(),
type: 'SmoDynamicText'
};
}
}
}
/**
* @category SmoObject
*/
export interface SmoTabBend {
bendType: number,
release: boolean,
text: string
}
/**
* @category SmoObject
*/
export interface SmoFretPosition {
string: number,
fret: number
}
/**
* @category SmoObject
*/
export interface SmoTabNoteParams {
positions: SmoFretPosition[]
noteId: string,
flagState: number,
flagThrough: boolean,
noteHead: number,
isAssigned: boolean
}
/**
* @category serialization
*/
export interface SmoTabNoteParamsSer extends SmoTabNoteParams {
ctor: string
}
function isSmoTabNoteParamsSer(params: Partial<SmoTabNoteParamsSer>): params is SmoTabNoteParamsSer {
if (typeof(params.ctor) !== 'string' || params.ctor !== 'SmoTabNote') {
return false;
}
return true;
}
/**
* @category SmoObject
*/
export class SmoTabNote extends SmoNoteModifierBase {
static get defaults(): SmoTabNoteParams {
return JSON.parse(JSON.stringify({
positions: [],
noteId: '',
isAssigned: false,
flagState: SmoTabNote.flagStates.None,
flagThrough: false,
noteHead: SmoTabNote.noteHeads.number
}));
}
positions: SmoFretPosition[];
noteId: string;
isAssigned: boolean;
noteHead: number;
flagState: number;
flagThrough: boolean;
static get flagStates() {
return { None: 0, Up: 1, Down: -1 };
}
static get noteHeads() {
return { number: 0, x: 1 };
}
constructor(params: SmoTabNoteParams) {
super('SmoTabNote');
this.positions = params.positions
this.noteId = params.noteId;
this.isAssigned = params.isAssigned;
this.noteHead = params.noteHead;
this.flagState = params.flagState;
this.flagThrough = params.flagThrough;
}
serialize(): SmoTabNoteParamsSer {
var params = { ctor: 'SmoTabNote' };
smoSerialize.serializedMergeNonDefault(SmoTabNote.defaults,
['positions', 'noteId', 'isAssigned', 'noteHead', 'flagState', 'flagThrough'], this, params);
if (!isSmoTabNoteParamsSer(params)) {
throw 'bad params in SmoTabNote';
}
return params;
}
}
export const noteModifierDynamicCtorInit = () => {
SmoDynamicCtor['SmoArpeggio'] = (params: SmoArpeggioParams) => new SmoArpeggio(params);
SmoDynamicCtor['SmoMicrotone'] = (params: SmoMicrotoneParams) => new SmoMicrotone(params);
SmoDynamicCtor['SmoOrnament'] = (params: SmoOrnamentParams) => new SmoOrnament(params);
SmoDynamicCtor['SmoGraceNote'] = (params: GraceNoteParams) => new SmoGraceNote(params);
SmoDynamicCtor['SmoArticulation'] = (params: SmoArticulationParameters) => new SmoArticulation(params);
SmoDynamicCtor['SmoLyric'] = (params: SmoLyricParams) => new SmoLyric(params);
SmoDynamicCtor['SmoNoteBar'] = (params: SmoNoteBarParams) => new SmoNoteBar(params);
SmoDynamicCtor['SmoDynamicText'] = (params: SmoDynamicTextParams) => new SmoDynamicText(params);
SmoDynamicCtor['SmoTabNote'] = (params: SmoTabNoteParams) => new SmoTabNote(params);
SmoDynamicCtor['SmoClefChange'] = (params: SmoClefChangeParams) => new SmoClefChange(params);
}