better template structure for multi-work scores
[orchestrallily.git] / generate_oly_score.py
blob8d23ddfc1c86c4d4c7d07cc4f5bd1c0e5186ee3f
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 import sys
5 import os
6 import os.path
7 import getopt
8 import re
9 import filecmp
10 import string
11 import subprocess
12 import codecs
13 import fnmatch
14 from jinja2 import Environment, Template, FileSystemLoader
15 import jinja2
17 import pprint
18 pp = pprint.PrettyPrinter(indent=4)
20 program_name = 'generate_oly_score';
21 settings_file = 'oly_structure.def';
22 script_path = os.path.dirname(__file__);
24 ######################################################################
25 # Options handling
26 ######################################################################
28 help_text = r"""Usage: %(program_name)s [OPTIONS]... [DEF-FILE]
29 Create a complete file structure for a score using OrchestralLily.
30 If no definitions file it given, the file oly_structure.def is used
32 Options:
33 -h, --help print this help
34 -o, --output=DIR write output files to DIR (default: read from settings file)
35 """
37 def help (text):
38 sys.stdout.write (text)
39 sys.exit (0)
43 ######################################################################
44 # Settings
45 ######################################################################
47 class Settings:
48 options = {};
49 arguments = [];
50 globals={};
51 raw_data={};
52 out_dir = "";
53 template_env = None;
55 def __init__ (self):
56 settings_file = self.load_options ();
57 self.load (settings_file);
58 self.init_template_env ();
59 self.init_arrays ();
61 def output_dir (self):
62 return self.out_dir
63 def defaults (self):
64 return self.raw_data.get ("defaults", {});
65 def score_names (self):
66 return self.raw_data.get ("scores", ["Score"]);
68 def get_score_settings (self, id):
69 settings = self.globals.copy();
70 settings.update (self.defaults ());
71 settings.update ({"name": id})
72 settings.update (self.raw_data.get (id, {}));
73 self.normalize_part_definitions (settings);
74 return settings;
76 def normalize_part_definitions (self, score_settings):
77 scorename = score_settings.get ("name", "Score");
78 parts = score_settings.get ("parts", [scorename]);
79 result = [];
80 for this_part in parts:
81 if isinstance (this_part, basestring):
82 this_part = {"id": this_part, "filename": this_part }
83 elif not isinstance (this_part, dict):
84 warning ("Invalid part list for score %a: %a" % (scorename, p));
85 return [];
86 if "id" not in this_part:
87 this_part["id"] = scorename;
88 if "filename" not in this_part:
89 this_part["filename"] = this_part["id"];
90 if "piece" not in this_part:
91 this_part["piece"] = this_part["id"];
92 this_part["score"] = scorename;
93 this_part.update (score_settings);
94 result.append (this_part);
95 score_settings["parts"] = result;
97 def has_tex (self):
98 return "latex" in self.raw_data;
99 def get_tex_settings (self):
100 settings = self.globals.copy();
101 settings.update (self.defaults ());
102 settings.update (self.raw_data.get ("latex", {}));
103 return settings;
105 def get_score_parts (self, score_settings):
106 scorename = score_settings.get ("name", "Score");
107 parts = score_settings.get ("parts", [scorename]);
108 result = [];
109 for p in parts:
110 this_part = p;
111 if isinstance (this_part, basestring):
112 this_part = {"id": this_part, "filename": this_part }
113 elif not isinstance (this_part, dict):
114 warning ("Invalid part list for score %a: %a" % (scorename, p));
115 return [];
116 if "id" not in this_part:
117 this_part["id"] = scorename;
118 if "filename" not in this_part:
119 this_part["filename"] = this_part["id"];
120 if "piece" not in this_part:
121 this_part["piece"] = this_part["id"];
122 this_part["score"] = scorename;
123 this_part.update (score_settings);
124 result.append (this_part);
125 return result;
127 def load_options (self):
128 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
129 for opt in self.options:
130 o = opt[0]
131 a = opt[1]
132 if o == '-h' or o == '--help':
133 help (help_text % globals ())
134 elif o == '-o' or o == '--output':
135 self.out_dir = a
136 else:
137 raise Exception ('unknown option: ' + o)
138 if self.arguments:
139 return self.arguments[0]
140 else:
141 return "oly_structure.def";
143 def load (self, filename):
144 try:
145 in_f = codecs.open (filename, "r", "utf-8")
146 s = in_f.read()
147 in_f.close()
148 self.raw_data = eval(s);
149 except IOError:
150 print ("Unable to load settings file '%s'. Exiting..." % file_name)
151 exit (-1);
152 except SyntaxError as ex:
153 print ex;
154 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
155 exit (-1);
156 if not self.out_dir:
157 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
159 def get_template_name (self):
160 return self.raw_data.get ("template", "Full");
161 def get_template_names (self, pattern):
162 allfiles = os.listdir (self.templatepath);
163 files = [];
164 for f in allfiles:
165 if fnmatch.fnmatch(f, pattern):
166 files.append (f);
167 return files;
170 def init_template_env (self):
171 global program_name;
172 global script_path;
173 templatename = self.get_template_name ();
174 self.templatepath = script_path + '/Templates/' + templatename;
175 self.template_env = Environment (
176 loader = FileSystemLoader(self.templatepath),
177 block_start_string = '<$', block_end_string = '$>',
178 variable_start_string = '<<', variable_end_string = '>>',
179 comment_start_string = '<#', comment_end_string = '#>',
180 #line_statement_prefix = '#'
182 def get_template (self, template):
183 return self.template_env.get_template (template);
186 def init_arrays (self):
187 # Globals
188 self.globals = self.raw_data.copy ();
189 del self.globals["defaults"];
190 if "latex" in self.globals:
191 del self.globals["latex"];
192 for i in self.globals.get ("scores", []):
193 if i in self.globals:
194 del self.globals[i];
196 def assemble_filename (self, base, id, name, ext):
197 parts = [base, id, name];
198 if "" in parts:
199 parts.remove ("")
200 return "_".join (parts) + "." + ext;
201 def assemble_movement_filename (self, basename, mvmnt):
202 return self.assemble_filename( basename, "Music", mvmnt, "ily");
203 def assemble_instrument_filename (self, basename, instr):
204 return self.assemble_filename( basename, "Instrument", instr, "ly");
205 def assemble_score_filename (self, basename, instr):
206 return self.assemble_filename( basename, "Score", instr, "ly");
207 def assemble_settings_filename (self, basename, s):
208 return self.assemble_filename( basename, "Settings", s, "ily");
209 def assemble_itex_filename (self, basename, filename):
210 return self.assemble_filename( "TeX", basename, filename, "itex");
211 def assemble_texscore_filename (self, basename, score):
212 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
214 ######################################################################
215 # File Writing
216 ######################################################################
218 def write_file (path, fname, contents):
219 file_name = path + fname;
220 fn = file_name
221 if os.path.exists (file_name):
222 fn += ".new"
224 dir = os.path.dirname (fn);
225 if not os.path.exists (dir):
226 os.mkdir (dir)
228 try:
229 out_f = codecs.open (fn, "w", "utf-8")
230 s = out_f.write(contents)
231 out_f.write ("\n")
232 out_f.close()
233 except IOError:
234 print ("Unable to write to output file '%s'. Exiting..." % fn)
235 exit (-1);
237 # If the file already existed, check if the new file is identical.
238 if (fn != file_name):
239 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
240 if os.path.exists (patchfile):
241 try:
242 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
243 if retcode < 0:
244 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
245 except OSError, e:
246 print >>sys.stderr, "Execution failed:", e
248 if filecmp.cmp (fn, file_name):
249 os.unlink (fn);
250 else:
251 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
255 ######################################################################
256 # Creating movement files
257 ######################################################################
260 def generate_movement_files (score_name, score_settings, settings):
261 parts_files = [];
262 for part_settings in settings.get_score_parts (score_settings):
263 template = settings.get_template ("Lily_Music_Movement.ily");
264 basename = part_settings.get ("basename", score_name);
265 filename = part_settings.get ("filename", score_name + "Part");
266 filename = settings.assemble_movement_filename (basename, filename);
267 write_file (settings.out_dir, filename, template.render (part_settings));
268 parts_files.append (filename);
270 return parts_files;
274 ######################################################################
275 # Creating instrumental score files
276 ######################################################################
278 def generate_instrument_files (score_name, score_settings, settings):
279 instrument_files = [];
280 settings_files = set ();
281 instrument_settings = score_settings.copy ();
282 instrument_settings["parts"] = settings.get_score_parts (score_settings);
284 template = settings.get_template ("Lily_Instrument.ly");
285 basename = score_settings.get ("basename", score_name );
287 noscore_instruments = score_settings.get ("noscore_instruments", [])
288 for i in score_settings.get ("instruments", []):
289 print "Working on instrument %s" % i;
290 if i in noscore_instruments:
291 continue;
292 instrument_settings["instrument"] = i;
293 if i in score_settings.get ("vocalvoices"):
294 instrument_settings["settings"] = "VocalVoice";
295 else:
296 instrument_settings["settings"] = "Instrument";
297 settings_files.add (instrument_settings["settings"]);
298 filename = settings.assemble_instrument_filename (basename, i);
299 write_file (settings.out_dir, filename, template.render (instrument_settings));
300 instrument_files.append (filename);
302 return (instrument_files, settings_files);
306 ######################################################################
307 # Creating score files
308 ######################################################################
311 score_name_map = {
312 "Particell": "Particell",
314 settings_map = {
315 "OrganScore": "VocalScore",
316 "ChoralScore": "ChoralScore",
317 "LongScore": "FullScore",
318 "OriginalScore": "FullScore",
319 "Particell": "FullScore"
321 scores_cues = ["ChoralScore", "VocalScore"];
323 def generate_score_files (score_name, score_settings, settings):
324 score_files = [];
325 settings_files = set ();
326 s_settings = score_settings.copy ();
327 s_settings["parts"] = settings.get_score_parts (score_settings);
328 s_settings["fullscore"] = True;
330 template = settings.get_template ("Lily_Score.ly");
331 basename = score_settings.get ("basename", score_name );
333 for s in score_settings.get ("scores", []):
334 fullsn = score_name_map.get (s, (s + "Score"));
335 s_settings["score"] = fullsn;
336 s_settings["nocues"] = fullsn not in scores_cues;
337 s_settings["settings"] = settings_map.get (fullsn,fullsn)
338 settings_files.add (s_settings["settings"]);
339 filename = settings.assemble_score_filename (basename, s);
340 write_file (settings.out_dir, filename, template.render (s_settings));
341 score_files.append (filename);
343 return (score_files, settings_files);
347 ######################################################################
348 # Creating settings files
349 ######################################################################
351 def write_settings_file_if_exists (settings, score_settings, template, filename):
352 try:
353 template = settings.get_template (template);
354 basename = score_settings.get ("basename", "");
355 filename = settings.assemble_settings_filename (basename, filename);
356 write_file (settings.out_dir, filename, template.render (score_settings));
357 return filename;
358 except jinja2.exceptions.TemplateNotFound:
359 return None;
361 def generate_settings_files (score_name, score_settings, settings, settings_files):
362 out_files = [];
363 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
364 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
366 for s in settings_files:
367 score_settings["settings"] = s.lower ();
368 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
370 return out_files;
374 ######################################################################
375 # Generation of Lilypond Files
376 ######################################################################
378 def generate_scores (settings):
379 files = {}
380 for s in settings.score_names ():
381 score_settings = settings.get_score_settings (s);
382 parts_files = generate_movement_files (s, score_settings, settings);
383 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
384 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
385 included_settings_files = ssettings_files | isettings_files;
386 score_settings["parts_files"] = parts_files;
387 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
388 files[s] = {"settings": settings_files,
389 "scores": score_files,
390 "instruments": instrument_files,
391 "parts": parts_files };
392 return files
396 ######################################################################
397 # Creating LaTeX files
398 ######################################################################
400 def write_itex_file (settings, tex_settings, template, filename):
401 template = settings.get_template (template);
402 basename = tex_settings.get ("basename", "");
403 filename = settings.assemble_itex_filename (basename, filename);
404 write_file (settings.out_dir, filename, template.render (tex_settings));
405 return filename;
407 def write_texscore_file (settings, tex_settings, template, score):
408 template = settings.get_template (template);
409 basename = tex_settings.get ("basename", "");
410 filename = settings.assemble_texscore_filename (basename, score);
411 write_file (settings.out_dir, filename, template.render (tex_settings));
412 return filename;
414 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
416 def generate_tex_files (settings, lily_files):
417 tex_files = [];
418 tex_includes = [];
419 tex_settings = settings.get_tex_settings ();
420 tex_settings["lily_files"] = lily_files;
422 score_map = {};
423 instruments_scores = [];
424 for s in settings.score_names ():
425 score_settings = settings.get_score_settings (s);
426 instruments_scores.append (score_settings);
427 for p in score_settings.get ("scores", []):
428 score_map[p] = score_map.get (p, []) + [score_settings];
429 tex_settings["works"] = instruments_scores;
431 tex_include_templates = settings.get_template_names("TeX_*.itex");
432 for t in tex_include_templates:
433 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
434 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
436 for (score, parts) in score_map.items ():
437 this_settings = tex_settings.copy ();
438 this_settings["scoretype"] = score;
439 this_settings["scores"] = parts;
440 if "createCriticalComments" not in tex_settings:
441 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
442 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
444 this_settings = tex_settings.copy ();
445 this_settings["scoretype"] = "InstrumentalParts";
446 if "createCriticalComments" not in tex_settings:
447 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
448 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
450 return [tex_files, tex_includes];
454 ######################################################################
455 # Creating Makefile
456 ######################################################################
458 def generate_make_files (settings, lily_files, tex_files):
459 make_settings = settings.raw_data.copy ();
461 tex_settings = settings.get_tex_settings ();
462 tex_settings["includes"] = tex_files[1];
463 tex_settings["files"] = tex_files[0];
464 make_settings["latex"] = tex_settings;
466 score_map = {};
467 instruments_scores = [];
468 nr = 0;
469 for s in settings.score_names ():
470 nr += 1;
471 score_settings = settings.get_score_settings (s);
472 if nr > 1:
473 score_settings["nr"] = nr;
474 score_settings["srcfiles"] = lily_files[s];
475 instruments_scores.append (score_settings);
476 for p in score_settings.get ("scores", []):
477 score_map[p] = score_map.get (p, []) + [score_settings];
478 make_settings["works"] = instruments_scores;
480 template = settings.get_template ("Makefile");
481 basename = tex_settings.get ("basename", "");
482 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
483 return file;
487 # movements = settings.get ("parts", {});
489 # replacements["instruments"] = string.join (settings.get ("instruments", []));
490 # replacements["scores"] = string.join (settings.get ("scores", []))## + " Instruments";
491 # replacements["srcfiles"] = string.join (src_files);
493 # write_file (output_dir + "Makefile", templates["Makefile"] % replacements);
495 # del replacements["instruments"]
496 # del replacements["scores"]
497 # del replacements["srcfiles"]
498 # return;
501 ######################################################################
502 # Link the orchestrallily package
503 ######################################################################
505 def generate_oly_link (settings):
506 global script_path;
507 try:
508 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
509 except OSError:
510 pass
512 ######################################################################
513 # Main function
514 ######################################################################
516 def main ():
517 settings = Settings ();
518 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
519 (settings.out_dir, settings.get_template_name () ));
521 print ("Creating Lilypond files")
522 lily_files = generate_scores (settings);
524 if settings.has_tex ():
525 print ("Creating LaTeX files")
526 tex_files = generate_tex_files (settings, lily_files);
527 print ("Creating OrchestralLily package link")
528 generate_oly_link (settings);
529 print ("Creating Makefile")
530 generate_make_files (settings, lily_files, tex_files);
532 main ();