// // OBSOLETE code, only for archive // import _ from "lodash"; import {DOMParser, XMLSerializer} from "xmldom"; import {MusicNotation} from "@k-l-lambda/music-widgets"; import {MIDI} from "@k-l-lambda/music-widgets"; import {Readable} from "stream"; import npmPackage from "../package.json"; import {xml2ly, engraveSvg, LilyProcessOptions} from "./lilyCommands"; import {LilyDocument, replaceSourceToken, LilyTerms} from "../inc/lilyParser"; import * as staffSvg from "../inc/staffSvg"; import {SingleLock} from "../inc/mutex"; import {PitchContextTable} from "../inc/pitchContext"; import * as LilyNotation from "../inc/lilyNotation"; import {svgToPng} from "./canvas"; import LogRecorder from "../inc/logRecorder"; import ScoreJSON, {NoteLinking} from "../inc/scoreJSON"; import {LilyDocumentAttribute, LilyDocumentAttributeReadOnly} from "../inc/lilyParser/lilyDocument"; import {Block} from "../inc/lilyParser/lilyTerms"; interface GrammarParser { parse (source: string): any; }; const markupLily = (source: string, markup: string, lilyParser: GrammarParser): string => { const docMarkup = new LilyDocument(lilyParser.parse(markup)); const docSource = new LilyDocument(lilyParser.parse(source)); // copy attributes const attrS = docSource.globalAttributes() as LilyDocumentAttribute; const attrM = docMarkup.globalAttributes({readonly: true}) as LilyDocumentAttributeReadOnly; [ "staffSize", "paperWidth", "paperHeight", "topMargin", "bottomMargin", "leftMargin", "rightMargin", "systemSpacing", "topMarkupSpacing", "raggedLast", "raggedBottom", "raggedLastBottom", "printPageNumber", ].forEach(field => { if (attrM[field] !== undefined) { if (typeof attrS[field].value === "object" && attrS[field].value && (attrS[field].value as any).set) (attrS[field].value as any).set(attrM[field]); else attrS[field].value = attrM[field]; } }); // execute commands list const commands = docMarkup.root.getField("LotusCommands"); const cmdList = commands && commands.value && commands.value.args && commands.value.args[0].body; if (cmdList && Array.isArray(cmdList)) { for (const command of cmdList) { if (command.exp && docSource[command.exp]) docSource[command.exp](); else console.warn("unexpected markup command:", command); } } // copy LotusOption assignments const assignments = docMarkup.root.entries.filter(term => term instanceof LilyTerms.Assignment && /^LotusOption\..+/.test(term.key.toString())); assignments.forEach(assignment => docSource.root.sections.push(assignment.clone())); return docSource.toString(); }; const xmlBufferToLy = async (xml: Buffer, options: LilyProcessOptions = {}): Promise => { const bom = (xml[0] << 8 | xml[1]); const utf16 = bom === 0xfffe; const content = xml.toString(utf16 ? "utf16le" : "utf8"); return await xml2ly(content, {replaceEncoding: utf16, ...options}); }; const unescapeStringExp = exp => exp && exp.toString(); const makeScoreV1 = async (source: string, lilyParser: GrammarParser, {midi, logger}: {midi?: MIDI.MidiData, logger?: LogRecorder} = {}): Promise => { const t0 = Date.now(); const engraving = await engraveSvg(source); logger.append("scoreMaker.profile.engraving", {cost: Date.now() - t0}); logger.append("lilypond.log", engraving.logs); const lilyDocument = new LilyDocument(lilyParser.parse(source)); const {doc, hashTable} = staffSvg.createSheetDocumentFromSvgs(engraving.svgs, source, lilyDocument, {logger, DOMParser}); const sheetNotation = staffSvg.StaffNotation.parseNotationFromSheetDocument(doc, {logger}); const attributes = lilyDocument.globalAttributes({readonly: true}); const meta = { title: unescapeStringExp(attributes.title), composer: unescapeStringExp(attributes.composer), pageSize: doc.pageSize, pageCount: doc.pages.length, staffSize: attributes.staffSize as number, }; midi = midi || engraving.midi; const midiNotation = MusicNotation.Notation.parseMidi(midi); const t5 = Date.now(); const matcher = await staffSvg.StaffNotation.matchNotations(midiNotation, sheetNotation); logger.append("scoreMaker.profile.matching", {cost: Date.now() - t5}); if (logger) { const cis = new Set(Array(matcher.criterion.notes.length).keys()); matcher.path.forEach(ci => cis.delete(ci)); const omitC = cis.size; const omitS = matcher.path.filter(ci => ci < 0).length; const coverage = ((matcher.criterion.notes.length - omitC) / matcher.criterion.notes.length) * ((matcher.sample.notes.length - omitS) / matcher.sample.notes.length); logger.append("makeScore.match", {coverage, omitC, omitS, path: matcher.path}); } const matchedIds: Set = new Set(); midiNotation.notes.forEach(note => note.ids && note.ids.forEach(id => matchedIds.add(id))); doc.updateMatchedTokens(matchedIds); const pitchContextGroup = PitchContextTable.createPitchContextGroup(sheetNotation.pitchContexts, midiNotation); const noteLinkings = midiNotation.notes.map(note => _.pick(note, ["ids", "staffTrack", "contextIndex"]) as NoteLinking); logger.append("scoreMaker.profile.full", {cost: Date.now() - t0}); return { meta, doc, midi, hashTable, noteLinkings, pitchContextGroup, }; }; interface IncompleteScoreJSON { meta?: any, doc?: any; hashTable?: {[key: string]: any}; midi?: MIDI.MidiData; noteLinkings?: NoteLinking; pitchContextGroup?: any; }; interface SheetNotationResult extends IncompleteScoreJSON { midiNotation: MusicNotation.NotationData; sheetNotation: staffSvg.StaffNotation.SheetNotation; lilyDocument: LilyDocument; bakingImages?: Readable[]; }; const makeScoreV2 = async (source: string, lilyParser: GrammarParser, {midi, logger}: {midi?: MIDI.MidiData, logger?: LogRecorder} = {}): Promise => { let midiNotation = null; const pages = []; const hashTable = {}; const t0 = Date.now(); const attrGen = new SingleLock(true); const engraving = await engraveSvg(source, { // do some work during lilypond process running to save time onProcStart: () => { //console.log("tp.0:", Date.now() - t0); const lilyDocument = new LilyDocument(lilyParser.parse(source)); attrGen.release(lilyDocument.globalAttributes({readonly: true}) as LilyDocumentAttributeReadOnly); //console.log("tp.1:", Date.now() - t0); }, onMidiRead: midi_ => { //console.log("tm.0:", Date.now() - t0); if (!midi) { midi = midi_; midiNotation = midi && MusicNotation.Notation.parseMidi(midi); } //console.log("tm.1:", Date.now() - t0); }, onSvgRead: async svg => { //console.log("ts.0:", Date.now() - t0); const attributes = await attrGen.wait(); const page = staffSvg.parseSvgPage(svg, source, {DOMParser, logger, attributes}); pages.push(page.structure); Object.assign(hashTable, page.hashTable); //console.log("ts.1:", Date.now() - t0); }, }); //console.log("t2:", Date.now() - t0); logger.append("scoreMaker.profile.engraving", {cost: Date.now() - t0}); logger.append("lilypond.log", engraving.logs); const doc = new staffSvg.SheetDocument({pages}); //console.log("t3:", Date.now() - t0); const attributes = await attrGen.wait(); const meta = { title: unescapeStringExp(attributes.title), composer: unescapeStringExp(attributes.composer), pageSize: doc.pageSize, pageCount: doc.pages.length, staffSize: attributes.staffSize, }; if (!midiNotation) { console.warn("Neither lilypond or external arguments did not offer MIDI data, score maker finish incompletely."); return { meta, doc, midi, hashTable, }; } //console.log("t4:", Date.now() - t0); const sheetNotation = staffSvg.StaffNotation.parseNotationFromSheetDocument(doc, {logger}); //console.log("t5:", Date.now() - t0); const t5 = Date.now(); const matcher = await staffSvg.StaffNotation.matchNotations(midiNotation, sheetNotation); //console.log("t6:", Date.now() - t0); logger.append("scoreMaker.profile.matching", {cost: Date.now() - t5}); if (logger) { const cis = new Set(Array(matcher.criterion.notes.length).keys()); matcher.path.forEach(ci => cis.delete(ci)); const omitC = cis.size; const omitS = matcher.path.filter(ci => ci < 0).length; const coverage = ((matcher.criterion.notes.length - omitC) / matcher.criterion.notes.length) * ((matcher.sample.notes.length - omitS) / matcher.sample.notes.length); logger.append("markScore.match", {coverage, omitC, omitS, path: matcher.path}); } //console.log("t7:", Date.now() - t0); const matchedIds: Set = new Set(); midiNotation.notes.forEach(note => note.ids && note.ids.forEach(id => matchedIds.add(id))); //console.log("t8:", Date.now() - t0); doc.updateMatchedTokens(matchedIds); //console.log("t9:", Date.now() - t0); const pitchContextGroup = PitchContextTable.createPitchContextGroup(sheetNotation.pitchContexts, midiNotation); //console.log("t10:", Date.now() - t0); const noteLinkings = midiNotation.notes.map(note => _.pick(note, ["ids", "staffTrack", "contextIndex"])); //console.log("t11:", Date.now() - t0); logger.append("scoreMaker.profile.full", {cost: Date.now() - t0}); return { meta, doc, midi, hashTable, noteLinkings, pitchContextGroup, }; }; const makeSheetNotation = async (source: string, lilyParser: GrammarParser, {withNotation = true, logger, lilyDocument, includeFolders, baking}: { withNotation?: boolean, logger?: LogRecorder, lilyDocument?: LilyDocument, includeFolders?: string[], baking?: boolean, } = {}): Promise => { let midi = null; let midiNotation = null; const pages = []; const hashTable = {}; const bakingImages = []; const t0 = Date.now(); type ParserArguments = {attributes: LilyDocumentAttributeReadOnly, tieLocations: {[key: string]: boolean}}; const argsGen = new SingleLock(true); const engraving = await engraveSvg(source, { includeFolders, // do some work during lilypond process running to save time onProcStart: () => { //console.log("tp.0:", Date.now() - t0); if (!lilyDocument) { lilyDocument = new LilyDocument(lilyParser.parse(source)); lilyDocument.interpret(); } const attributes = lilyDocument.globalAttributes({readonly: true}) as LilyDocumentAttributeReadOnly; //const tieLocations = lilyDocument.getTiedNoteLocations(text) const tieLocations = lilyDocument.getTiedNoteLocations2() .reduce((table, loc) => ((table[`${loc[0]}:${loc[1]}`] = true), table), {}); argsGen.release({attributes, tieLocations}); //console.log("tp.1:", Date.now() - t0); }, onMidiRead: withNotation && (midi_ => { //console.log("tm.0:", Date.now() - t0); midi = midi_; midiNotation = midi && MusicNotation.Notation.parseMidi(midi); //console.log("tm.1:", Date.now() - t0); }), onSvgRead: async (index, svg) => { //console.log("ts.0:", Date.now() - t0); const args = await argsGen.wait(); const page = staffSvg.parseSvgPage(svg, source, {DOMParser, logger, ...args}); pages[index] = page.structure; Object.assign(hashTable, page.hashTable); //console.log("ts.1:", Date.now() - t0); }, }); logger.append("scoreMaker.profile.engraving", {cost: Date.now() - t0}); logger.append("lilypond.log", engraving.logs); const doc = new staffSvg.SheetDocument({pages}); staffSvg.postProcessSheetDocument(doc, lilyDocument); if (baking) { await Promise.all(engraving.svgs.map(async (svg, index) => { const svgText = staffSvg.turnRawSvgWithSheetDocument(svg, pages[index], {DOMParser, XMLSerializer}); bakingImages[index] = await svgToPng(Buffer.from(svgText)); })); } const {attributes} = await argsGen.wait(); const meta = { title: unescapeStringExp(attributes.title), composer: unescapeStringExp(attributes.composer), pageSize: doc.pageSize, pageCount: doc.pages.length, staffSize: attributes.staffSize, }; const sheetNotation = staffSvg.StaffNotation.parseNotationFromSheetDocument(doc, {logger}); // correct notation time by location-tick table from lily document const tickTable = lilyDocument.getLocationTickTable(); staffSvg.StaffNotation.assignTickByLocationTable(sheetNotation, tickTable); return { midi, bakingImages: baking ? bakingImages : null, midiNotation, sheetNotation, meta, doc, hashTable, lilyDocument, }; }; const makeScoreV3 = async (source: string, lilyParser: GrammarParser, {midi, logger, unfoldRepeats = false, includeFolders}: { midi?: MIDI.MidiData, logger?: LogRecorder, unfoldRepeats?: boolean, includeFolders?: string[], } = {}): Promise => { const t0 = Date.now(); let lilyDocument = null; let unfoldSource = null; if (unfoldRepeats) { lilyDocument = new LilyDocument(lilyParser.parse(source)); lilyDocument.interpret(); if (lilyDocument.containsRepeat()) { lilyDocument.unfoldRepeats(); unfoldSource = lilyDocument.toString(); // keep 2 version lilypond source note href uniform source = replaceSourceToken(unfoldSource, "\\unfoldRepeats"); } } const foldData = await makeSheetNotation(source, lilyParser, {logger, lilyDocument, withNotation: !midi && !unfoldSource, includeFolders}); const {meta, doc, hashTable} = foldData; lilyDocument = lilyDocument || foldData.lilyDocument; let midiNotation = foldData.midiNotation; let sheetNotation = foldData.sheetNotation; if (midi) midiNotation = MusicNotation.Notation.parseMidi(midi); if (unfoldSource) { const unfoldData = await makeSheetNotation(unfoldSource, lilyParser, {logger, lilyDocument, withNotation: !midi, includeFolders}); midi = midi || unfoldData.midi; midiNotation = unfoldData.midiNotation; sheetNotation = unfoldData.sheetNotation; } midi = midi || foldData.midi; if (!midi || !sheetNotation) { if (!midi) console.warn("Neither lilypond or external arguments did not offer MIDI data, score maker finished incompletely."); if (!sheetNotation) console.warn("sheetNotation parsing failed, score maker finished incompletely."); return { meta, doc, midi, hashTable, }; } const t5 = Date.now(); const matcher = await staffSvg.StaffNotation.matchNotations(midiNotation, sheetNotation); logger.append("scoreMaker.profile.matching", {cost: Date.now() - t5}); if (logger && logger.enabled) { const cis = new Set(Array(matcher.criterion.notes.length).keys()); matcher.path.forEach(ci => cis.delete(ci)); const omitC = cis.size; const omitS = matcher.path.filter(ci => ci < 0).length; const coverage = ((matcher.criterion.notes.length - omitC) / matcher.criterion.notes.length) * ((matcher.sample.notes.length - omitS) / matcher.sample.notes.length); logger.append("makeScore.match", {coverage, omitC, omitS, path: matcher.path}); } const matchedIds: Set = new Set(); midiNotation.notes.forEach(note => note.ids && note.ids.forEach(id => matchedIds.add(id))); doc.updateMatchedTokens(matchedIds); const pitchContextGroup = PitchContextTable.createPitchContextGroup(sheetNotation.pitchContexts, midiNotation); const noteLinkings = midiNotation.notes.map(note => _.pick(note, ["ids", "staffTrack", "contextIndex"]) as NoteLinking); logger.append("scoreMaker.profile.full", {cost: Date.now() - t0}); return { meta, doc, midi, hashTable, noteLinkings, pitchContextGroup, }; }; const makeScoreV4 = async (source: string, lilyParser: GrammarParser, {midi, logger, unfoldRepeats = false, baking = false, includeFolders}: { midi?: MIDI.MidiData, logger?: LogRecorder, unfoldRepeats?: boolean, includeFolders?: string[], baking?: boolean, } = {}): Promise<{ bakingImages?: Readable[], score: ScoreJSON | IncompleteScoreJSON, }> => { const t0 = Date.now(); let lilyDocument = null; let unfoldSource = null; if (unfoldRepeats) { lilyDocument = new LilyDocument(lilyParser.parse(source)); lilyDocument.interpret(); if (lilyDocument.containsRepeat()) { lilyDocument.unfoldRepeats(); unfoldSource = lilyDocument.toString(); // keep 2 version lilypond source note href uniform source = replaceSourceToken(unfoldSource, "\\unfoldRepeats"); } } const foldData = await makeSheetNotation(source, lilyParser, {logger, lilyDocument, withNotation: !midi && !unfoldSource, includeFolders, baking}); const {meta, doc, hashTable, bakingImages} = foldData; lilyDocument = lilyDocument || foldData.lilyDocument; //let sheetNotation = foldData.sheetNotation; const matchingMidi = midi || foldData.midi; if (unfoldSource) { const unfoldData = await makeSheetNotation(unfoldSource, lilyParser, {logger, lilyDocument, withNotation: !midi, includeFolders}); midi = midi || unfoldData.midi; //sheetNotation = unfoldData.sheetNotation; } midi = midi || foldData.midi; const lilyNotation = lilyDocument.interpret().getNotation(); if (!midi || !lilyNotation) { if (!midi) console.warn("Neither lilypond or external arguments did not offer MIDI data, score maker finished incompletely."); if (!lilyNotation) console.warn("sheetNotation parsing failed, score maker finished incompletely."); return { score: { version: npmPackage.version, meta, doc, midi, hashTable, }, }; } const t5 = Date.now(); const matcher = await LilyNotation.matchWithMIDI(lilyNotation, matchingMidi); logger.append("scoreMaker.profile.matching", {cost: Date.now() - t5}); if (logger && logger.enabled) { const cis = new Set(Array(matcher.criterion.notes.length).keys()); matcher.path.forEach(ci => cis.delete(ci)); const omitC = cis.size; const omitS = matcher.path.filter(ci => ci < 0).length; const coverage = ((matcher.criterion.notes.length - omitC) / matcher.criterion.notes.length) * ((matcher.sample.notes.length - omitS) / matcher.sample.notes.length); logger.append("makeScore.match", {coverage, omitC, omitS, path: matcher.path}); } const midiNotation = matcher.sample; const matchedIds: Set = new Set(); midiNotation.notes.forEach(note => note.ids && note.ids.forEach(id => matchedIds.add(id))); doc.updateMatchedTokens(matchedIds); if (baking) doc.pruneForBakingMode(); const pitchContextGroup = PitchContextTable.createPitchContextGroup( lilyNotation.pitchContextGroup.map(table => table.items.map(item => item.context)), midiNotation); const noteLinkings = midiNotation.notes.map(note => _.pick(note, ["ids", "staffTrack", "contextIndex"]) as NoteLinking); logger.append("scoreMaker.profile.full", {cost: Date.now() - t0}); return { bakingImages, score: { version: npmPackage.version, meta, doc, midi, hashTable: !baking ? hashTable : null, noteLinkings, pitchContextGroup, }, }; }; void makeScoreV1; void makeScoreV2; void makeScoreV3; const makeScore = makeScoreV4; const makeMIDI = async (source: string, lilyParser: GrammarParser, {unfoldRepeats = false, fixNestedRepeat = false, includeFolders = undefined} = {}): Promise => { const lilyDocument = new LilyDocument(lilyParser.parse(source)); if (fixNestedRepeat) lilyDocument.fixNestedRepeat(); if (unfoldRepeats) lilyDocument.unfoldRepeats(); const score = lilyDocument.root.getBlock("score"); if (score) { // remove layout block to save time score.body = score.body.filter(term => !(term instanceof LilyTerms.Block && term.head === "\\layout")); // remove invalid tempo const midi: any = score.body.find(term => term instanceof LilyTerms.Block && term.head === "\\midi"); if (midi) midi.body = midi.body.filter(term => !(term instanceof LilyTerms.Tempo && term.beatsPerMinute > 200)); } const markupSource = lilyDocument.toString(); //console.log("markupSource:", markupSource); return new Promise((resolve, reject) => engraveSvg(markupSource, { includeFolders, onMidiRead: resolve, }).catch(reject)); }; const makeArticulatedMIDI = async (source: string, lilyParser: GrammarParser, {ignoreRepeats = true, includeFolders = undefined} = {}): Promise => { const lilyDocument = new LilyDocument(lilyParser.parse(source)); if (ignoreRepeats) lilyDocument.removeRepeats(); lilyDocument.articulateMIDIOutput(); // remove layout block to save time lilyDocument.root.sections = lilyDocument.root.sections.filter(section => !(section instanceof Block) || !(section.head === "\\score") || section.isMIDIDedicated); const markupSource = lilyDocument.toString(); //console.log("markupSource:", markupSource); return new Promise((resolve, reject) => engraveSvg(markupSource, { includeFolders, onMidiRead: resolve, }).catch(reject)); }; export { markupLily, xmlBufferToLy, makeScore, makeMIDI, makeArticulatedMIDI, };