#! /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 ( ' " ' , ' \\ " ' )
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 ( " % $$FILENAME$$ " , session . sessionPrefix )
templateString = templateString . replace ( " % $$HEADER$$ " , processMeta ( meta ) )
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 \b ar " |. " } """
return " {} = {} " . format ( stringToCharsOnlyString ( track . name ) , " { " + lilypondTrack + " } " )
def structure ( track , lilypondTrack ) :
""" \n ew Staff = " tgliHJuGCFAIICICBEHBJABHIJE_Staff " << \\ new Voice = " tgliHJuGCFAIICICBEHBJABHIJE " \t gliHJuGCFAIICICBEHBJABHIJE >> %E ndTrack """
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 = " {} " \ {} >> %E ndTrack """ . 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