Small updates, new commands, etc.
[orchestrallily.git] / generate_oly_score.py
blob851bf07f3b2a42f52eb72ef7c9bc355cf575e965
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 copy
13 import codecs
14 import fnmatch
15 from jinja2 import Environment, Template, FileSystemLoader
16 import jinja2
18 import pprint
19 pp = pprint.PrettyPrinter(indent=4)
21 program_name = 'generate_oly_score';
22 settings_file = 'oly_structure.def';
23 script_path = os.path.dirname(__file__);
25 ######################################################################
26 # Options handling
27 ######################################################################
29 help_text = r"""Usage: %(program_name)s [OPTIONS]... [DEF-FILE]
30 Create a complete file structure for a score using OrchestralLily.
31 If no definitions file it given, the file oly_structure.def is used
33 Options:
34 -h, --help print this help
35 -o, --output=DIR write output files to DIR (default: read from settings file)
36 """
38 def help (text):
39 sys.stdout.write (text)
40 sys.exit (0)
44 ######################################################################
45 # Settings
46 ######################################################################
48 class Settings:
49 options = {};
50 arguments = [];
51 globals={};
52 raw_data={};
53 out_dir = "";
54 template_env = None;
56 def __init__ (self):
57 settings_file = self.load_options ();
58 self.load (settings_file);
59 self.init_template_env ();
60 self.init_arrays ();
62 def output_dir (self):
63 return self.out_dir
64 def defaults (self):
65 return self.raw_data.get ("defaults", {});
66 def score_names (self):
67 return self.raw_data.get ("scores", ["Score"]);
69 def get_score_settings (self, id):
70 settings = self.globals.copy();
71 settings.update (self.defaults ());
72 settings.update ({"name": id})
73 settings.update (self.raw_data.get (id, {}));
74 self.normalize_part_definitions (settings);
75 return settings;
77 def normalize_part_definitions (self, score_settings):
78 scorename = score_settings.get ("name", "Score");
79 parts = score_settings.get ("parts", [scorename]);
80 result = [];
81 for this_part in parts:
82 if isinstance (this_part, basestring):
83 this_part = {"id": this_part, "filename": this_part }
84 elif not isinstance (this_part, dict):
85 warning ("Invalid part list for score %a: %a" % (scorename, p));
86 return [];
87 if "id" not in this_part:
88 this_part["id"] = scorename;
89 if "filename" not in this_part:
90 this_part["filename"] = this_part["id"];
91 if "piece" not in this_part:
92 this_part["piece"] = this_part["id"];
93 this_part["score"] = scorename;
94 this_part.update (score_settings);
95 result.append (this_part);
96 score_settings["parts"] = result;
98 def has_tex (self):
99 return "latex" in self.raw_data;
100 def get_tex_settings (self):
101 settings = self.globals.copy();
102 settings.update (self.defaults ());
103 settings.update (self.raw_data.get ("latex", {}));
104 return settings;
106 def get_score_parts (self, score_settings):
107 scorename = score_settings.get ("name", "Score");
108 parts = score_settings.get ("parts", [scorename]);
109 result = [];
110 for p in parts:
111 this_part = p;
112 if isinstance (this_part, basestring):
113 this_part = {"id": this_part, "filename": this_part }
114 elif not isinstance (this_part, dict):
115 warning ("Invalid part list for score %a: %a" % (scorename, p));
116 return [];
117 if "id" not in this_part:
118 this_part["id"] = scorename;
119 if "filename" not in this_part:
120 this_part["filename"] = this_part["id"];
121 if "piece" not in this_part:
122 this_part["piece"] = this_part["id"];
123 this_part["score"] = scorename;
124 this_part.update (score_settings);
125 result.append (this_part);
126 return result;
128 def load_options (self):
129 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
130 for opt in self.options:
131 o = opt[0]
132 a = opt[1]
133 if o == '-h' or o == '--help':
134 help (help_text % globals ())
135 elif o == '-o' or o == '--output':
136 self.out_dir = a
137 else:
138 raise Exception ('unknown option: ' + o)
139 if self.arguments:
140 return self.arguments[0]
141 else:
142 return "oly_structure.def";
144 def load (self, filename):
145 try:
146 in_f = codecs.open (filename, "r", "utf-8")
147 s = in_f.read()
148 in_f.close()
149 self.raw_data = eval(s);
150 except IOError:
151 print ("Unable to load settings file '%s'. Exiting..." % file_name)
152 exit (-1);
153 except SyntaxError as ex:
154 print ex;
155 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
156 exit (-1);
157 if not self.out_dir:
158 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
160 def get_template_name (self):
161 return self.raw_data.get ("template", "Full");
162 def get_template_names (self, pattern):
163 allfiles = os.listdir (self.templatepath);
164 files = [];
165 for f in allfiles:
166 if fnmatch.fnmatch(f, pattern):
167 files.append (f);
168 return files;
171 def init_template_env (self):
172 global program_name;
173 global script_path;
174 templatename = self.get_template_name ();
175 self.templatepath = script_path + '/Templates/' + templatename;
176 self.template_env = Environment (
177 loader = FileSystemLoader(self.templatepath),
178 block_start_string = '<$', block_end_string = '$>',
179 variable_start_string = '<<', variable_end_string = '>>',
180 comment_start_string = '<#', comment_end_string = '#>',
181 #line_statement_prefix = '#'
183 def get_template (self, template):
184 return self.template_env.get_template (template);
187 def init_arrays (self):
188 # Globals
189 self.globals = self.raw_data.copy ();
190 del self.globals["defaults"];
191 if "latex" in self.globals:
192 del self.globals["latex"];
193 for i in self.globals.get ("scores", []):
194 if i in self.globals:
195 del self.globals[i];
197 def assemble_filename (self, base, id, name, ext):
198 parts = [base, id, name];
199 if "" in parts:
200 parts.remove ("")
201 return "_".join (parts) + "." + ext;
202 def assemble_movement_filename (self, basename, mvmnt):
203 return self.assemble_filename( basename, "Music", mvmnt, "ily");
204 def assemble_instrument_filename (self, basename, instr):
205 return self.assemble_filename( basename, "Instrument", instr, "ly");
206 def assemble_score_filename (self, basename, instr):
207 return self.assemble_filename( basename, "Score", instr, "ly");
208 def assemble_settings_filename (self, basename, s):
209 return self.assemble_filename( basename, "Settings", s, "ily");
210 def assemble_itex_filename (self, basename, filename):
211 return self.assemble_filename( "TeX", basename, filename, "itex");
212 def assemble_texscore_filename (self, basename, score):
213 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
215 ######################################################################
216 # File Writing
217 ######################################################################
219 def write_file (path, fname, contents):
220 file_name = path + fname;
221 fn = file_name
222 if os.path.exists (file_name):
223 fn += ".new"
225 dir = os.path.dirname (fn);
226 if not os.path.exists (dir):
227 os.mkdir (dir)
229 try:
230 out_f = codecs.open (fn, "w", "utf-8")
231 s = out_f.write(contents)
232 out_f.write ("\n")
233 out_f.close()
234 except IOError:
235 print ("Unable to write to output file '%s'. Exiting..." % fn)
236 exit (-1);
238 # If the file already existed, check if the new file is identical.
239 if (fn != file_name):
240 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
241 if os.path.exists (patchfile):
242 try:
243 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
244 if retcode < 0:
245 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
246 except OSError, e:
247 print >>sys.stderr, "Execution failed:", e
249 if filecmp.cmp (fn, file_name):
250 os.unlink (fn);
251 else:
252 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
256 ######################################################################
257 # Creating movement files
258 ######################################################################
261 def generate_movement_files (score_name, score_settings, settings):
262 parts_files = [];
263 for part_settings in settings.get_score_parts (score_settings):
264 template = settings.get_template ("Lily_Music_Movement.ily");
265 basename = part_settings.get ("basename", score_name);
266 filename = part_settings.get ("filename", score_name + "Part");
267 filename = settings.assemble_movement_filename (basename, filename);
268 write_file (settings.out_dir, filename, template.render (part_settings));
269 parts_files.append (filename);
271 return parts_files;
275 ######################################################################
276 # Creating instrumental score files
277 ######################################################################
279 def generate_instrument_files (score_name, score_settings, settings):
280 instrument_files = [];
281 settings_files = set ();
282 instrument_settings = score_settings.copy ();
283 instrument_settings["parts"] = settings.get_score_parts (score_settings);
285 template = settings.get_template ("Lily_Instrument.ly");
286 basename = score_settings.get ("basename", score_name );
288 noscore_instruments = score_settings.get ("noscore_instruments", [])
289 for i in score_settings.get ("instruments", []):
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"];
415 tex_options_map = {
416 "Organ": "vocalscore",
417 "Choral": "choralscore",
418 "Vocal": "vocalscore",
419 "Long": "fullscore",
420 "Full": "fullscore",
421 "Original": "fullscore",
422 "Particell": "vocalscore",
423 "InstrumentalParts": "instrumentalparts",
424 # TODO: chambermusic
427 def generate_tex_files (settings, lily_files):
428 tex_files = [];
429 tex_includes = [];
430 tex_settings = settings.get_tex_settings ();
431 tex_settings["lily_files"] = lily_files;
433 score_map = {};
434 instruments_scores = [];
435 for s in settings.score_names ():
436 score_settings = settings.get_score_settings (s);
437 instruments_scores.append (score_settings);
438 for p in score_settings.get ("scores", []):
439 score_map[p] = score_map.get (p, []) + [score_settings];
440 tex_settings["works"] = instruments_scores;
442 tex_include_templates = settings.get_template_names("TeX_*.itex");
443 for t in tex_include_templates:
444 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
445 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
447 for (score, parts) in score_map.items ():
448 this_settings = copy.deepcopy (tex_settings);
449 this_settings["scoretype"] = score;
450 this_settings["scores"] = parts;
451 tmpopts = this_settings.get ("tex_options", [])
452 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
453 this_settings["tex_options"] = tmpopts
454 if "createCriticalComments" not in tex_settings:
455 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
456 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
458 this_settings = copy.deepcopy (tex_settings);
459 this_settings["scoretype"] = "InstrumentalParts";
460 tmpopts = this_settings.get ("tex_options", [])
461 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
462 this_settings["tex_options"] = tmpopts
463 if "createCriticalComments" not in tex_settings:
464 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
465 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
467 return [tex_files, tex_includes];
471 ######################################################################
472 # Creating Makefile
473 ######################################################################
475 def generate_make_files (settings, lily_files, tex_files):
476 make_settings = settings.raw_data.copy ();
478 if settings.has_tex ():
479 tex_settings = settings.get_tex_settings ();
480 tex_settings["includes"] = tex_files[1];
481 tex_settings["files"] = tex_files[0];
482 make_settings["latex"] = tex_settings;
484 score_map = {};
485 instruments_scores = [];
486 nr = 0;
487 for s in settings.score_names ():
488 nr += 1;
489 score_settings = settings.get_score_settings (s);
490 if nr > 1:
491 score_settings["nr"] = nr;
492 else:
493 score_settings["nr"] = "";
494 score_settings["srcfiles"] = lily_files[s];
495 instruments_scores.append (score_settings);
496 for p in score_settings.get ("scores", []):
497 score_map[p] = score_map.get (p, []) + [score_settings];
498 make_settings["works"] = instruments_scores;
500 template = settings.get_template ("Makefile");
501 #basename = tex_settings.get ("basename", "");
502 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
503 return file;
507 # movements = settings.get ("parts", {});
509 # replacements["instruments"] = string.join (settings.get ("instruments", []));
510 # replacements["scores"] = string.join (settings.get ("scores", []))## + " Instruments";
511 # replacements["srcfiles"] = string.join (src_files);
513 # write_file (output_dir + "Makefile", templates["Makefile"] % replacements);
515 # del replacements["instruments"]
516 # del replacements["scores"]
517 # del replacements["srcfiles"]
518 # return;
521 ######################################################################
522 # Link the orchestrallily package
523 ######################################################################
525 def generate_oly_link (settings):
526 global script_path;
527 try:
528 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
529 except OSError:
530 pass
532 ######################################################################
533 # Main function
534 ######################################################################
536 def main ():
537 settings = Settings ();
538 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
539 (settings.out_dir, settings.get_template_name () ));
541 print ("Creating Lilypond files")
542 lily_files = generate_scores (settings);
544 tex_files = []
545 if settings.has_tex ():
546 print ("Creating LaTeX files")
547 tex_files = generate_tex_files (settings, lily_files);
548 print ("Creating OrchestralLily package link")
549 generate_oly_link (settings);
550 print ("Creating Makefile")
551 generate_make_files (settings, lily_files, tex_files);
553 main ();