8edbf2a6e8cc04a7808ce57e6e3164526aeb9c07
[orchestrallily.git] / generate_oly_score.py
blob8edbf2a6e8cc04a7808ce57e6e3164526aeb9c07
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.raw_data["settings_file_path"] = settings_file;
60 self.raw_data["settings_file"] = os.path.basename(settings_file);
61 self.init_template_env ();
62 self.init_arrays ();
64 def output_dir (self):
65 return self.out_dir
66 def defaults (self):
67 return self.raw_data.get ("defaults", {});
68 def score_names (self):
69 return self.raw_data.get ("scores", ["Score"]);
71 def get_score_settings (self, id):
72 settings = self.globals.copy();
73 settings.update (self.defaults ());
74 settings.update ({"name": id})
75 settings.update (self.raw_data.get (id, {}));
76 self.normalize_part_definitions (settings);
77 return settings;
79 def normalize_part_definitions (self, score_settings):
80 scorename = score_settings.get ("name", "Score");
81 parts = score_settings.get ("parts", [scorename]);
82 result = [];
83 for this_part in parts:
84 if isinstance (this_part, basestring):
85 this_part = {"id": this_part, "filename": this_part }
86 elif not isinstance (this_part, dict):
87 warning ("Invalid part list for score %a: %a" % (scorename, p));
88 return [];
89 if "id" not in this_part:
90 this_part["id"] = scorename;
91 if "filename" not in this_part:
92 this_part["filename"] = this_part["id"];
93 if "piece" not in this_part:
94 this_part["piece"] = this_part["id"];
95 this_part["score"] = scorename;
96 this_part.update (score_settings);
97 result.append (this_part);
98 score_settings["parts"] = result;
100 def has_tex (self):
101 return "latex" in self.raw_data;
102 def get_tex_settings (self):
103 settings = self.globals.copy();
104 settings.update (self.defaults ());
105 settings.update (self.raw_data.get ("latex", {}));
106 return settings;
108 def get_score_parts (self, score_settings):
109 scorename = score_settings.get ("name", "Score");
110 parts = score_settings.get ("parts", [scorename]);
111 result = [];
112 for p in parts:
113 this_part = p;
114 if isinstance (this_part, basestring):
115 this_part = {"id": this_part, "filename": this_part }
116 elif not isinstance (this_part, dict):
117 warning ("Invalid part list for score %a: %a" % (scorename, p));
118 return [];
119 if "id" not in this_part:
120 this_part["id"] = scorename;
121 if "filename" not in this_part:
122 this_part["filename"] = this_part["id"];
123 if "piece" not in this_part:
124 this_part["piece"] = this_part["id"];
125 this_part["score"] = scorename;
126 this_part.update (score_settings);
127 result.append (this_part);
128 return result;
130 def load_options (self):
131 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
132 for opt in self.options:
133 o = opt[0]
134 a = opt[1]
135 if o == '-h' or o == '--help':
136 help (help_text % globals ())
137 elif o == '-o' or o == '--output':
138 self.out_dir = a
139 else:
140 raise Exception ('unknown option: ' + o)
141 if self.arguments:
142 return self.arguments[0]
143 else:
144 return "oly_structure.def";
146 def load (self, filename):
147 try:
148 in_f = codecs.open (filename, "r", "utf-8")
149 s = in_f.read()
150 in_f.close()
151 self.raw_data = eval(s);
152 except IOError:
153 print ("Unable to load settings file '%s'. Exiting..." % file_name)
154 exit (-1);
155 except SyntaxError as ex:
156 print ex;
157 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
158 exit (-1);
159 if not self.out_dir:
160 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
162 def get_template_name (self):
163 return self.raw_data.get ("template", "Full");
164 def get_template_names (self, pattern):
165 allfiles = os.listdir (self.templatepath);
166 files = [];
167 for f in allfiles:
168 if fnmatch.fnmatch(f, pattern):
169 files.append (f);
170 return files;
173 def init_template_env (self):
174 global program_name;
175 global script_path;
176 templatename = self.get_template_name ();
177 self.templatepath = script_path + '/Templates/' + templatename;
178 self.template_env = Environment (
179 loader = FileSystemLoader(self.templatepath),
180 block_start_string = '<$', block_end_string = '$>',
181 variable_start_string = '<<', variable_end_string = '>>',
182 comment_start_string = '<#', comment_end_string = '#>',
183 #line_statement_prefix = '#'
185 def get_template (self, template):
186 return self.template_env.get_template (template);
189 def init_arrays (self):
190 # Globals
191 self.globals = self.raw_data.copy ();
192 del self.globals["defaults"];
193 if "latex" in self.globals:
194 del self.globals["latex"];
195 for i in self.globals.get ("scores", []):
196 if i in self.globals:
197 del self.globals[i];
199 def assemble_filename (self, base, id, name, ext):
200 parts = [base, id, name];
201 if "" in parts:
202 parts.remove ("")
203 return "_".join (parts) + "." + ext;
204 def assemble_movement_filename (self, basename, mvmnt):
205 return self.assemble_filename( basename, "Music", mvmnt, "ily");
206 def assemble_instrument_filename (self, basename, instr):
207 return self.assemble_filename( basename, "Instrument", instr, "ly");
208 def assemble_score_filename (self, basename, instr):
209 return self.assemble_filename( basename, "Score", instr, "ly");
210 def assemble_settings_filename (self, basename, s):
211 return self.assemble_filename( basename, "Settings", s, "ily");
212 def assemble_itex_filename (self, basename, filename):
213 return self.assemble_filename( "TeX", basename, filename, "itex");
214 def assemble_texscore_filename (self, basename, score):
215 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
217 ######################################################################
218 # File Writing
219 ######################################################################
221 def write_file (path, fname, contents):
222 file_name = path + fname;
223 fn = file_name
224 if os.path.exists (file_name):
225 fn += ".new"
227 dir = os.path.dirname (fn);
228 if not os.path.exists (dir):
229 os.mkdir (dir)
231 try:
232 out_f = codecs.open (fn, "w", "utf-8")
233 s = out_f.write(contents)
234 out_f.write ("\n")
235 out_f.close()
236 except IOError:
237 print ("Unable to write to output file '%s'. Exiting..." % fn)
238 exit (-1);
240 # If the file already existed, check if the new file is identical.
241 if (fn != file_name):
242 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
243 if os.path.exists (patchfile):
244 try:
245 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
246 if retcode < 0:
247 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
248 except OSError, e:
249 print >>sys.stderr, "Execution failed:", e
251 if filecmp.cmp (fn, file_name):
252 os.unlink (fn);
253 else:
254 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
258 ######################################################################
259 # Creating movement files
260 ######################################################################
263 def generate_movement_files (score_name, score_settings, settings):
264 parts_files = [];
265 for part_settings in settings.get_score_parts (score_settings):
266 template = settings.get_template ("Lily_Music_Movement.ily");
267 basename = part_settings.get ("basename", score_name);
268 filename = part_settings.get ("filename", score_name + "Part");
269 filename = settings.assemble_movement_filename (basename, filename);
270 write_file (settings.out_dir, filename, template.render (part_settings));
271 parts_files.append (filename);
273 return parts_files;
277 ######################################################################
278 # Creating instrumental score files
279 ######################################################################
281 def generate_instrument_files (score_name, score_settings, settings):
282 instrument_files = [];
283 settings_files = set ();
284 instrument_settings = score_settings.copy ();
285 instrument_settings["parts"] = settings.get_score_parts (score_settings);
287 template = settings.get_template ("Lily_Instrument.ly");
288 basename = score_settings.get ("basename", score_name );
290 noscore_instruments = score_settings.get ("noscore_instruments", [])
291 for i in score_settings.get ("instruments", []):
292 if i in noscore_instruments:
293 continue;
294 instrument_settings["instrument"] = i;
295 if i in score_settings.get ("vocalvoices"):
296 instrument_settings["settings"] = "VocalVoice";
297 else:
298 instrument_settings["settings"] = "Instrument";
299 settings_files.add (instrument_settings["settings"]);
300 filename = settings.assemble_instrument_filename (basename, i);
301 write_file (settings.out_dir, filename, template.render (instrument_settings));
302 instrument_files.append (filename);
304 return (instrument_files, settings_files);
308 ######################################################################
309 # Creating score files
310 ######################################################################
313 score_name_map = {
314 "Particell": "Particell",
316 settings_map = {
317 "OrganScore": "VocalScore",
318 "ChoralScore": "ChoralScore",
319 "LongScore": "FullScore",
320 "OriginalScore": "FullScore",
321 "Particell": "FullScore"
323 scores_cues = ["ChoralScore", "VocalScore"];
325 def generate_score_files (score_name, score_settings, settings):
326 score_files = [];
327 settings_files = set ();
328 s_settings = score_settings.copy ();
329 s_settings["parts"] = settings.get_score_parts (score_settings);
330 s_settings["fullscore"] = True;
332 template = settings.get_template ("Lily_Score.ly");
333 basename = score_settings.get ("basename", score_name );
335 for s in score_settings.get ("scores", []):
336 fullsn = score_name_map.get (s, (s + "Score"));
337 s_settings["score"] = fullsn;
338 s_settings["nocues"] = fullsn not in scores_cues;
339 s_settings["settings"] = settings_map.get (fullsn,fullsn)
340 settings_files.add (s_settings["settings"]);
341 filename = settings.assemble_score_filename (basename, s);
342 write_file (settings.out_dir, filename, template.render (s_settings));
343 score_files.append (filename);
345 return (score_files, settings_files);
349 ######################################################################
350 # Creating settings files
351 ######################################################################
353 def write_settings_file_if_exists (settings, score_settings, template, filename):
354 try:
355 template = settings.get_template (template);
356 basename = score_settings.get ("basename", "");
357 filename = settings.assemble_settings_filename (basename, filename);
358 write_file (settings.out_dir, filename, template.render (score_settings));
359 return filename;
360 except jinja2.exceptions.TemplateNotFound:
361 return None;
363 def generate_settings_files (score_name, score_settings, settings, settings_files):
364 out_files = [];
365 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
366 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
368 for s in settings_files:
369 score_settings["settings"] = s.lower ();
370 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
372 return out_files;
376 ######################################################################
377 # Generation of Lilypond Files
378 ######################################################################
380 def generate_scores (settings):
381 files = {}
382 for s in settings.score_names ():
383 score_settings = settings.get_score_settings (s);
384 parts_files = generate_movement_files (s, score_settings, settings);
385 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
386 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
387 included_settings_files = ssettings_files | isettings_files;
388 score_settings["parts_files"] = parts_files;
389 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
390 files[s] = {"settings": settings_files,
391 "scores": score_files,
392 "instruments": instrument_files,
393 "parts": parts_files };
394 return files
398 ######################################################################
399 # Creating LaTeX files
400 ######################################################################
402 def write_itex_file (settings, tex_settings, template, filename):
403 template = settings.get_template (template);
404 basename = tex_settings.get ("basename", "");
405 filename = settings.assemble_itex_filename (basename, filename);
406 write_file (settings.out_dir, filename, template.render (tex_settings));
407 return filename;
409 def write_texscore_file (settings, tex_settings, template, score):
410 template = settings.get_template (template);
411 basename = tex_settings.get ("basename", "");
412 filename = settings.assemble_texscore_filename (basename, score);
413 write_file (settings.out_dir, filename, template.render (tex_settings));
414 return filename;
416 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
417 tex_options_map = {
418 "Organ": "vocalscore",
419 "Choral": "choralscore",
420 "Vocal": "vocalscore",
421 "Long": "fullscore",
422 "Full": "fullscore",
423 "Original": "fullscore",
424 "Particell": "vocalscore",
425 "InstrumentalParts": "instrumentalparts",
426 # TODO: chambermusic
429 def generate_tex_files (settings, lily_files):
430 tex_files = [];
431 tex_includes = [];
432 tex_settings = settings.get_tex_settings ();
433 tex_settings["lily_files"] = lily_files;
435 score_map = {};
436 instruments_scores = [];
437 for s in settings.score_names ():
438 score_settings = settings.get_score_settings (s);
439 instruments_scores.append (score_settings);
440 for p in score_settings.get ("scores", []):
441 score_map[p] = score_map.get (p, []) + [score_settings];
442 for i in tex_settings.get ("instruments", []):
443 if i in tex_settings.get ("instrumentalscores", []):
444 score_map[i] = score_map.get (i, []) + [score_settings];
445 tex_settings["works"] = instruments_scores;
447 tex_include_templates = settings.get_template_names("TeX_*.itex");
448 for t in tex_include_templates:
449 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
450 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
452 for (score, parts) in score_map.items ():
453 this_settings = copy.deepcopy (tex_settings);
454 this_settings["scoretype"] = score;
455 this_settings["scores"] = parts;
456 #this_settings["scorebasetype"] = "Score";
457 #this_settings["scorenamebase"] = "ScoreType";
458 tmpopts = this_settings.get ("tex_options", [])
459 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
460 this_settings["tex_options"] = tmpopts
461 if "createCriticalComments" not in tex_settings:
462 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
463 this_settings["is_instrument"] = score in tex_settings.get ("instruments", []);
464 #this_settings["scorebasetype"] = "Instrument";
465 #this_settings["scorenamebase"] = "Name";
466 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
468 this_settings = copy.deepcopy (tex_settings);
469 this_settings["scoretype"] = "InstrumentalParts";
470 tmpopts = this_settings.get ("tex_options", [])
471 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
472 this_settings["tex_options"] = tmpopts
473 if "createCriticalComments" not in tex_settings:
474 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
475 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
477 return [tex_files, tex_includes];
481 ######################################################################
482 # Creating Makefile
483 ######################################################################
485 def generate_make_files (settings, lily_files, tex_files):
486 make_settings = settings.raw_data.copy ();
488 if settings.has_tex ():
489 tex_settings = settings.get_tex_settings ();
490 tex_settings["includes"] = tex_files[1];
491 tex_settings["files"] = tex_files[0];
492 make_settings["latex"] = tex_settings;
494 score_map = {};
495 instruments_scores = [];
496 nr = 0;
497 for s in settings.score_names ():
498 nr += 1;
499 score_settings = settings.get_score_settings (s);
500 if nr > 1:
501 score_settings["nr"] = nr;
502 else:
503 score_settings["nr"] = "";
504 score_settings["srcfiles"] = lily_files[s];
505 instruments_scores.append (score_settings);
506 for p in score_settings.get ("scores", []):
507 score_map[p] = score_map.get (p, []) + [score_settings];
508 make_settings["works"] = instruments_scores;
510 template = settings.get_template ("Makefile");
511 #basename = tex_settings.get ("basename", "");
512 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
513 return file;
517 # movements = settings.get ("parts", {});
519 # replacements["instruments"] = string.join (settings.get ("instruments", []));
520 # replacements["scores"] = string.join (settings.get ("scores", []))## + " Instruments";
521 # replacements["srcfiles"] = string.join (src_files);
523 # write_file (output_dir + "Makefile", templates["Makefile"] % replacements);
525 # del replacements["instruments"]
526 # del replacements["scores"]
527 # del replacements["srcfiles"]
528 # return;
531 ######################################################################
532 # Link the orchestrallily package
533 ######################################################################
535 def generate_oly_link (settings):
536 global script_path;
537 try:
538 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
539 except OSError:
540 pass
542 ######################################################################
543 # Main function
544 ######################################################################
546 def main ():
547 settings = Settings ();
548 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
549 (settings.out_dir, settings.get_template_name () ));
551 print ("Creating Lilypond files")
552 lily_files = generate_scores (settings);
554 tex_files = []
555 if settings.has_tex ():
556 print ("Creating LaTeX files")
557 tex_files = generate_tex_files (settings, lily_files);
558 print ("Creating OrchestralLily package link")
559 generate_oly_link (settings);
560 print ("Creating Makefile")
561 generate_make_files (settings, lily_files, tex_files);
563 main ();