mscx2ly
Version:
Tool to render lilypond code from a MuseScore save file
1,379 lines (1,305 loc) • 52.6 kB
JavaScript
import { XmlWrapper } from "./xml_wrapper.js";
/**
* @param {MetaTag} metaTag
* @returns {FlatMetaTag}
*/
export const parseMetaTag = (metaTag) => {
if (!Array.isArray(metaTag)) {
metaTag = [metaTag];
}
const ret = {};
metaTag.forEach((tag) => {
ret[tag.get('name')] = tag.text;
});
return ret;
}
const posKeySigs = ['C', 'G', 'D', 'A', 'E', 'B', 'Fis', 'Cis'];
const negKeySigs = ['C', 'F', 'Bes', 'Es', 'As', 'Des', 'Ges', 'Ces'];
/**
*
* @param {XmlWrapper} keySig
* @returns {string} Lilypond note name for key signature in major
*/
export const parseKeySig = (keySig) => {
const concertKey = parseInt(keySig.get('concertKey'), 10);
if (concertKey > 0) {
return posKeySigs[concertKey];
}
return negKeySigs[-concertKey];
}
/**
*
* @param {XmlWrapper} keySig
* @returns {string} Lilypond code for key signature
*/
export const renderKeySig = (keySig) => {
return `\\key ${parseKeySig(keySig).toLocaleLowerCase()} \\major`;
}
/**
*
* @param {XmlWrapper} timeSig
*/
const parseTimeSig = (timeSig) => {
const n = timeSig.get('sigN');
const d = timeSig.get('sigD');
if (!n || !d) return "";
return `\\time ${n}/${d}`;
}
const durationMap = {
"1": "1",
"1/1": "1",
"1/2": "2",
"2/2": "1",
"1/4": "4",
"2/4": "2",
"3/4": "2.",
"4/4": "1",
"1/8": "8",
"2/8": "4",
"3/8": "4.",
"4/8": "2",
"6/8": "2.",
"8/8": "1",
"1/16": "16",
"2/16": "8",
"3/16": "8.",
"4/16": "4",
"6/16": "4.",
"8/16": "2",
"12/16": "2.",
"16/16": "1",
"1/32": "32",
"2/32": "16",
"3/32": "16.",
"4/32": "8",
"6/32": "8.",
"8/32": "4",
"12/32": "4.",
"16/32": "2",
"24/32": "2.",
"32/32": "1",
"half": "2",
"whole": "1",
"quarter": "4",
"eighth": "8",
"16th": "16",
"32nd": "32",
}
export const createValidPartName = (partName) => {
// we need to make the partname a valid lilypond identifier
// so we replace spaces with nothing, and make it lowercase
// we also need to replace all numbers with its word equivalent
let validPartName = partName.toLowerCase().replace(/[_-\s]/g, '');
validPartName = validPartName.replace(/0/g, 'Zero');
validPartName = validPartName.replace(/1/g, 'One');
validPartName = validPartName.replace(/2/g, 'Two');
validPartName = validPartName.replace(/3/g, 'Three');
validPartName = validPartName.replace(/4/g, 'Four');
validPartName = validPartName.replace(/5/g, 'Five');
validPartName = validPartName.replace(/6/g, 'Six');
validPartName = validPartName.replace(/7/g, 'Seven');
validPartName = validPartName.replace(/8/g, 'Eight');
validPartName = validPartName.replace(/9/g, 'Nine');
return validPartName;
}
export const getDurationFor = (evt) => {
const duration = evt.get('duration') ? evt.get('duration') : evt.get('durationType');
return durationMap[duration];
}
/**
*
* @param {Rest} rest
* @param {TimeSig} timeSig
* @returns {string} Lilypond code for rest
*/
export const parseRest = (rest, timeSig) => {
// we need to know the duration of the rest in comparison to the time signature
// rest has a durationType and a duration
// if the durationType === "measure" then we have a whole rest, so we
// can use `R`.
// now we need to convert the ratio to a lilypond duration
const restDuration = rest.get('duration') ? rest.get('duration') : rest.get('durationType');
const duration = durationMap[restDuration];
if (!duration) {
throw new Error(`Unknown duration: ${restDuration}`);
}
if (rest.get('durationType') === "measure") {
return `R${duration}`;
}
else {
return `r${duration}`;
}
}
const metronomeNoteSymbolMap = {
"metNoteQuarterUp": 4,
"metNoteQuarterDown": 4,
"metNoteHalfUp": 2,
"metNoteHalfDown": 2,
"metNoteWholeUp": 1,
"metNoteWholeDown": 1,
"metNoteEighthUp": 8,
"metNoteEighthDown": 8,
"metNote16thUp": 16,
"metNote16thDown": 16,
}
const articulationMap = {
"articStaccatoBelow": "-.",
"articAccentBelow": "->",
"articStaccatissimoBelow": "-!",
"articTenutoBelow": "--",
"articTenutoStaccatoBelow": "---.",
"articMarcatoAbove": "---^",
"articAccentStaccatoBelow": "->-.",
"articMarcatoStaccatoAbove": "-^-.",
"articMarcatoTenutoAbove": "-^--",
"articStacatissimoStrokeBelow": "-!", // this one doesn't exist in Lilypond, vertical line under a note
"articStacatissimoWedgeBelow": "-!", // this one doesn't exist in Lilypond, wedge under a note
"articStressBelow": "->", // this one doesn't exist in Lilypond, accent sign under a note
"articTenutoAccentBelow": "---^",
// "articUnstressBelow": "<", // this one doesn't exist in Lilypond, it looks like a text tie
"brassMuteOpen": "\\open",
"brassMuteClosed": "-+",
"stringsHarmonic": "\\flageolet",
"stringsUpBow": "\\upbow",
"stringsDownBow": "\\downbow",
"articLaisserVibrer": "\\laisserVibrer",
"articSoftAccentBelow": "\\espressivo", // espressivo
"articSoftAccentStaccatoBelow": "-.\\espressivo", // espressivo staccato
"articSoftAccentTenutoBelow": "--\\espressivo", // espressivo tenuto
"articSoftAccentStaccatoTenutoBelow": "---.\\espressivo", // espressivo staccato tenuto
// "guitarFadeIn": "\\fadein", // this one doesn't exist in Lilypond
// "guitarFadeOut": "\\fadeout", // this one doesn't exist in Lilypond
// "guitarVolumeSwell": "\\swell", // this one doesn't exist in Lilypond
// "wiggleSawtooth": "\\sawtooth", // this one doesn't exist in Lilypond
// "wiggleSawtoothWide": "\\sawtoothWide", // this one doesn't exist in Lilypond
// "wiggleVibratoLargeFaster": "\\vibratoLargeFaster", // this one doesn't exist in Lilypond
// "wiggleVibratoLargeSlowest": "\\vibratoLargeSlowest", // this one doesn't exist in Lilypond
"pluckedSnapPizzicatoAbove": "\\snappizzicato"
}
export const renderArticulation = (articulation) => {
const type = articulation.get('subtype');
const ret = articulationMap[type];
if (!ret) {
return ""; // nothing to do
}
return ret;
}
// midi pitch to note name
// midi pitch is a number between 0 and 127
// note name is a string with a note name and an octave indicated by ' or , or nothing
// midi pitch 60 is c' (middle c)
// midi pitch 61 can be cis or des, depending on the key signature
const octaveIndicator = {
0: "'",
1: "''",
2: "'''",
3: "''''",
4: "'''''",
"-1": '',
"-2": ',',
"-3": ',,',
"-4": ',,,',
"-5": ',,,,'
};
const noteNamesForKeySig = {
"-1": ['c', 'cis', 'd', 'es', 'e', 'f', 'fis', 'g', 'as', 'a', 'bes', 'b'],
plain: ['c', 'cis', 'd', 'es', 'e', 'f', 'fis', 'g', 'gis', 'a', 'bes', 'b'],
sharp: ['c', 'cis', 'd', 'dis', 'e', 'f', 'fis', 'g', 'gis', 'a', 'ais', 'b'],
flat: ['c', 'des', 'd', 'es', 'e', 'f', 'ges', 'g', 'as', 'a', 'bes', 'b']
}
/**
* This is a very primitive conversion from midi pitch to note name
* It tries to take the key signature into account to get a bit of a
* hint what the best name would be. Far from perfect.
* There is a technique to do this properly, but that requires actual musical analysis
* of the material, which is beyond the scope of this project.
* @param {number} midiPitch
* @param {KeySig} keySig
* @returns {string} note name
*/
const convertMidiPitchToNoteName = (midiPitch, keySig) => {
// list of note names indexed
const concertKey = keySig? keySig.get('concertKey'): 'plain';
let keySigType = null;
if (noteNamesForKeySig[concertKey]) {
keySigType = concertKey;
}
else {
keySigType = concertKey === 0 ? 'plain' : concertKey > 0 ? 'sharp' : 'flat';
}
const noteNames = noteNamesForKeySig[keySigType];
const baseNoteIdx = midiPitch % 12;
const baseOctave = Math.floor(midiPitch / 12) - 5;
return noteNames[baseNoteIdx] + octaveIndicator[baseOctave];
}
/**
*
* @param {Note} note
* @param {KeySig} keySig
* @returns {string} Lilypond note name with possible accidental
*/
const parseNote = (note, keySig) => {
// we need to convert the midi pitch (at pitch) to a note name
// this is not straightforward, because it depends on the key signature (a bit)
// we do have a possible accidental
const base = convertMidiPitchToNoteName(note.get('pitch'), keySig);
if (note.get('Accidental')) {
return base + "!";
}
return base;
};
/**
*
* @param {Chord} chord
* @param {TimeSig} timeSig
*/
export const parseChord = (chord, timeSig, keySig, lyric) => {
const dur = durationMap[chord.get('durationType')];
const dots = chord.get('dots')? parseInt(chord.get('dots'), 10) : 0;
const duration = dur + '.'.repeat(dots);
let noteInfo = chord.get('Note');
if (!Array.isArray(noteInfo)) {
noteInfo = [noteInfo];
}
let hasTie = false;
const notes = noteInfo.map((note) => {
let spanner = note.get('Spanner');
if (spanner) {
if (!Array.isArray(spanner)) spanner = [spanner];
hasTie = spanner.some((span) => {
return span.get('type') === 'Tie' && span.get('next');
});
}
return parseNote(note, keySig);
});
let ret;
if (notes.length > 1) {
ret = `<${notes.join(' ')}>${duration}`;
}
else ret = notes[0] + duration;
// this assumes that the value in subtype is of the format `r[sub_length]`
// so r8 for a subdivision in 8ths
const tremolo = chord.get('TremoloSingleChord');
if (tremolo) {
ret += `:${tremolo.get('subtype').substr(1)}`; // cut off the r at the beginning
}
let articulation = chord.get('Articulation');
if (articulation) {
if (!Array.isArray(articulation)) {
articulation = [articulation];
}
ret += articulation.map(renderArticulation).join(' ');
}
let spanner = chord.get('Spanner');
if (spanner) {
if (!Array.isArray(spanner)) {
spanner = [spanner];
}
spanner.forEach((span) => {
if (span.get('type') === 'Slur') {
if (span.get('next')) {
ret += '(';
}
if (span.get('prev')) {
ret += ')';
}
}
});
}
if (hasTie) {
if (lyric) {
if (lyric.isDone === undefined) {
lyric.isDone = false;
}
else {
lyric.end += 4/parseFloat(dur);
}
}
ret += ' ~';
}
else {
// add the duration to the lyric
if (lyric) {
if (lyric.isDone === undefined) {
lyric.isDone = true;
}
else { // continuing from previous note
lyric.end += 4/parseFloat(dur);
lyric.isDone = true;
}
}
}
return ret;
}
/**
*
* @param {string} clef Clef as string from the MuseScore XML file
* @returns {string} Lilypond code for clef
*/
export function renderClef(clef) {
if (Array.isArray(clef)) {
clef = clef[0];
}
switch (clef) {
case 'G': return '\\clef treble';
case 'F': return '\\clef bass';
case 'C': return '\\clef alto';
case 'G8vb': return '\\clef "treble_8"';
case 'F8vb': return '\\clef "bass_8"';
case 'G8va': return '\\clef "treble^8"';
case 'G15ma': return '\\clef "treble^15"';
case 'PERC': return '\\clef percussion';
default: return '\\clef treble';
}
}
export function renderBarLine(barLine) {
const type = barLine.get('subtype');
let str = '';
switch (type) {
case 'final': str = '|.'; break;
case 'end': str = '|.'; break;
case 'double': str = '||'; break;
case 'dashed': str = '!'; break;
case 'dotted': str = ';' ; break;
case 'reverse-end': str = '.|'; break;
case 'heavy': str = 'x-.'; break;
case 'double-heavy': str = '..'; break;
case null: {
// these are gregorian barlines
const fromOffset = parseInt(barLine.get('spanFromOffset'), 10);
const toOffset = parseInt(barLine.get('spanToOffset'), 10);
if (fromOffset === -1 && toOffset === -7) {
str = "'";
}
else if (fromOffset === -2 && toOffset === -6) {
// this is a longer version of the one above, Lilypond doesn't seem to have this by default
// as a \bar type
str = "'"; // return the same as the small
}
else if (fromOffset === -2 && toOffset === -2) {
str = ",";
}
else if (fromOffset === -1 && toOffset === -1) {
// this is a longer version of the one above, Lilypond doesn't seem to have this by default
str = ","; // return the same as the small
}
break;
}
}
if (str) {
return `\\bar "${str}"`;
}
else return ""; // no barline if we don't know what it is
}
/**
*
* @param {Part} part
* @param {FlatScoreStaff} staffInfo
* @returns
*/
export const readPartInfo = (part, staffInfo) => {
// this returns an object with the information of the part
let instruments = part.get('Instrument');
if (!Array.isArray(instruments)) {
instruments = [instruments];
}
const ret = {};
ret.id = part.get('id');
ret.trackName = part.get('trackName');
ret.instrument = instruments.map((instrument) => {
let instrChannel = instrument.get('Channel');
if (!Array.isArray(instrChannel)) {
instrChannel = [instrChannel];
}
return {
id: instrument.get('id'),
longName: instrument.get('longName'),
shortName: instrument.get('shortName'),
trackName: instrument.get('trackName'),
minPitchP: instrument.get('minPitchP'),
maxPitchP: instrument.get('maxPitchP'),
minPitchA: instrument.get('minPitchA'),
maxPitchA: instrument.get('maxPitchA'),
transposeDiatonic: instrument.get('transposeDiatonic'),
transposeChromatic: instrument.get('transposeChromatic'),
instrumentId: instrument.get('instrumentId'),
clef: instrument.get('clef'),
Channel: instrChannel.map((channel) => {
return {
synti: channel.get('synti'),
midiPort: channel.get('midiPort'),
midiChannel: channel.get('midiChannel'),
program: channel.get('program')
}
})
}
});
let staffs = part.get('Staff');
if (!Array.isArray(staffs)) {
staffs = [staffs];
}
ret.staffs = staffs.map((staff) => {
const staffContents = staffInfo.find((staffdata) => {
return staffdata.id === staff.get('id');
});
let defaultConcertClef = staff.get('defaultConcertClef')? staff.get('defaultConcertClef') : null;
let defaultClef = staff.get('defaultClef')? staff.get('defaultClef') : defaultConcertClef;
return {
id: staff.get('id'),
StaffType: staff.get('StaffType'),
isStaffVisible: staff.get('isStaffVisible'),
barLineSpan: staff.get('barLineSpan'),
defaultClef,
contents: staffContents.Measure,
}
});
return ret;
}
/**
*
* @param {Part[]} parts
* @param {OrderInfo} orderInfo
* @param {FlatScoreStaff} staffInfo
* @returns {}
*/
export const readPartsInfo = (parts, orderInfo, staffInfo) => {
// this returns an object with the part name as key
// and any staffs used in the part, plus other information
// sadly the instruments in the orderInfo is not ordered as in the score
// also, the order is inconclusive based on the info available in the orderInfo
// there is no direct evidence why the string instruments are below the keyboards
if (!Array.isArray(parts)) {
parts = [parts];
}
// so we better go by parts, find sections they could belong to, and wrap them that way.
const partsInfo = parts.map((part) => {
return readPartInfo(part, staffInfo);
});
// per part we need to find:
// - sections
// - multiple staffs
let ret = [];
let currentSection = {
isSection: true,
id: null,
parts: []
};
partsInfo.forEach((partInfo) => {
// we trust the order of the instruments in the partInfo
// take the instrumentId
// console.log('part:', partInfo.trackName);
const instrumentId = partInfo.instrument[0].instrumentId;
// console.log('instrumentId:', instrumentId);
const section = orderInfo.sections.find((section) => {
const sharesSectionFamilies = [section.family].flat().some((family) => {
return instrumentId.includes(family);
});
const sharesSectionId = instrumentId.includes(section.id);
return sharesSectionFamilies || sharesSectionId;
});
if (section) {
// console.log('found section for part:', partInfo.trackName, section.id);
if (currentSection.id === section.id) {
// we are still in the same section
currentSection.parts.push(partInfo);
}
else {
// we are in a new section
if (currentSection.id && currentSection.id !== section.id) {
// we need to close the current section
// console.log('closing section:', currentSection.id);
ret.push(currentSection);
}
// we start a new section
currentSection = {
id: section.id,
isSection: true,
parts: [partInfo]
}
}
}
else {
// no problem, it is then not part of a section
// we add it straight
// console.log('no section for part:', partInfo.trackName);
ret.push(partInfo);
}
});
// we need to close the last section
if (currentSection.id) {
// console.log('closing section:', currentSection.id);
ret.push(currentSection);
}
// console.log('ret:', ret);
return ret;
}
/**
*
* @param {Order} order
* @returns {OrderInfo}
*/
export const readOrderInfo = (order) => {
if (!order) {
return {
instruments: [],
sections: []
}
}
// first: instruments
let instruments = order.get('instrument');
if (!Array.isArray(instruments)) {
instruments = [instruments];
}
instruments.map((instrument) => {
let fam = instrument.get('family', true);
if (!Array.isArray(fam)) {
fam = [fam];
}
return {
id: instrument.get('id'),
family: fam.map((family) => {
return {
id: family.get('id'),
name: family.text
}
})
}
});
// second: sections
let sections = order.get('section');
if (!Array.isArray(sections)) {
sections = [sections];
};
sections.map((section) => {
let unsorted = section.get('unsorted');
if (unsorted && !Array.isArray(unsorted)) {
unsorted = [unsorted];
}
else {
unsorted = [];
}
let family = section.get('family');
if (!Array.isArray(family)) {
family = [family];
}
return {
id: section.get('id'),
brackets: section.get('brackets') === 'true',
barLineSpan: section.get('barLineSpan') === 'true',
thinBrackets: section.get('thinBrackets') === 'true',
family,
unsorted: unsorted.map((unsorted_obj) => {
return {
group: unsorted_obj.get('group')
}
})
}
});
// now we return an order of instruments and sections
// so we can attach parts to the sections
return {
instruments,
sections
}
}
/**
*
* @param {XmlWrapper[]} staffs
* @returns {FlatScoreStaff} reduced staff info
*/
export const readStaffInfo = (staffs) => {
if (!Array.isArray(staffs)) {
staffs = [staffs];
}
return staffs.map((staff) => {
const ret = {
id: staff.get('id'),
Measure: staff.get('Measure').map((measure) => {
let voices = measure.get('voice');
if (!Array.isArray(voices)) {
voices = [voices];
}
return {
irregular: measure.get('irregular'),
len: measure.get('len'),
voice: voices.map((voice) => {
// the purpose here is a filter to only get the relevant information
// now order becomes important, so we use the children instead.
return voice.children.filter((child) => {
return [
'KeySig',
'TimeSig',
'Tempo',
'Rest',
'Dynamic',
'Spanner',
'Chord',
'BarLine',
'VBox',
'Clef',
'Fermata',
'PlayTechAnnotation' // this is for pizz and arco
].includes(child.name);
});
})
}
})
}
return ret;
});
};
const fermatas = {
fermataAbove: "\\fermata",
fermataBelow: "_\\fermata",
fermataShortAbove: "\\shortfermata",
fermataShortBelow: "_\\shortfermata",
fermataLongAbove: "\\longfermata",
fermataLongBelow: "_\\longfermata",
fermataLongHenzeAbove: "\\henzelongfermata",
fermataLongHenzeBelow: "_\\henzelongfermata",
fermataShortHenzeAbove: "\\henzeshortfermata",
fermataShortHenzeBelow: "_\\henzeshortfermata",
fermataVeryLongAbove: "\\verylongfermata",
fermataVeryLongBelow: "_\\verylongfermata",
fermataVeryShortAbove: "\\veryshortfermata",
fermataVeryShortBelow: "_\\veryshortfermata",
}
export const renderFermata = (fermata) => {
const type = fermata.get('subtype');
return fermatas[type] || "";
}
export const renderHairPin = (spanner) => {
const isEnd = !!spanner.get('prev');
if (isEnd) {
return "\\!";
}
const hairpin = spanner.get('HairPin'); // this will be null when isEnd
const isStart = !!spanner.get('next');
const subType = hairpin.get('subtype'); // 0 for cresc, 1 for decresc
const type = subType === '0'? '\\<' : '\\>';
return type;
}
export const renderSpanner = (spanner) => {
const type = spanner.get('type'); // xml attribute
const typeObject = spanner.get(type); // xml object named after the xml attribute type
const isStart = !!spanner.get('next');
const isEnd = !!spanner.get('prev');
if (!typeObject && !isEnd) {
// this means that the spanner does not exist as a subchild
console.log('spanner type not found:', type);
return "";
}
if (type === 'HairPin') {
return renderHairPin(spanner);
}
else {
console.log('spanner type not implemented:', type);
}
return "";
}
// this is a way to keep track of spanners and other things that needs to be retrieved in t
class OnceConsumer {
constructor (type = 'fifo') {
this.data = {};
this.type = type; // fifo or lifo
}
set (key, value) {
if (!this.data[key]) {
this.data[key] = [value];
}
else {
this.data[key].push(value);
}
}
has (key) {
return this.data[key] && this.data[key].length > 0;
}
get (key) {
if (this.data[key] && this.data[key].length > 0) {
if (this.type === 'lifo') {
return this.data[key].pop();
}
else {
return this.data[key].shift();
}
}
}
}
export const renderDynamic = (dynamic) => {
const type = dynamic.get('subtype');
let ret = '';
switch (type) {
case 'ppp': ret = '\\ppp'; break;
case 'pp': ret = '\\pp'; break;
case 'p': ret = '\\p'; break;
case 'mp': ret = '\\mp'; break;
case 'mf': ret = '\\mf'; break;
case 'f': ret = '\\f'; break;
case 'ff': ret = '\\ff'; break;
case 'fff': ret = '\\fff'; break;
case 'fp': ret = '\\fp'; break;
case 'pf': ret = '\\pf'; break;
case 'sf': ret = '\\sf'; break;
case 'sfz': ret = '\\sfz'; break;
case 'sff': ret = '\\sff'; break;
case 'sffz': ret = '\\sffz';
case 'sfp': ret = '\\sfp'; break;
case 'rfz': ret = '\\rfz'; break;
case 'rf': ret = '\\rf'; break;
case 'fz': ret = '\\fz'; break;
}
return ret;
}
export const parseTempo = (tempoEvt) => {
// this requires a bit of trickery
/*
<Tempo>
<tempo>1</tempo>
<followText>1</followText>
<eid>nZKNqFjlayM_cWyfGetlnSK</eid>
<text><sym>metNoteQuarterUp</sym><font face="Edwin"></font> = 60</text>
</Tempo>
<Tempo>
<tempo>1</tempo>
<followText>1</followText>
<eid>+13ij4Pf60O_vTAaptrxsfI</eid>
<minDistance>-999</minDistance>
<family>FreeSerif</family>
<bold>0</bold>
<offset x="-1.35348" y="-4.29607"/>
<text><b></b><sym>metNoteQuarterUp</sym><b><font face="FreeSerif"/> = 60
</b></text>
</Tempo>
*/
const tempo = tempoEvt.get('tempo');
const followText = tempoEvt.get('followText');
const text = tempoEvt.get('text'); // this is '= 60', or free text, such as with type aTempo, which we need to copy.
// text might also be an XmlWrapper, which means we need to do some digging to find what we need.
let tempoValue, tempoText;
if (text instanceof XmlWrapper) {
const data = text.data;
// Deep search through the XML data structure to find tempo value
const searchForTempoValue = (obj) => {
if (typeof obj === 'string') {
const match = obj.match(/=[\s\S]?([0-9]+)/);
if (match) {
return match[1];
}
}
if (obj && typeof obj === 'object') {
for (const key in obj) {
const result = searchForTempoValue(obj[key]);
if (result) return result;
}
}
return null;
};
tempoValue = searchForTempoValue(data);
if (tempoValue) {
tempoValue = parseInt(tempoValue, 10);
}
}
else {
tempoText = text;
}
const type = tempoEvt.get('type'); // this can be aTempo, or nothing
// const tempoValue = type? "": parseInt(text.split('=')[1].trim(), 10);
const tempoSymbol = type? "" : tempoEvt.get('text', true).get('sym');
const tempoSymbolValue = type? "": metronomeNoteSymbolMap[tempoSymbol];
if (!tempoSymbolValue && !type) {
console.warn('Unknown tempo symbol:', tempoSymbol);
}
return {
tempo,
followText,
tempoText,
tempoValue,
tempoSymbol,
tempoSymbolValue
};
}
export const renderMusicForStaff = (staff) => {
const staffContents = staff.contents;
let currentKeySig = null;
let currentTimeSig = null;
const isPercussion = staff.defaultClef === 'PERC';
const lyrics = [];
let currentTime = 0;
let currentTempo;
let lastLyric;
const ret = staffContents.map((measure) => {
// TODO: we need to do something about the transposition
let measureText = "";
if (measure.len) {
// we assume a \partial for now
measureText += `\\partial ${durationMap[measure.len]}`;
}
const voices = measure.voice;
const parsedVoices = voices.map((voice, voiceIdx) => {
// we need to keep the fermata, as in Lilypond it should always follow the event it is attached to
const voiceConsumer = new OnceConsumer('lifo');
return voice.map((evt) => {
switch (evt.name) {
case 'Tempo': {
currentTempo = parseTempo(evt);
break;
}
case 'KeySig': {
if (!isPercussion) {
currentKeySig = evt;
return renderKeySig(evt);
}
}
case 'TimeSig': {
currentTimeSig = evt;
return parseTimeSig(evt);
}
case 'Rest': {
// we want to do time tracking, so we need to know the duration of the rest
// this needs to be done different. I convert it to whole bars now, but that might not be the best.
// Preferrably I would convert the duration to a tick value, then from there I the lyrics offset is easy to convert to ms.
const duration = getDurationFor(evt);
currentTime += 4/parseInt(duration, 10);
let renderedRest = parseRest(evt, currentTimeSig);
if (voiceConsumer.has('dynamic')) {
renderedRest = `${renderedRest}${voiceConsumer.get('dynamic')}`;
}
if (voiceConsumer.has('spanner')) {
renderedRest = `${renderedRest}${voiceConsumer.get('spanner')}`;
}
if (voiceConsumer.has('text')) {
renderedRest = `${renderedRest}${voiceConsumer.get('text')}`;
}
if (voiceConsumer.has('fermata')) {
renderedRest = `${renderedRest}${voiceConsumer.get('fermata')}`;
}
return renderedRest;
}
case 'Chord': {
const duration = 4/parseInt(getDurationFor(evt), 10);
if (evt.get('Lyrics')) {
// we have to be aware that the current note might be tied to the next note
// and so the duration of the lyric is not the same as the current duration of this
// note event.
const lyricData = evt.get('Lyrics');
const lyric = {
voiceIdx: voiceIdx,
start: currentTime,
end: currentTime + duration,
text: lyricData.get('text'),
syllabic: lyricData.get('syllabic'),
currentTempo,
};
lyrics.push(lyric);
lastLyric = lyric;
}
currentTime += duration;
let renderedChord = parseChord(evt, currentTimeSig, currentKeySig, lastLyric);
if (voiceConsumer.has('dynamic')) {
renderedChord = `${renderedChord}${voiceConsumer.get('dynamic')}`;
}
if (voiceConsumer.has('spanner')) {
renderedChord = `${renderedChord}${voiceConsumer.get('spanner')}`;
}
if (voiceConsumer.has('text')) {
renderedChord = `${renderedChord}${voiceConsumer.get('text')}`;
}
if (voiceConsumer.has('fermata')) {
renderedChord = `${renderedChord}${voiceConsumer.get('fermata')}`;
}
return renderedChord;
}
case 'Clef': {
const clef = evt.get('concertClefType') || evt.get('transposingClefType');
return renderClef(clef);
}
case 'BarLine': {
return renderBarLine(evt);
}
case 'Fermata': {
voiceConsumer.set('fermata', renderFermata(evt));
break;
}
case 'PlayTechAnnotation': {
const text = `^\\markup { \\italic ${evt.get('text')} }`;
voiceConsumer.set('text', text);
break;
}
case 'Spanner': {
const spanner = renderSpanner(evt);
if (spanner) voiceConsumer.set('spanner', spanner);
break;
}
case 'Dynamic': {
const dynamic = renderDynamic(evt);
if (dynamic) voiceConsumer.set('dynamic', dynamic);
break;
}
default: return '';
}
}).join(' ');
});
if (parsedVoices.length > 1) {
return `${measureText} << { ${parsedVoices.join(' } \\\\ { ')} } >>`;
}
return `${measureText} ${parsedVoices[0]}`;
});
return { lyrics, music: ret };
}
const renderStaff = (staff) => {
const { lyrics, music } = renderMusicForStaff(staff);
const ret = {
id: staff.id,
defaultClef: staff.defaultClef,
isStaffVisible: staff.isStaffVisible? staff.isStaffVisible[0] : null,
measures: music,
lyrics,
};
return ret;
}
const diatonics = ['c', 'd', 'e', 'f', 'g', 'a', 'b', 'c'];
const chromatics = [2, 2, 1, 2, 2, 2, 1];
const calculateTransposition = (part) => {
const instr = part.instrument[0];
let diatonic = instr.transposeDiatonic;
let chromatic = Math.abs(instr.transposeChromatic);
let octaves = 0;
if (!diatonic && !chromatic) {
return null; // nothing to do
}
// if the diatonic is a multiple of 7 and the chromatic is a multiple of 12, we don't need to transpose
if (Math.abs(diatonic) % 7 === 0 && Math.abs(chromatic) % 12 === 0) {
return null;
}
// support for transpositions bigger than an octave:
// first the diatonic and chromatic needs be brought within the octave
// but we store the amount of octaves we took out
// we add that to the endnote
if (Math.abs(diatonic) > 7) {
octaves = Math.floor(diatonic / 7); // we want to keep the sign
diatonic = diatonic % 7;
chromatic = chromatic % 12; // bring it back to the octave
}
// we need to express the transposition through \\transpose and wrap the music in a { } block
// what we do need to do here though is the c to [x] mapping, the transpose block can be done in renderPart
// in case of diatonic -1 and chromatic -2, it means c to d (as we inverse)
// so: diatonic means from c do x steps up => Math.abs(diatonic);
// chromatic indicates what kind of alteration we need to apply to the diatonic
// in case of -1, -3 it means c to dis
// in case of -2, -3 it means c to es
const isUp = diatonic > 0;
const stepNames = isUp? diatonics.reverse() : diatonics;
const chromaticValues = isUp > 0? chromatics.reverse() : chromatics;
// now the process is identical, walk the diatonic steps, calculate the chromatic value
let totalChromatic = 0;
let endName = stepNames[Math.abs(diatonic)];
for (let i = 0; i < Math.abs(diatonic); i++) {
totalChromatic += chromaticValues[i];
}
if (isUp) {
if (chromatic > totalChromatic) {
// c is start, need the second name + extension
if (endName === 'a' || endName === 'e') {
endName += 's';
}
else {
endName += 'es';
}
}
else if (chromatic < totalChromatic) {
endName += 'is';
}
}
else {
if (chromatic > totalChromatic) {
endName += 'is';
}
else if (chromatic < totalChromatic) {
// c is start, need the second name + extension
if (endName === 'a' || endName === 'e') {
endName += 's';
}
else {
endName += 'es';
}
}
}
if (octaves) {
if (octaves > 0) {
endName += `,`.repeat(octaves);
}
if (octaves < 0) {
endName += `'`.repeat(octaves);
}
}
return `c ${endName}`;
}
const renderLyrics = (lyrics) => {
const ret = [];
for (const lyric of lyrics) {
let lyricText = lyric.text;
if (lyricText.includes('"')) {
lyricText = lyricText.replace(/"/g, '\\"');
lyricText = `"${lyricText}"`;
}
ret.push(lyricText);
if (lyric.syllabic && lyric.syllabic !== 'end') {
ret.push('--');
}
}
return ret.join(" ");
}
const renderPart = (part) => {
const partName = part.trackName;
const longName = part.instrument[0].longName;
const shortName = part.instrument[0].shortName;
const transposition = calculateTransposition(part);
const ret = {
musicData: {},
scoreData: [], // in the score, the order does matter
partData: {},
lyricData: [],
};
if (part.staffs.length > 1) {
// we need to render multiple staffs
// we need to make a macro for the contents of each staff
const renderedStaffs = part.staffs.map(renderStaff);
// lets assume a piano for now
// this means one staff is treble and one is bass
// we need to abstract the data for the staffs first
// because we need the names of the macros
let tmpScoreData = "\\new PianoStaff <<\n";
tmpScoreData += ` \\set PianoStaff.instrumentName = "${longName}"\n`;
tmpScoreData += ` \\set PianoStaff.shortInstrumentName = "${shortName}"\n`;
renderedStaffs.forEach((staff, idx) => {
const partDataName = createValidPartName(`${partName}${idx+1}`);
ret.musicData[`${partDataName}`] = staff.measures;
const clefname = renderClef(staff.defaultClef);
tmpScoreData += "\\new Staff {\n";
tmpScoreData += ` ${clefname} \n`;
if (transposition) tmpScoreData += ` \\transpose ${transposition} { \n`;
tmpScoreData += ` \\${partDataName}\n`;
if (transposition) tmpScoreData += ` }\n`;
tmpScoreData += `}\n`;
ret.lyricData.push(staff.lyrics);
});
tmpScoreData += ">>\n";
ret.scoreData.push(tmpScoreData);
ret.partData[partName] = tmpScoreData;
}
else {
const renderedStaff = renderStaff(part.staffs[0]);
const staffHasLyrics = renderedStaff.lyrics && renderedStaff.lyrics.length;
const partDataName = createValidPartName(partName);
ret.musicData[partDataName] = renderedStaff.measures;
const clefname = renderClef(part.staffs[0].defaultClef);
// render the score contents for the Score
let tmpScoreData = " \\new Staff {\n";
tmpScoreData += ` \\set Staff.instrumentName = "${longName}"\n`;
tmpScoreData += ` \\set Staff.shortInstrumentName = "${shortName}"\n`;
tmpScoreData += ` ${clefname} \n`;
if (staffHasLyrics) {
tmpScoreData += ` \\new Voice = "${partDataName}" {\n`;
}
if (transposition) tmpScoreData += ` \\transpose ${transposition} { \n`;
tmpScoreData += ` \\${partDataName}\n`;
if (transposition) tmpScoreData += ` }\n`;
if (staffHasLyrics) {
tmpScoreData += ` }\n`;
}
tmpScoreData += ' }\n';
if (staffHasLyrics) {
tmpScoreData += ` \\new Lyrics \\lyricsto "${partDataName}" { \n `;
// write out lyrics
tmpScoreData += ` ${renderLyrics(renderedStaff.lyrics)} \n } \n`;
}
ret.scoreData.push(tmpScoreData);
// render the score contents for sthe separate part
let tmpPartData = `\\new Staff { \n`;
if (transposition) tmpPartData += ` \\transpose ${transposition} { \n`;
tmpPartData += `${clefname} \n \\${partDataName } \n`;
if (staffHasLyrics) {
tmpPartData += ` \\new Voice = ${partDataName} {\n`;
}
if (transposition) tmpPartData += ` }\n`;
if (staffHasLyrics) {
tmpScoreData += ` }\n`;
}
tmpPartData += `}\n`;
if (staffHasLyrics) {
tmpPartData += ` \\new Lyrics \\lyricsto "${partDataName}" { \n `;
// write out lyrics
tmpPartData += ` ${renderLyrics(renderedStaff.lyrics)} \n } \n`;
}
ret.partData[partName] = tmpPartData;
ret.lyricData.push(renderedStaff.lyrics);
}
return ret;
}
export const renderLilypond = (partsInfo, metaInfo, options = {}) => {
// there are two parts we are going to render
// the parts music library, the score and possibly the parts (which can be part of the same file)
const data = {
musicData: {},
scoreData: [], // in the score, the order does matter
partData: {},
};
const musicKeyCount = {};
const partKeyCount = {};
partsInfo.forEach((partInfo) => {
if (partInfo.isSection) {
const parts = partInfo.parts.map((part) => {
return renderPart(part, data);
});
// we can now copy the musicdata to the data.musicData
let ret = " \\new StaffGroup <<\n";
parts.forEach((part) => {
Object.keys(part.musicData).forEach((key) => {
data.musicData[key] = part.musicData[key];
});
Object.keys(part.scoreData).forEach((key) => {
ret += ` ${part.scoreData[key]}\n`;
});
Object.keys(part.partData).forEach((key) => {
data.partData[key] = part.partData[key];
});
});
ret += " >>\n";
data.scoreData.push(ret);
}
else {
// not a section, but a part
const part = renderPart(partInfo, data);
// we can now copy the musicdata to the data.musicData
Object.keys(part.musicData).forEach((key) => {
if (!data.musicData[key]) {
data.musicData[key] = part.musicData[key];
musicKeyCount[key] = 1;
}
else {
musicKeyCount[key] += 1;
const newKey = createValidPartName(`${key}${musicKeyCount[key]}`);
data.musicData[newKey] = part.musicData[key];
}
});
Object.keys(part.partData).forEach((key) => {
if (!data.partData[key]) {
data.partData[key] = part.partData[key];
partKeyCount[key] = 1;
}
else {
partKeyCount[key] += 1;
const newKey = createValidPartName(`${key}${partKeyCount[key]}`);
data.partData[newKey] = part.partData[key];
}
});
data.scoreData.push(part.scoreData);
}
});
let music = "";
Object.keys(data.musicData).forEach((key) => {
music += `${key} = { \n ${data.musicData[key].join(' |\n ')} \\bar "|."\n}\n`;
});
let parts = "";
Object.keys(data.partData).forEach((key) => {
parts += "\\book {\n";
if (options.partsPaperSize) {
parts += ` \\paper {\n #(set-paper-size "${options.partsPaperSize}")\n }\n`;
}
parts += ` \\bookOutputSuffix "${key}"\n`;
parts += ` \\header {\n`;
if (metaInfo.workTitle) {
parts += ` title = "${metaInfo.workTitle}"\n`;
}
if (metaInfo.subtitle) {
parts += ` subtitle = "${metaInfo.subtitle}"\n`;
}
if (metaInfo.composer) {
parts += ` composer = "${metaInfo.composer}"\n`;
}
if (metaInfo.lyricist) {
parts += ` lyricist = "${metaInfo.lyricist}"\n`;
}
if (metaInfo.arranger) {
parts += ` arranger = "${metaInfo.arranger}"\n`;
}
// instrument name
parts += ` instrument = "${key}"\n`;
parts += " }\n";
parts += " \\score {\n"
parts += ` ${data.partData[key]}\n`
if (options.partsStaffSize) {
parts += " \\layout {\n";
parts += ` #(layout-set-staff-size ${options.partsStaffSize})\n`;
parts += " }\n";
}
parts += " }\n";
parts += "}\n";
});
let score = "\\book {\n";
if (options.scorePaperSize) {
score += ` \\paper {\n #(set-paper-size "${options.scorePaperSize}")\n }\n`;
}
score += ` \\header {\n`;
if (metaInfo.workTitle) {
score += ` title = "${metaInfo.workTitle}"\n`;
}
if (metaInfo.subtitle) {
score += ` subtitle = "${metaInfo.subtitle}"\n`;
}
if (metaInfo.composer) {
score += ` composer = "${metaInfo.composer}"\n`;
}
if (metaInfo.lyricist) {
score += ` lyricist = "${metaInfo.lyricist}"\n`;
}
if (metaInfo.arranger) {
score += ` arranger = "${metaInfo.arranger}"\n`;
}
score += " }\n";
score += " \\score { <<\n";
data.scoreData.forEach((part) => {
score += part;
});
score += " >>\n";
if (options.scoreStaffSize) {
score += " \\layout {\n";
score += ` #(layout-set-staff-size ${options.scoreStaffSize})\n`;
score += " }\n";
}
score += " }\n";
score += "}\n"; // book end
return {
music,
parts,
score
}
}
/**
*
* @param {XmlWrapper} data MuseScore XML data converted to JS
* @returns
*/
export const convertMSCX2LY = (data, options = {}) => {
// step 1: Order
// this is a list of instruments used, and adds the family attribute to the instruments
// it also describes the sections of the score by family
const Score = data.get('Score');
const orderInfo = readOrderInfo(Score.get('Order'));
// step 2: Staff
const staffInfo = readStaffInfo(Score.get('Staff'));
// step 3: the parts
const partsInfo = readPartsInfo(Score.get('Part'), orderInfo, staffInfo);
// step 4: the meta info
const metaInfo = parseMetaTag(Score.get('metaTag'));
// with the parts info we have the parts in the order they are in the score
// now we can start generating the lilypond structure
// we will go throught the parts, section by section, and generate the lilypond structure and data
const lilypondData = renderLilypond(partsInfo, metaInfo, options);
return lilypondData;
}
export const renderLyricTimings = (data ) => {
const Score = data.get('Score');
const orderInfo = readOrderInfo(Score.get('Order'));
// step 2: Staff
const staffInfo = readStaffIn