From be9dcdc8cff0c1de551ae7d283425f8aa122995a Mon Sep 17 00:00:00 2001 From: Nils Date: Fri, 29 Jul 2022 02:40:37 +0200 Subject: [PATCH] Add missing transpose lilypond code, add lilypond template filename to properties and metadata dialog. Will be symlinked into the session dir automatically. --- CHANGELOG | 11 +++ engine/lilypond.py | 89 +++++++++++++------ engine/main.py | 16 +++- engine/resources/lilypondTemplates/default.ly | 6 +- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ce5dd85..a749990 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,17 @@ Two empty lines before the next entry. External contributors notice at the end of the line: (LastName, FirstName / nick) +## 2022-10-15 2.2.0 +Add more time signatures to the quick-insert dialog: 8/4, three variants of 7/8 and two variants of 5/4 and more. Also reorder and better labels. +Fix splitting of notes that were created by a previous split. +Various small fixes, like typos in variable names and wrong string quotes. Small things can crash as well. +Lilypond: + Add transposition of the whole score to properties and metadata dialog + Only set tempo markings in parenthesis when user provided a string like Allegro + Add lilypond template filename to properties and metadata dialog. Will be symlinked into the session dir automatically. + Overhaul Key Signature GUI dialog to indicate "Deviation" better, instead of absolute accidentals. + + ## 2022-07-15 2.1.0 New function: custom key signature for any combination. Add area in the GUI to set initial key signature, metrical instructions and clefs, accessed through the track editor diff --git a/engine/lilypond.py b/engine/lilypond.py index 62cd515..4ab4f13 100644 --- a/engine/lilypond.py +++ b/engine/lilypond.py @@ -87,17 +87,44 @@ def lilyfy(string): string = string.replace('"', '\\"') return string -def fromTemplate(session, templateFile, data, meta, tempoStaff): +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(). - A template is either one of the built-ins "foo.ly" or - if the templateFile is None/"" the current filename+ly is used: "bar.lbj.ly" - Laborejo Markers in the template have the syntax %$$DATE$$ . That is pretty unique and additionaly a lilypond - comment. + 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. """ - #Load the template - templatePath = findTemplate(session, templateFile) + + #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() @@ -109,27 +136,39 @@ def fromTemplate(session, templateFile, data, meta, tempoStaff): 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, templateFile:str=None)->str: - """returns a path. checks for existence. - - There are two options: - - Use a standard template in SHARE/lilypondTemplates - - Use a template that belongs to the save file. - - The one that belongs to the save file needs to be created by hand by the user. - If not we throw a FileNotFoundError""" - if templateFile: - path = os.path.join(PATHS["share"], "lilypondTemplates", templateFile) - else: - path = os.path.join(session.sessionPrefix, "template.laborejo2.ly") #the name is always the same because the sessionPrefix is the unqiue part - assert path.endswith(".ly") - if os.path.exists(path): - return path - else: - raise FileNotFoundError(templateFile) +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, diff --git a/engine/main.py b/engine/main.py index 33abbab..9f21558 100644 --- a/engine/main.py +++ b/engine/main.py @@ -46,7 +46,9 @@ class Data(template.engine.sequencer.Score): #Lilypond Properties that might be used elsewhere as well, e.g. JACK Pretty Names self.metaData = {key:"" for key in ("title", "subtitle", "dedication","composer","subsubtitle","instrument","meter","arranger", "poet","piece","opus","copyright","tagline", "subtext")} - self.metaData["metronome"] = True #show metronome in printout? + self.metaData["metronome"] = True #show metronome in printout? v2.1.0 + self.metaData["transposition"] = "c c" # Whole score transposition for Lilypond. v2.2.0 + self.metaData["template-file"] = "" # Lilypond Template file. If empty use default.ly . v2.2.0 self.currentMetronomeTrack = self.tracks[0] #A Laborejo Track, indepedent of currentTrack. The metronome is in self.metronome, set by the template Score. self._processAfterInit() @@ -1058,7 +1060,9 @@ class Data(template.engine.sequencer.Score): """ tempoStaff = self.tempoTrack.lilypond() if "metronome" in self.metaData and self.metaData["metronome"] else "" data = {track:track.lilypond() for track in self.tracks} #processed in the lilypond module - return fromTemplate(session = self.parentSession, templateFile = "default.ly", data = data, meta = self.metaData, tempoStaff = tempoStaff) + #the template file name is in metadata, but it can be empty or not existient + #From Template has direct access to the score metadata and WILL destructively modify it's own parameters, like the lilypond template entry. + return fromTemplate(session = self.parentSession, data = data, meta = self.metaData, tempoStaff = tempoStaff) def getMidiInputNameAndUuid(self): """ @@ -1093,6 +1097,14 @@ class Data(template.engine.sequencer.Score): self.hiddenTracks = {Track.instanceFromSerializedData(parentData=self, serializedData=track):originalIndex for track, originalIndex in serializedData["hiddenTracks"]} self.tempoTrack = TempoTrack.instanceFromSerializedData(serializedData["tempoTrack"], parentData = self) self.metaData = serializedData["metaData"] + + if not "metronome" in self.metaData: #2.1.0 + self.metaData["metronome"] = True #replicate __init__ default + if not "transposition" in self.metaData: #2.2.0 + self.metaData["transposition"] = "c c" #replicate __init__ default + if not "template-file" in self.metaData: #2.2.0 + self.metaData["template-file"] = "" #replicate __init__ default + self.currentMetronomeTrack = self.tracks[serializedData["currentMetronomeTrackIndex"]] self._processAfterInit() return self diff --git a/engine/resources/lilypondTemplates/default.ly b/engine/resources/lilypondTemplates/default.ly index aded9c4..95c2d9b 100644 --- a/engine/resources/lilypondTemplates/default.ly +++ b/engine/resources/lilypondTemplates/default.ly @@ -95,7 +95,10 @@ tempoStaff = { %$$TEMPOSTAFF$$ } %$$VOICES$$ -\score{ +\score { + +%Transpose the entire score. Nothing happens when "c c" +%$$TRANSPOSITION$$ << %How the definitions are arranged. Only staffgroups (staff prefix) and staffs, merged as voices. @@ -103,6 +106,7 @@ tempoStaff = { %$$TEMPOSTAFF$$ } %$$STRUCTURE$$ >>} + \markup{ %\column { %$$SUBTEXT$$