You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
9.7 KiB
213 lines
9.7 KiB
#! /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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
"""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 <c'>4 <e'>4 <g'>4 <c''>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
|
|
|