mscx2ly
Version:
Tool to render lilypond code from a MuseScore save file
887 lines (842 loc) • 31.5 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');
return `\\time ${n}/${d}`;
}
const durationMap = {
"1": "1",
"1/1": "1",
"1/2": "2",
"2/2": "2",
"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;
}
/**
*
* @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 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) => {
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];
}
const notes = noteInfo.map((note) => {
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(' ');
}
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"';
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'),
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.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
const sections = order.get('section').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'].includes(child.name);
});
//
// return {
// KeySig: voice.KeySig,
// TimeSig: voice.TimeSig,
// Tempo: voice.Tempo,
// Rest: voice.Rest,
// Dynamic: voice.Dynamic,
// Spanner: voice.Spanner,
// Chord: voice.Chord,
// Barline: voice.Barline,
// }
})
}
})
}
return ret;
});
};
export const renderMusicForStaff = (staffContents) => {
let currentKeySig = null;
let currentTimeSig = null;
const ret = staffContents.map((measure) => {
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) => {
return voice.map((evt) => {
switch (evt.name) {
case 'KeySig': {
currentKeySig = evt;
return renderKeySig(evt);
}
case 'TimeSig': {
currentTimeSig = evt;
return parseTimeSig(evt);
}
case 'Rest': {
return parseRest(evt, currentTimeSig);
}
case 'Chord': {
return parseChord(evt, currentTimeSig, currentKeySig);
}
case 'Clef': {
const clef = evt.get('concertClefType') || evt.get('transposingClefType');
return renderClef(clef);
}
case 'BarLine': {
return renderBarLine(evt);
}
default: return '';
}
}).join(' ');
});
if (parsedVoices.length > 1) {
return `${measureText} << { ${parsedVoices.join(' } \\\\ { ')} } >>`;
}
return `${measureText} ${parsedVoices[0]}`;
});
return ret;
}
const renderStaff = (staff) => {
const ret = {
id: staff.id,
defaultClef: staff.defaultClef,
isStaffVisible: staff.isStaffVisible? staff.isStaffVisible[0] : null,
measures: renderMusicForStaff(staff.contents)
};
return ret;
}
const renderPart = (part) => {
const partName = part.trackName;
const longName = part.instrument[0].longName;
const shortName = part.instrument[0].shortName;
const ret = {
musicData: {},
scoreData: [], // in the score, the order does matter
partData: {}
};
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`;
tmpScoreData += ` \\${partDataName}\n`;
tmpScoreData += `}\n`;
});
tmpScoreData += ">>\n";
ret.scoreData.push(tmpScoreData);
ret.partData[partName] = tmpScoreData;
}
else {
const renderedStaff = renderStaff(part.staffs[0]);
const partDataName = createValidPartName(partName);
ret.musicData[partDataName] = renderedStaff.measures;
const clefname = renderClef(part.staffs[0].defaultClef);
let tmpScoreData = " \\new Staff {\n";
tmpScoreData += ` \\set Staff.instrumentName = "${longName}"\n`;
tmpScoreData += ` \\set Staff.shortInstrumentName = "${shortName}"\n`;
tmpScoreData += ` ${clefname} \n`;
tmpScoreData += ` \\${partDataName}\n`;
tmpScoreData += ' }\n';
ret.scoreData.push(tmpScoreData);
ret.partData[partName] = `\\new Staff { ${clefname} \n \\${partDataName} }\n`;
}
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;
}