lotus / backend /lilyCommands.ts
k-l-lambda's picture
commit lotus dist.
d605f27
raw
history blame
11.9 kB
import fs from "fs";
import path from "path";
import child_process from "child-process-promise";
import {MIDI} from "@k-l-lambda/music-widgets";
import {Writable} from "stream";
import asyncCall from "../inc/asyncCall";
import {SingleLock} from "../inc/mutex";
import {preprocessXml} from "./xmlTools";
import * as lilyAddon from "./lilyAddon";
let MUSICXML2LY_PATH;
let MIDI2LY_PATH;
let LILYPOND_PATH;
let env; // setEnvironment must be called before use commands
const setEnvironment = e => {
env = e;
MUSICXML2LY_PATH = filePathResolve(env.LILYPOND_DIR, "musicxml2ly");
MIDI2LY_PATH = filePathResolve(env.LILYPOND_DIR, "midi2ly");
LILYPOND_PATH = filePathResolve(env.LILYPOND_DIR, "lilypond");
emptyCache();
if (env.LILYPOND_ADDON) {
if (env.LILYPOND_ADDON_ASSETS_DIR) {
process.env.LILYPOND_PATH = path.join(env.LILYPOND_ADDON_ASSETS_DIR, "bin/lilypond");
process.env.GUILE_LOAD_PATH = path.join(env.LILYPOND_ADDON_ASSETS_DIR, "share/guile/1.8");
process.env.FONTCONFIG_PATH = path.join(env.LILYPOND_ADDON_ASSETS_DIR, "share/fonts");
}
lilyAddon.loadAddon(env.LILYPOND_ADDON);
}
};
const _WINDOWS = process.platform === "win32";
const genHashString = (len = 8) => Buffer.from(Math.random().toString()).toString("base64").substr(3, 3 + len);
const filePathResolve = (...parts: string[]): string => {
const result = path.join(...parts);
return _WINDOWS ? `"${result}"` : result;
};
const emptyCache = async () => {
// empty temporary directory
try {
if (env.TEMP_DIR) {
if (_WINDOWS)
await child_process.exec(`del /q "${env.TEMP_DIR}*"`);
else
await child_process.exec(`rm ${env.TEMP_DIR}*`);
console.log("Temporary directory clear.");
}
}
catch (err) {
if (_WINDOWS)
console.log("emptyCache error:", err);
}
};
export interface LilyProcessOptions {
// xml
language?: string;
removeMeasureImplicit?: boolean;
replaceEncoding?: boolean;
removeNullDynamics?: boolean;
fixHeadMarkup?: boolean;
fixBackSlashes?: boolean;
roundTempo?: boolean;
escapedWordsDoubleQuotation?: boolean;
removeTrivialRests?: boolean;
removeBadMetronome?: boolean;
removeInvalidHarmonies?: boolean;
removeAllHarmonies?: boolean;
fixChordVoice?: boolean;
fixBarlines?: boolean;
removeInvalidClef?: boolean;
// lilypond
pointClick?: boolean;
midi?: boolean;
removeBreak?: boolean;
removePageBreak?: boolean;
removeInstrumentName?: boolean;
removeTempo?: boolean;
tupletReplace?: boolean;
};
const postProcessLy = (ly: string, {
pointClick = true,
midi = true,
removeBreak = false,
removePageBreak = false,
removeInstrumentName = false,
removeTempo = false,
tupletReplace = false,
} = {}): string => {
let result = ly;
if (pointClick)
result = result.replace(/\\pointAndClickOff\n/g, "");
if (midi)
result = result.replace(/% \\midi/g, "\\midi");
if (removeBreak)
result = result.replace(/\s\\break\s/g, " ");
if (removePageBreak)
result = result.replace(/\s\\pageBreak\s/g, " ");
if (removeInstrumentName)
result = result.replace(/\\set Staff\.instrumentName/g, "% \\set Staff.instrumentName");
if (removeTempo)
result = result.replace(/\\tempo /g, "% \\tempo ");
if (tupletReplace) {
result = result
.replace(/4\*128\/384/g, "8*2/3")
.replace(/4\*64\/384/g, "16*2/3");
}
return result;
};
const xml2ly = async (xml: string, {language = "english", ...options}: LilyProcessOptions): Promise<string> => {
xml = preprocessXml(xml, options);
//console.log("xml:", options, xml.substr(0, 100));
const hash = genHashString();
const xmlFileName = `${env.TEMP_DIR}xml2ly-${hash}.xml`;
await asyncCall(fs.writeFile, xmlFileName, xml);
const lyFileName = `${env.TEMP_DIR}xml2ly-${hash}.ly`;
if (env.MUSICXML2LY_BY_PYTHON) {
await child_process.spawn(path.resolve(env.LILYPOND_DIR, "python"), [
path.resolve(env.LILYPOND_DIR, "musicxml2ly.py"), xmlFileName, "-o", lyFileName,
...(language ? ["-l", language] : []),
]);
}
else
await child_process.exec(`${MUSICXML2LY_PATH} ${xmlFileName} -o ${lyFileName} ${language ? "-l " + language : ""}`, {maxBuffer: 0x80000});
//console.log("musicxml2ly:", result.stdout, result.stderr);
const ly = await asyncCall(fs.readFile, lyFileName);
return postProcessLy(ly.toString(), options);
};
const midi2ly = async (midi, options: LilyProcessOptions): Promise<string> => {
const hash = genHashString();
//const midiFileName = `${env.TEMP_DIR}midi2ly-${hash}.midi`;
//await asyncCall(fs.writeFile, midiFileName, midi);
const lyFileName = `${env.TEMP_DIR}midi2ly-${hash}-midi.ly`;
let result;
if (env.MUSICXML2LY_BY_PYTHON) {
result = await child_process.spawn(path.resolve(env.LILYPOND_DIR, "python"),
[path.resolve(env.LILYPOND_DIR, "midi2ly.py"), midi.path, "-o", lyFileName]);
}
else
result = await child_process.exec(`${MIDI2LY_PATH} ${midi.path} -o ${lyFileName}`);
console.log("midi2ly:", result.stdout, result.stderr);
const ly = await asyncCall(fs.readFile, lyFileName);
return postProcessLy(ly.toString(), options);
};
const postProcessSvg = (svg: string): string => {
return svg.replace(/textedit:[^"]+:(\d+:\d+:\d+)/g, "textedit:$1");
};
//const nameNumber = name => Number(name.match(/-(\d+)\./)[1]);
// lilypond command line output file pattern
const FILE_BORN_OUPUT_PATTERN = /output\sto\s`(.+)'/;
export interface EngraverOptions {
onProcStart?: () => void|Promise<void>;
onMidiRead?: (content: MIDI.MidiData, options?: {filePath: string}) => void|Promise<void>;
onSvgRead?: (index: number, content: string, options?: {filePath: string}) => void|Promise<void>;
includeFolders?: string[]; // include folder path should be relative to TEMP_DIR
};
export interface EngraverResult {
logs: string;
svgs: string[];
midi: MIDI.MidiData;
errorLevel: number;
};
const engraveSvgCli = async (source: string,
{onProcStart, onMidiRead, onSvgRead, includeFolders = []}: EngraverOptions = {},
): Promise<Partial<EngraverResult>> => {
const hash = genHashString();
const sourceFilename = `${env.TEMP_DIR}engrave-${hash}.ly`;
await asyncCall(fs.writeFile, sourceFilename, source);
//console.log("ly source written:", sourceFilename);
let midi = null;
const svgs = [];
const fileReady = new SingleLock<string>();
const loadFile = async filename => {
//console.log("loadFile:", filename);
// wait for file writing finished
//await new Promise(resolve => setTimeout(resolve, 100));
const captures = filename.match(/\.(\w+)$/);
if (!captures) {
console.warn("invalid filename:", filename);
return;
}
const [_, ext] = captures;
const filePath = path.resolve(env.TEMP_DIR, filename);
//console.log("file output:", filePath, ext);
switch (ext) {
case env.MIDI_FILE_EXTEND: {
// wait extra time for MIDI file
//await fileReady.lock();
await new Promise(resolve => setTimeout(resolve, 100));
const buffer = await asyncCall(fs.readFile, filePath);
if (!buffer.length)
console.warn("empty MIDI buffer:", filename);
midi = MIDI.parseMidiData(buffer);
await onMidiRead && onMidiRead(midi, {filePath});
}
break;
case "svg": {
// eslint-disable-next-line
const captures = filename.match(/(\d+)\.svg$/);
const index = captures ? Number(captures[1]) - 1 : 0;
const buffer = await asyncCall(fs.readFile, filePath);
if (!buffer.length)
console.warn("empty SVG buffer:", filename);
const svg = postProcessSvg(buffer.toString());
svgs[index] = svg;
//console.log("svg load:", filePath);
await onSvgRead && onSvgRead(index, svg, {filePath});
}
break;
}
};
const loadProcs: Promise<void>[] = [];
const checkFile = async (line: string) => {
fileReady.release(line);
// skip error messages in command line output
if (FILE_BORN_OUPUT_PATTERN.test(line)) {
let newLine = await fileReady.lock();
while (/error|warning/.test(newLine))
newLine = await fileReady.lock();
let captures;
const exp = new RegExp(FILE_BORN_OUPUT_PATTERN.source, "g");
while (captures = exp.exec(line))
loadProcs.push(loadFile(captures[1]));
}
};
const includeParameters = includeFolders.map(folder => `--include=${folder}`).join(" ");
const proc: any = child_process.exec(`${LILYPOND_PATH} -dbackend=svg -o ${env.TEMP_DIR} ${includeParameters} ${sourceFilename}`,
{maxBuffer: 0x100000});
//const proc = child_process.spawn(LILYPOND_PATH, ["-dbackend=svg", `-o ${env.TEMP_DIR}`, sourceFilename]);
proc.childProcess.stdout.on("data", checkFile);
proc.childProcess.stderr.on("data", checkFile);
const startPromise = onProcStart && onProcStart();
if (startPromise)
loadProcs.push(startPromise);
const result = await proc;
fileReady.release();
await Promise.all(loadProcs);
//console.log("svgs:", svgs.length);
const validCount = svgs.filter(svg => svg).length;
if (validCount < svgs.length)
console.warn("svg loading incompleted: ", validCount, svgs.length);
return {
logs: result.stderr,
svgs,
midi,
};
};
const engraveSvgWithStreamCli = async (source: string, output: Writable, {includeFolders = []}: {includeFolders?: string[]} = {}) => {
const hash = genHashString();
const sourceFilename = `${env.TEMP_DIR}engrave-${hash}.ly`;
await asyncCall(fs.writeFile, sourceFilename, source);
const writing = new SingleLock();
const fileReady = new SingleLock();
const loadFile = async line => {
const [_, filename] = line.match(FILE_BORN_OUPUT_PATTERN);
const [__, ext] = filename.match(/\.(\w+)$/);
switch (ext) {
case "svg": {
await fileReady.lock();
await writing.wait();
writing.lock();
const filePath = path.resolve(env.TEMP_DIR, filename);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(output, {end: false});
fileStream.on("close", () => output.write("\n\n\n\n"));
writing.release();
}
break;
}
};
const checkFile = line => {
fileReady.release();
if (FILE_BORN_OUPUT_PATTERN.test(line))
loadFile(line);
};
const includeParameters = includeFolders.map(folder => `--include=${folder}`).join(" ");
const proc: any = child_process.exec(`${LILYPOND_PATH} -dbackend=svg -o ${env.TEMP_DIR} ${includeParameters} ${sourceFilename}`);
proc.childProcess.stdout.on("data", checkFile);
proc.childProcess.stderr.on("data", checkFile);
proc.then(() => fileReady.release());
return proc;
};
const engraveScm = async (source: string, {onProcStart, includeFolders = []}: {
onProcStart?: () => void|Promise<void>,
includeFolders?: string[], // include folder path should be relative to TEMP_DIR
} = {}): Promise<{
logs: string,
scm: string,
}> => {
const hash = genHashString();
const sourceFilename = `${env.TEMP_DIR}engrave-${hash}.ly`;
const targetFilename = `${env.TEMP_DIR}engrave-${hash}.scm`;
await fs.promises.writeFile(sourceFilename, source);
//console.log("ly source written:", sourceFilename);
const includeParameters = includeFolders.map(folder => `--include=${folder}`).join(" ");
const proc = child_process.exec(`${LILYPOND_PATH} -dbackend=scm -o ${env.TEMP_DIR} ${includeParameters} ${sourceFilename}`,
{maxBuffer: 0x100000});
await onProcStart && onProcStart();
const result = await proc;
const scmBuffer = await fs.promises.readFile(targetFilename);
return {
logs: result.stderr,
scm: scmBuffer.toString(),
};
};
const engraveSvg = async (source: string, options: EngraverOptions = {}): Promise<Partial<EngraverResult>> =>
(env.LILYPOND_ADDON ? lilyAddon.engraveSvg : engraveSvgCli)(source, options);
const engraveSvgWithStream = async (source: string, output: Writable, options: {includeFolders?: string[]} = {}) =>
(env.LILYPOND_ADDON ? lilyAddon.engraveSvgWithStream : engraveSvgWithStreamCli)(source, output, options);
export {
xml2ly,
midi2ly,
engraveSvg,
engraveSvgCli,
engraveSvgWithStream,
engraveSvgWithStreamCli,
engraveScm,
setEnvironment,
emptyCache,
postProcessSvg,
};