#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), Laborejo2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ """Use generated music data and build a complete lilypond file from it""" import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules import os.path from datetime import date import subprocess from tempfile import gettempdir #Third Party Modules #Template Modules from template.start import PATHS #Our modules da = date.fromordinal(730920) # 730920th day after 1. 1. 0001 def saveAsLilypond(score, absoluteFilePath = None): #absoluteFilePath = self.absoluteFilePath + ".ly" if (not absoluteFilePath) else absoluteFilePath assert absoluteFilePath.endswith(".ly"), absoluteFilePath result = score.lilypond() if result: with open(absoluteFilePath, "w", encoding="utf-8") as f: f.write(result) return absoluteFilePath def saveAsLilypondPDF(score, openPDF = False): tempfile = os.path.join(gettempdir(), str(id(score)) + ".ly") exportedLilypond = saveAsLilypond(score, tempfile) ret = subprocess.call("lilypond --output={} {}".format(exportedLilypond, exportedLilypond), shell=True) #suffix to output added by lilypond if ret == 0 and openPDF: subprocess.Popen("xdg-open {}.pdf".format(exportedLilypond), shell=True) def stringToCharsOnlyString(string): def num2word(n): """Take a number, return a str. Lilypond does not allow numbers in variable names""" return ''.join(chr(ord(c)+17) for c in str(n)) def num2wordForIterations(stringOrNum): if stringOrNum in [1,2,3,4,5,6,7,8,9,0] or stringOrNum in "1234567890": return num2word(int(stringOrNum)) elif stringOrNum in ("-", "_"): return "" else: return stringOrNum string = string.replace(" ", "_") return "".join([num2wordForIterations(c) for c in string]) def lilyfy(string): """Escape special lilypond characters like the quote " etc. This is extended on a as-needed basis. """ if not string: return "" if string.startswith("\\markup"): return string #trust the user string = string.replace('\\n', '\n') #replace the user newline with actual one string = string.replace('"', '\\"') return string def fromTemplate(session, data, meta, tempoStaff): """Returns a string built from already ly-exported track data and a lilypond template. Called by score.lilypond(), which is called by session.lilypond(). meta is the actual score.metaData dictionary, not a copy. We use this to modify the template path entry. Laborejo Markers in the template have the syntax %$$DATE$$ . That is pretty unique and additionally a lilypondcomment. The template file is in metaData["template-file"]. The default is empty string, which means we use the programs "default.ly" template. For session management reasons this the user template must be a file in the session dir. However, a system wide user library of lilypond export templates is very useful and convenient. Thus we allow absolute paths as well. Or even relative and ~/ if the user wants. They will be converted automatically to a symlink into the session dir and the absolute path will be auto-replaced by the local filename to the symlink. (The absolute path will remain in metadata and gui field until the file is found. Thus a typo will not lead to a broken symlink creation) If template file lookup fails for any reason we use the internal default.ly and log an error message. """ #Find and load the template templateFile = meta["template-file"] try: templatePath = findTemplate(session, meta, templateFile) except FileNotFoundError: #Fall back to default.py and try again logger.error(f"User lilypond template {templateFile} not found. Will use the builtin default.ly instead.") templatePath = findTemplate(session, meta, "") #If this fails something is indeed wrong and we let the error raise except PermissionError: #Fall back to default.py and try again logger.error(f"User lilypond template {templateFile} not readable. Will use the builtin default.ly instead.") templatePath = findTemplate(session, meta, "") #If this fails something is indeed wrong and we let the error raise #Now we know the templatePath exists and it is either default.ly or a file/link in our session dir. with open(templatePath, 'r') as f: templateString = f.read() templateString = templateString.replace("%$$DATE$$", da.today().strftime("%A %d. %B %Y")) #The current date templateString = templateString.replace("%$$GLOBAL-STAFF-SIZE$$", str(meta["global-staff-size"])) templateString = templateString.replace("%$$FILENAME$$", session.sessionPrefix) templateString = templateString.replace("%$$HEADER$$", processMeta(meta)) #templateString = templateString.replace("%$$SUBTEXT$$", '"' + lilyfy(meta["subtext"]) + '"') templateString = templateString.replace("%$$SUBTEXT$$", lilyfy(meta["subtext"]) ) voicesString, structureString = processData(data) templateString = templateString.replace("%$$VOICES$$", voicesString) templateString = templateString.replace("%$$STRUCTURE$$", structureString) templateString = templateString.replace("%$$TEMPOSTAFF$$", tempoStaff) templateString = templateString.replace("%$$TRANSPOSITION$$", '\\transpose ' + " ".join(meta["transposition"].split()) ) #something like "c' f". Defaults to "c c ". Lilypond output without ""! return templateString def findTemplate(session, meta, templateFile:str)->str: """returns a path. checks for existence and read access through NSMclient functionality. This will create a symlink in our session dir and always use this. It will also destructively change the user metadata to this symlink file, if present.""" if not templateFile: path = os.path.join(PATHS["share"], "lilypondTemplates", "default.ly") return path #we don't need the checks below for our own file #User provided a template file name or path #Our NSM client can safely import any file. If we try to "import" a file already in our session dir nothing bad will happen, we just use the file. assert session.nsmClient.importResource logger.info("Trying to use user provided ly template: " + templateFile) #This can throw FileNotFoundError or PermissionError, which we catch in the parent function. No need to check for ourselves again here. #We get just the filename for symlinks in our session dir. Quickly check that and adjust to absolute path. templateFile = os.path.expanduser(templateFile) # ~home if os.path.basename(templateFile) == templateFile: templateFile = os.path.join(session.sessionPrefix, templateFile) #no need to check. even if that fails it will be caught safely below and reported to the log path = session.nsmClient.importResource(templateFile) #it will always be an absolute path. Check the nsm INFO log. meta["template-file"] = os.path.basename(path) logger.info("Imported or re-used: " + path) return path def processData(data): """returns two strings. the first actual music data, VOICES, the second the structure and order, STRUCTURE""" def voice(track, lilypondTrack): """ tgliHJuGCFAIICICBEHBJABHIJE= { \key c \major 4 4 4 4 \bar "|."} """ return "{} = {}".format(stringToCharsOnlyString(track.name), "{ " + lilypondTrack + " }") def structure(track, lilypondTrack): """ \new Staff = "tgliHJuGCFAIICICBEHBJABHIJE_Staff" << \\new Voice = "tgliHJuGCFAIICICBEHBJABHIJE" \tgliHJuGCFAIICICBEHBJABHIJE >> %EndTrack """ name = stringToCharsOnlyString(track.name) instruments = '\\with {{ \n instrumentName = #"{}" \n shortInstrumentName = #"{}" }}\n'.format(lilyfy(track.initialInstrumentName), lilyfy(track.initialShortInstrumentName)) mergeTempo = "\\new Voice = \"Tempo\" \\tempoStaff" return """\\new Staff = "{}_Staff" {} << {} \\new Voice = "{}" \{} >> %EndTrack""".format(name, instruments, mergeTempo, name, name) voices = "\n ".join(voice(track, lilypondTrack) for track, lilypondTrack in data.items() if lilypondTrack) structure = "\n ".join(structure(track, lilypondTrack) for track, lilypondTrack in data.items() if lilypondTrack) return voices, structure def processMeta(meta): """returns a string with lilypond header data like title and composer""" def valueMarkup(value): value = value.strip() if value: if value.startswith("\\markup"): return value else: return "\"{}\"".format(value) #results in "value" else: return "##f" #We have more data than lilypond in there. Filter: whitelist = ("title", "subtitle", "dedication", "composer", "subsubtitle", "instrument", "meter", "arranger", "poet", "opus", "copyright", "tagline") metaString = "\n ".join(key + " = " + valueMarkup(value) for key, value in meta.items() if key in whitelist) return metaString