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
672 lines (668 loc) • 31.2 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
import { SmoMusic } from '../../smo/data/music';
import { SmoNote } from '../../smo/data/note';
import { SmoMeasure, SmoVoice, MeasureTickmaps } from '../../smo/data/measure';
import { SmoScore } from '../../smo/data/score';
import { SmoArticulation, SmoLyric, SmoOrnament } from '../../smo/data/noteModifiers';
import {VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments, getVexTuplets} from '../../common/vex';
import { SmoBarline, SmoRehearsalMark } from '../../smo/data/measureModifiers';
import { SmoSelection, SmoSelector } from '../../smo/xform/selections';
import { SmoSystemStaff } from '../../smo/data/systemStaff';
import { getId } from '../../smo/data/common';
import { SmoSystemGroup } from '../../smo/data/scoreModifiers';
import { StaffModifierBase, SmoStaffHairpin, SmoSlur, SmoTie, SmoStaffTextBracket } from '../../smo/data/staffModifiers';
import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, leftConnectorVx, rightConnectorVx,
toVexVolta, getVexChordBlocks } from '../../render/vex/smoAdapter';
import {SmoTuplet} from "../../smo/data/tuplet";
const VF = VexFlow;
export const fontStacks: Record<string, string[]> = {
Bravura: ['"Bravura"', '"Gonville"', '"Custom"'],
Gonville: ['"Gonville"', '"Bravura"', '"Custom"'],
Petaluma: ['"Petaluma"', '"Bravura"', '"Gonville"', '"Custom"'],
Leland: ['"Leland"', '"Bravura"', '"Gonville"', '"Custom"']
}
interface LyricAdjust {
verse: number, lyric: SmoLyric,
}
interface VexNoteRenderInfo {
smoNote: SmoNote,voiceIx: number, noteIx: number, tickmapObject: MeasureTickmaps, lyricAdj: string[]
}
interface VexStaveGroupMusic {
formatter: string, measures: SmoMeasure[], voiceStrings: string[], heightOffset: number,
systemGroup?: SmoSystemGroup
}
function smoNoteToVexKeys(smoNote: SmoNote) {
const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead;
const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead);
return keys;
}
function smoNoteToGraceNotes(smoNote: SmoNote, strs: string[]) {
const gar = smoNote.getGraceNotes();
var toBeam = true;
if (gar && gar.length) {
const grGroup: string[] = [];
gar.forEach((g) => {
const grid = g.attrs.id;
const args = JSON.stringify(g.toVexGraceNote());
strs.push(`const ${grid} = new VF.GraceNote(JSON.parse('${args}'))`);
strs.push(`${grid}.setAttribute('id', '${grid}');`);
for (var i = 0; i < g.pitches.length; ++i) {
const pitch = g.pitches[i];
if (!pitch.accidental) {
console.warn('no accidental in grace note');
}
if (pitch.accidental && pitch.accidental !== 'n' || pitch.cautionary) {
const acid = 'acc' + i.toString() + grid;
strs.push(`const ${acid} = new VF.Accidental('${pitch.accidental}');`);
if (pitch.cautionary) {
strs.push(`${acid}.setAsCautionary();`);
}
strs.push(`${grid}.addModifier(${acid}, ${i})`);
}
}
if (g.tickCount() >= 4096) {
toBeam = false;
}
grGroup.push(grid);
});
const ggid = 'ggrp' + smoNote.attrs.id;
const grString = '[' + grGroup.join(',') + ']';
strs.push(`const ${ggid} = new VF.GraceNoteGroup(${grString});`);
if (toBeam) {
strs.push(`${ggid}.beamNotes();`);
}
strs.push(`${smoNote.attrs.id}.addModifier(${ggid}, 0);`);
}
}
function smoNoteToStaveNote(smoNote: SmoNote) {
const duration = SmoMusic.ticksToDuration[smoNote.stemTicks];
const sn: StaveNoteStruct = {
clef: smoNote.clef,
duration,
dots: smoNote.dots,
type: smoNote.noteType
};
if (smoNote.flagState !== SmoNote.flagStates.auto) {
sn.stem_direction = smoNote.flagState === SmoNote.flagStates.up ? 1 : -1;
sn.auto_stem = false;
} else {
sn.auto_stem = true;
}
sn.keys = smoNoteToVexKeys(smoNote);
return sn;
}
export const getVoiceId = (smoMeasure:SmoMeasure, voiceIx: number) => {
return smoMeasure.id + 'v' + voiceIx.toString();
}
function lastNoteInSystem(smoScore: SmoScore, selection: SmoSelection) {
let rv = selection;
let next: SmoSelection | null = null;
next = SmoSelection.nextNoteSelection(smoScore, selection.selector.staff,
selection.selector.measure, selection.selector.voice, selection.selector.tick);
while (next) {
if (next.measure.svg.rowInSystem !== selection.measure.svg.rowInSystem) {
return rv;
break;
}
rv = next;
next = SmoSelection.nextNoteSelection(smoScore, next.selector.staff,
next.selector.measure, next.selector.voice, next.selector.tick);
}
return rv;
}
function createMeasureModifiers(smoMeasure: SmoMeasure, strs: string[]) {
const sb = smoMeasure.getStartBarline();
const eb = smoMeasure.getEndBarline();
const sym = smoMeasure.getRepeatSymbol();
const vxStave = 'stave' + smoMeasure.id;
if (smoMeasure.measureNumber.systemIndex !== 0 && sb.barline === SmoBarline.barlines.singleBar
&& smoMeasure.format.padLeft === 0) {
strs.push(`${vxStave}.setBegBarType(VF.Barline.type.NONE);`);
} else {
strs.push(`${vxStave}.setBegBarType(${toVexBarlineType(sb)});`);
}
if (smoMeasure.svg.multimeasureLength > 0 && !smoMeasure.svg.hideMultimeasure) {
const bl = vexBarlineType[smoMeasure.svg.multimeasureEndBarline];
strs.push(`${vxStave}.setEndBarType(${bl});`);
} else if (eb.barline !== SmoBarline.barlines.singleBar) {
const bl = toVexBarlineType(eb);
strs.push(`${vxStave}.setEndBarType(${bl});`);
}
if (smoMeasure.svg.rowInSystem === 0) {
const rmb = smoMeasure.getRehearsalMark();
const rm = rmb as SmoRehearsalMark;
if (rm) {
strs.push(`${vxStave}.setSection('${rm.symbol}', 0);`);
}
}
const tempo = smoMeasure.getTempo();
if (tempo && smoMeasure.svg.forceTempo) {
const vexTempo = tempo.toVexTempo();
const tempoString = JSON.stringify(vexTempo);
strs.push(`${vxStave}.setTempo(JSON.parse('${tempoString}'), -1 * ${tempo.yOffset});`);
}
}
export function renderVoltas(smoScore: SmoScore, startMeasure: number, endMeasure: number, strs: string[]) {
const voltas = smoScore.staves[0].getVoltaMap(startMeasure, endMeasure);
for (var i = 0; i < voltas.length; ++i) {
const ending = voltas[i];
for (var j = ending.startBar; j <= ending.endBar; ++j) {
const smoMeasure = smoScore.staves[0].measures[j];
const vtype = toVexVolta(ending, smoMeasure.measureNumber.measureIndex);
const vx = smoMeasure.staffX + ending.xOffsetStart;
const vxStave = 'stave' + smoMeasure.id;
const endingName = ending.attrs.id + smoMeasure.id;
strs.push(`const ${endingName} = new VF.Volta(${vtype}, '${ending.number.toString()}', ${vx}, ${ending.yOffset});`);
strs.push(`${endingName}.setContext(context).draw(${vxStave}, -1 * ${ending.xOffsetEnd});`);
}
}
}
function renderModifier(modifier: StaffModifierBase, startNote: SmoNote | null, endNote: SmoNote | null, strs: string[]) {
const modifierName = getId();
const startKey = SmoSelector.getNoteKey(modifier.startSelector);
const endKey = SmoSelector.getNoteKey(modifier.endSelector);
strs.push(`// modifier from ${startKey} to ${endKey}`);
if (modifier.ctor === 'SmoStaffHairpin' && startNote && endNote) {
const hp = modifier as SmoStaffHairpin;
const vxStart = startNote.attrs.id;
const vxEnd = startNote.attrs.id;
const hpParams = { first_note: vxStart, last_note: vxEnd };
strs.push(`const ${modifierName} = new VF.StaveHairpin({ first_note: ${vxStart}, last_note: ${vxEnd},
firstNote: ${vxStart}, lastNote: ${vxEnd} });`);
strs.push(`${modifierName}.setRenderOptions({ height: ${hp.height}, y_shift: ${hp.yOffset}, left_shift_px: ${hp.xOffsetLeft},right_shift_px: ${hp.xOffsetRight} });`);
strs.push(`${modifierName}.setContext(context).setPosition(${hp.position}).draw();`);
} else if (modifier.ctor === 'SmoSlur') {
const slur = modifier as SmoSlur;
const vxStart = startNote?.attrs?.id ?? 'null';
const vxEnd = endNote?.attrs?.id ?? 'null';
const svgPoint: SVGPoint[] = JSON.parse(JSON.stringify(slur.controlPoints));
let slurX = 0;
if (startNote === null || endNote === null) {
slurX = -5;
svgPoint[0].y = 10;
svgPoint[1].y = 10;
}
if (modifier.startSelector.staff === modifier.endSelector.staff) {
const hpParams = {
thickness: slur.thickness,
xShift: slurX,
yShift: slur.yOffset,
cps: svgPoint,
orientation: slur.orientation,
position: slur.position,
positionEnd: slur.position_end
};
const paramStrings = JSON.stringify(hpParams);
strs.push(`const ${modifierName} = new VF.Curve(${vxStart}, ${vxEnd}, JSON.parse('${paramStrings}'));`);
strs.push(`${modifierName}.setContext(context).draw();`);
}
} else if (modifier.ctor === 'SmoTie') {
const ctie = modifier as SmoTie;
const vxStart = startNote?.attrs?.id ?? 'null';
const vxEnd = endNote?.attrs?.id ?? 'null';
// TODO: handle case of overlap
if (modifier.startSelector.staff === modifier.endSelector.staff) {
if (ctie.lines.length > 0) {
// Hack: if a chord changed, the ties may no longer be valid. We should check
// this when it changes.
const fromLines = ctie.lines.map((ll) => ll.from);
const toLines = ctie.lines.map((ll) => ll.to);
strs.push(`const ${modifierName} = new VF.StaveTie({ first_note: ${vxStart}, last_note: ${vxEnd},
firstNote: ${vxStart}, lastNote: ${vxEnd}, first_indices: [${fromLines}], last_indices: [${toLines}]});`);
strs.push(`${modifierName}.setContext(context).draw();`);
}
}
} else if (modifier.ctor === 'SmoStaffTextBracket' && startNote && endNote) {
const ctext = modifier as SmoStaffTextBracket;
const vxStart = startNote.attrs.id;
const vxEnd = endNote.attrs.id;
if (vxStart && vxEnd) {
strs.push(`const ${modifierName} = new VF.TextBracket({ start: ${vxStart}, stop: ${vxEnd}, text: '${ctext.text}', position: ${ctext.position} });`);
strs.push(`${modifierName}.setLine(${ctext.line}).setContext(context).draw();`);
}
}
}
function renderModifiers(smoScore: SmoScore, staff: SmoSystemStaff,
startMeasure: number, endMeasure: number, strs: string[]) {
const modifiers = staff.renderableModifiers.filter((mm) => mm.startSelector.measure >= startMeasure && mm.endSelector.measure <= endMeasure);
modifiers.forEach((modifier) => {
const startNote = SmoSelection.noteSelection(smoScore,
modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);
const endNote = SmoSelection.noteSelection(smoScore,
modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);
// TODO: handle case of multiple line slur/tie
if (startNote && startNote.note && endNote && endNote.note) {
if (endNote.measure.svg.lineIndex !== startNote.measure.svg.lineIndex) {
const endFirst = lastNoteInSystem(smoScore, startNote);
if (endFirst && endFirst.note) {
const startLast = SmoSelection.noteSelection(smoScore, endNote.selector.staff,
endNote.selector.measure, 0, 0);
if (startLast && startLast.note) {
renderModifier(modifier, startNote.note, null, strs);
renderModifier(modifier, null, endNote.note, strs);
}
}
} else {
renderModifier(modifier, startNote.note, endNote.note, strs);
}
}
});
}
function createStaveNote(renderInfo: VexNoteRenderInfo, key: string, row: number, strs: string[]) {
const { smoNote, voiceIx, noteIx, tickmapObject, lyricAdj } = { ...renderInfo };
const id = smoNote.attrs.id;
const ctorInfo = smoNoteToStaveNote(smoNote);
const ctorString = JSON.stringify(ctorInfo);
if (smoNote.noteType === '/') {
strs.push(`const ${id} = new VF.GlyphNote(new VF.Glyph('repeatBarSlash', 40), { duration: '${ctorInfo.duration}' });`)
} else {
strs.push(`const ${id} = new VF.StaveNote(JSON.parse('${ctorString}'))`);
}
smoNoteToGraceNotes(smoNote, strs);
strs.push(`${id}.setAttribute('id', '${id}');`);
if (smoNote.fillStyle) {
strs.push(`${id}.setStyle({ fillStyle: '${smoNote.fillStyle}' });`);
} else if (voiceIx > 0) {
strs.push(`${id}.setStyle({ fillStyle: "#115511" });`);
} else if (smoNote.isHidden()) {
strs.push(`${id}.setStyle({ fillStyle: "#ffffff00" });`);
}
if (smoNote.noteType === 'n') {
smoNote.pitches.forEach((pitch, ix) => {
const zz = SmoMusic.accidentalDisplay(pitch, key,
tickmapObject.tickmaps[voiceIx].durationMap[noteIx], tickmapObject.accidentalArray);
if (zz) {
const aname = id + ix.toString() + 'acc';
strs.push(`const ${aname} = new VF.Accidental('${zz.symbol}');`);
if (zz.courtesy) {
strs.push(`${aname}.setAsCautionary();`);
}
strs.push(`${id}.addModifier(${aname}, ${ix});`);
}
});
}
for (var i = 0; i < smoNote.dots; ++i) {
for (var j = 0; j < smoNote.pitches.length; ++j) {
strs.push(`${id}.addModifier(new VF.Dot(), ${j});`);
}
}
smoNote.articulations.forEach((aa) => {
let smoPosition = aa.position;
if (smoPosition === 'auto') {
smoPosition = SmoMusic.positionFromStaffLine(smoNote);
}
const position: number = SmoArticulation.positionToVex[smoPosition];
const vexArt = SmoArticulation.articulationToVex[aa.articulation];
const sn = getId();
strs.push(`const ${sn} = new VF.Articulation('${vexArt}').setPosition(${position});`);
strs.push(`${id}.addModifier(${sn}, 0);`);
});
smoNote.getJazzOrnaments().forEach((ll) => {
const vexCode = ll.toVex();
strs.push(`const ${ll.attrs.id} = new VF.Ornament('${vexCode}');`)
strs.push(`${id}.addModifier(${ll.attrs.id}, 0);`);
});
smoNote.getOrnaments().forEach((ll) => {
const vexCode = vexOrnaments[ll.ornament];
strs.push(`const ${ll.attrs.id} = new VF.Ornament('${vexCode}');`);
if (ll.offset === SmoOrnament.offsets.after) {
strs.push(`${ll.attrs.id}.setDelayed(true);`);
}
strs.push(`${id}.addModifier(${ll.attrs.id}, 0);`);
});
const lyrics = smoNote.getTrueLyrics();
if (smoNote.noteType !== '/') {
lyrics.forEach((bll) => {
const ll = bll as SmoLyric;
let classString = 'lyric lyric-' + ll.verse;
let text = ll.getText();
if (!ll.skipRender) {
if (!text.length && ll.isHyphenated()) {
text = '-';
}
// no text, no hyphen, don't add it.
if (text.length) {
const sn = ll.attrs.id;
text = text.replace("'","\\'");
strs.push(`const ${sn} = new VF.Annotation('${text}');`);
strs.push(`${sn}.setAttribute('id', '${sn}');`);
const weight = ll.fontInfo.weight ?? 'normal';
strs.push(`${sn}.setFont('${ll.fontInfo.family}', ${ll.fontInfo.size}, '${weight}');`)
strs.push(`${sn}.setVerticalJustification(VF.Annotation.VerticalJustify.BOTTOM);`);
strs.push(`${id}.addModifier(${sn});`);
if (ll.adjY > 0) {
const adjy = Math.round(ll.adjY);
lyricAdj.push(`context.svg.getElementById('vf-${sn}').setAttributeNS('', 'transform', 'translate(0 ${adjy})');`);
}
if (ll.isHyphenated()) {
classString += ' lyric-hyphen';
}
strs.push(`${sn}.addClass('${classString}');`);
}
}
});
}
const chords = smoNote.getChords();
chords.forEach((chord) => {
strs.push(`const ${chord.attrs.id} = new VF.ChordSymbol();`);
strs.push(`${chord.attrs.id}.setAttribute('id', '${chord.attrs.id}');`);
const vblocks = getVexChordBlocks(chord);
vblocks.forEach((vblock) => {
const glyphParams = JSON.stringify(vblock);
if (vblock.glyph) {
strs.push(`${chord.attrs.id}.addGlyphOrText('${vblock.glyph}', JSON.parse('${glyphParams}'));`);
} else {
const btext = vblock.text ?? '';
if (btext.trim().length) {
strs.push(`${chord.attrs.id}.addGlyphOrText('${btext}', JSON.parse('${glyphParams}'));`);
}
}
});
strs.push(`${chord.attrs.id}.setFont('${chord.fontInfo.family}', ${chord.fontInfo.size}).setReportWidth(${chord.adjustNoteWidth});`);
strs.push(`${id}.addModifier(${chord.attrs.id}, 0);`);
});
return id;
}
function createColumn(groups: Record<string, VexStaveGroupMusic>, strs: string[]) {
const groupKeys = Object.keys(groups);
let maxXAdj = 0;
groupKeys.forEach((groupKey) => {
const music = groups[groupKey];
// Need to create beam groups before formatting
strs.push(`// create beam groups and tuplets for format grp ${groupKey} before formatting`);
music.measures.forEach((smoMeasure) => {
maxXAdj = Math.max(maxXAdj, smoMeasure.svg.adjX);
createBeamGroups(smoMeasure, strs);
createTuplets(smoMeasure, strs);
});
strs.push(' ');
strs.push(`// formatting measures in staff group ${groupKey}`);
// set x offset for alignment before format
music.measures.forEach((smoMeasure) => {
smoMeasure.voices.forEach((vv) => {
vv.notes.forEach((nn) => {
const id = nn.attrs.id;
const offset = maxXAdj - smoMeasure.svg.adjX;
strs.push(`${id}.setXShift(${offset});`);
});
});
});
const joinVoiceStr = '[' + music.voiceStrings.join(',') + ']';
const widthMeasure = music.measures[0];
const staffWidth = Math.round(widthMeasure.staffWidth -
(widthMeasure.svg.maxColumnStartX + widthMeasure.svg.adjRight + widthMeasure.format.padLeft) - 10);
strs.push(`${music.formatter}.format(${joinVoiceStr}, ${staffWidth});`);
music.measures.forEach((smoMeasure) => {
createMeasure(smoMeasure, music.heightOffset, strs);
});
});
}
function createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) {
smoMeasure.voices.forEach((voice, voiceIx) => {
const bgs = smoMeasure.beamGroups.filter((bb) => bb.voice === voiceIx);
for (var i = 0; i < bgs.length; ++i) {
const bg = bgs[i];
let keyNoteIx = bg.notes.findIndex((nn) => nn.noteType === 'n');
keyNoteIx = (keyNoteIx >= 0) ? keyNoteIx : 0;
const sdName = 'dir' + bg.attrs.id;
strs.push(`const ${sdName} = ${bg.notes[keyNoteIx].attrs.id}.getStemDirection();`);
const nar: string[] = [];
for (var j = 0; j < bg.notes.length; ++j) {
const note = bg.notes[j];
const vexNote = `${note.attrs.id}`;
if (note.noteType !== '/') {
nar.push(vexNote);
}
if (note.noteType !== 'n') {
continue;
}
strs.push(`${vexNote}.setStemDirection(${sdName});`);
}
const narString = '[' + nar.join(',') + ']';
strs.push(`const ${bg.attrs.id} = new VF.Beam(${narString});`);
}
});
}
function createTuplets(smoMeasure: SmoMeasure, strs: string[]) {
smoMeasure.voices.forEach((voice, voiceIx) => {
for (let i = 0; i < smoMeasure.tupletTrees.length; ++i) {
const tupletTree = smoMeasure.tupletTrees[i];
if (tupletTree.voice !== voiceIx) {
continue;
}
const traverseTupletTree = ( parentTuplet: SmoTuplet): void => {
const vexNotes = [];
for (let smoNote of smoMeasure.tupletNotes(parentTuplet)) {
const vexNote = `${smoNote.attrs.id}`;
vexNotes.push(vexNote);
}
const direction = smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ?
VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM;
const tpParams: TupletOptions = {
num_notes: parentTuplet.numNotes,
notes_occupied: parentTuplet.notesOccupied,
ratioed: parentTuplet.ratioed,
bracketed: parentTuplet.bracketed,
location: direction
};
const tpParamString = JSON.stringify(tpParams);
const vexNotesString = '[' + vexNotes.join(',') + ']';
strs.push(`const ${parentTuplet.attrs.id} = new VF.Tuplet(${vexNotesString}, JSON.parse('${tpParamString}'));`);
for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) {
const tuplet = parentTuplet.childrenTuplets[i];
traverseTupletTree(tuplet);
}
}
traverseTupletTree(tupletTree.tuplet);
}
});
}
function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: string[]) {
const ssid = 'stave' + smoMeasure.id;
const staffY = smoMeasure.svg.staffY + heightOffset;
const staffWidth = Math.round(smoMeasure.svg.staffWidth);
strs.push(`const ${ssid} = new VF.Stave(${smoMeasure.svg.staffX}, ${staffY}, ${staffWidth});`);
strs.push(`${ssid}.setAttribute('id', '${ssid}');`);
createMeasureModifiers(smoMeasure, strs);
if (smoMeasure.svg.forceClef) {
strs.push(`${ssid}.addClef('${smoMeasure.clef}');`);
}
if (smoMeasure.svg.forceTimeSignature) {
const ts = smoMeasure.timeSignature;
let tsString = ts.timeSignature;
if (smoMeasure.timeSignature.useSymbol && ts.actualBeats === 4 && ts.beatDuration === 4) {
tsString = 'C';
} else if (smoMeasure.timeSignature.useSymbol && ts.actualBeats === 2 && ts.beatDuration === 4) {
tsString = 'C|';
} else if (smoMeasure.timeSignature.displayString.length) {
tsString = smoMeasure.timeSignature.displayString;
}
strs.push(`${ssid}.addTimeSignature('${tsString}');`);
}
if (smoMeasure.svg.forceKeySignature) {
const key = SmoMusic.vexKeySignatureTranspose(smoMeasure.keySignature, 0);
const ksid = 'key' + smoMeasure.id;
strs.push(`const ${ksid} = new VF.KeySignature('${key}');`);
if (smoMeasure.canceledKeySignature) {
const canceledKey = SmoMusic.vexKeySignatureTranspose(smoMeasure.canceledKeySignature, 0);
strs.push(`${ksid}.cancelKey('${canceledKey}');`);
}
strs.push(`${ksid}.addToStave(${ssid});`);
}
strs.push(`${ssid}.setContext(context);`);
strs.push(`${ssid}.draw();`);
smoMeasure.voices.forEach((voice, voiceIx) => {
const vs = getVoiceId(smoMeasure, voiceIx);
strs.push(`${vs}.draw(context, ${ssid});`);
});
smoMeasure.beamGroups.forEach((bg) => {
strs.push(`${bg.attrs.id}.setContext(context);`);
strs.push(`${bg.attrs.id}.draw();`)
});
smoMeasure.tupletTrees.forEach((tp) => {
const traverseTupletTree = ( parentTuplet: SmoTuplet): void => {
strs.push(`${parentTuplet.attrs.id}.setContext(context).draw();`)
for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) {
const tuplet = parentTuplet.childrenTuplets[i];
traverseTupletTree(tuplet);
}
}
traverseTupletTree(tp.tuplet);
});
}
/**
* Simple serialize class that produced VEX note and voice objects
* for vex EasyScore (for easier bug reports and test cases)
* @category SuiRender
*/
export class SmoToVex {
static convert(smoScore: SmoScore, options: any): string {
let div = 'boo';
let page = 0;
options = options ?? {};
if (typeof(options['div']) === 'string') {
div = options.div
}
if (typeof(options['page']) === 'number') {
page = options.page;
}
let startMeasure = -1;
let endMeasure = -1;
const strs: string[] = [];
const pageHeight = smoScore.layoutManager?.getGlobalLayout().pageHeight ?? 1056;
const pageWidth = smoScore.layoutManager?.getGlobalLayout().pageWidth ?? 816;
const pageLength = smoScore.staves[0].measures[smoScore.staves[0].measures.length - 1].svg.pageIndex + 1;
let scoreName = smoScore.scoreInfo.title + ' p ' + (page + 1).toString() + '/' + pageLength.toString();
const scoreSub = smoScore.scoreInfo.subTitle?.length ? `(${smoScore.scoreInfo.subTitle})` : '';
scoreName = `${scoreName} ${scoreSub} by ${smoScore.scoreInfo.composer}`;
strs.push(`// @@ ${scoreName}`);
strs.push('function main() {');
strs.push('// create the div and svg element for the music');
strs.push(`const div = document.getElementById('${div}');`);
strs.push('const VF = Vex.Flow;');
strs.push(`const renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);`);
const zoomScale = (smoScore.layoutManager?.getZoomScale() ?? 1.0);
const svgScale = (smoScore.layoutManager?.getGlobalLayout().svgScale ?? 1.0);
const width = zoomScale * pageWidth;
const height = zoomScale * pageHeight;
const scale = svgScale * zoomScale;
const heightOffset = -1 * (height * page) / scale;
const vbWidth = Math.round(width / scale);
const vbHeight = Math.round(height / scale);
strs.push('const context = renderer.getContext();');
strs.push('const svg = context.svg');
strs.push(`svg.setAttributeNS('', 'width', '${width}');`);
strs.push(`svg.setAttributeNS('', 'height', '${height}');`);
strs.push(`svg.setAttributeNS('', 'viewBox', '0 0 ${vbWidth} ${vbHeight}');`);
strs.push('//');
strs.push('// create the musical objects');
const font = smoScore.fonts.find((x) => x.purpose === SmoScore.fontPurposes.ENGRAVING);
if (font) {
const fs = fontStacks[font.family].join(',');
strs.push(`VF.setMusicFont(${fs});`);
}
const measureCount = smoScore.staves[0].measures.length;
const lyricAdj: string[] = [];
for (var k = 0; k < measureCount; ++k) {
const groupMap: Record<string, VexStaveGroupMusic> = {};
if (smoScore.staves[0].measures[k].svg.pageIndex < page) {
continue;
}
if (smoScore.staves[0].measures[k].svg.pageIndex > page) {
break;
}
startMeasure = startMeasure < 0 ? k : startMeasure;
endMeasure = Math.max(k, endMeasure);
smoScore.staves.forEach((smoStaff, staffIx) => {
const smoMeasure = smoStaff.measures[k];
const selection = SmoSelection.measureSelection(smoScore, smoStaff.staffId, smoMeasure.measureNumber.measureIndex);
if (!selection) {
throw('ouch no selection');
}
const systemGroup = smoScore.getSystemGroupForStaff(selection);
const justifyGroup: string = (systemGroup && smoMeasure.format.autoJustify) ? systemGroup.attrs.id : selection.staff.attrs.id;
const tickmapObject = smoMeasure.createMeasureTickmaps();
const measureIx = smoMeasure.measureNumber.measureIndex;
const voiceStrings: string[] = [];
const fmtid = 'fmt' + smoMeasure.id + measureIx.toString();
strs.push(`const ${fmtid} = new VF.Formatter();`);
if (!groupMap[justifyGroup]) {
groupMap[justifyGroup] = {
formatter: fmtid,
measures: [],
heightOffset,
voiceStrings: [],
systemGroup
}
}
groupMap[justifyGroup].measures.push(smoMeasure);
strs.push('//');
strs.push(`// voices and notes for stave ${smoStaff.staffId} ${smoMeasure.measureNumber.measureIndex}`);
smoMeasure.voices.forEach((smoVoice: SmoVoice, voiceIx: number) => {
const vn = getVoiceId(smoMeasure, voiceIx);
groupMap[justifyGroup].voiceStrings.push(vn);
const vc = vn + 'ar';
const ts = JSON.stringify({
numBeats: smoMeasure.timeSignature.actualBeats,
beatValue: smoMeasure.timeSignature.beatDuration
});
strs.push(`const ${vn} = new VF.Voice(JSON.parse('${ts}')).setMode(VF.Voice.Mode.SOFT);`);
strs.push(`const ${vc} = [];`);
smoVoice.notes.forEach((smoNote: SmoNote, noteIx: number) => {
const renderInfo: VexNoteRenderInfo = { smoNote, voiceIx, noteIx, tickmapObject, lyricAdj };
const noteId = createStaveNote(renderInfo, smoMeasure.keySignature, smoMeasure.svg.rowInSystem, strs);
strs.push(`${vc}.push(${noteId});`);
});
strs.push(`${vn}.addTickables(${vc})`);
voiceStrings.push(vn);
strs.push(`${fmtid}.joinVoices([${vn}]);`);
});
if (smoMeasure.svg.rowInSystem === smoScore.staves.length - 1) {
createColumn(groupMap, strs);
const mapKeys = Object.keys(groupMap);
mapKeys.forEach((mapKey) => {
const tmpGroup = groupMap[mapKey];
if (tmpGroup.systemGroup) {
const systemIndex = smoMeasure.measureNumber.systemIndex;
const startMeasure = 'stave' + smoScore.staves[tmpGroup.systemGroup.startSelector.staff].measures[k].id;
const endMeasure = 'stave' + smoScore.staves[tmpGroup.systemGroup.endSelector.staff].measures[k].id;
const leftConnector = leftConnectorVx(tmpGroup.systemGroup);
const rightConnector = rightConnectorVx(tmpGroup.systemGroup);
const jgname = justifyGroup + startMeasure + staffIx.toString();
if (systemIndex === 0 && smoScore.staves.length > 1) {
strs.push(`const left${jgname} = new VF.StaveConnector(${startMeasure}, ${endMeasure}).setType(${leftConnector});`);
strs.push(`left${jgname}.setContext(context).draw();`);
}
let endStave = false;
if (smoMeasure.measureNumber.systemIndex !== 0) {
if (smoMeasure.measureNumber.systemIndex === smoScore.staves[0].measures.length - 1) {
endStave = true;
} else if (smoScore.staves[0].measures.length > k + 1 &&
smoScore.staves[0].measures[k + 1].measureNumber.systemIndex === 0) {
endStave = true;
}
}
if (endStave) {
strs.push(`const right${jgname} = new VF.StaveConnector(${startMeasure}, ${endMeasure}).setType(${rightConnector});`);
strs.push(`right${jgname}.setContext(context).draw();`);
}
}
});
}
});
}
smoScore.staves.forEach((staff) => {
renderModifiers(smoScore, staff, startMeasure, endMeasure, strs);
});
renderVoltas(smoScore, startMeasure, endMeasure, strs);
if (lyricAdj.length) {
strs.push('// ');
strs.push('// Align lyrics on different measures, once they are rendered.');
}
const render = strs.concat(lyricAdj);
render.push('}');
return render.join(`\n`);
// console.log(render.join(`\n`));
}
}