cobol
[lilypond.git] / scripts / musedata2ly.py
blob99d74fb764298e516d08a4bac2a29d3dd6b9b800
1 #!@PYTHON@
3 # musedata = musedata.stanford.edu
4 # musedata = COBOL for musicians.
7 # TODO
9 # * clefs,
10 # * keys,
11 # * staffs,
12 # * multiple voices (they use `Backspace' (shudder)
13 # * tuplets
17 # I completely forgot how this was supposed to work --hwn 5/2002
21 import re
22 import sys
23 import string
24 import getopt
25 import os
26 program_name = 'musedata2ly'
27 version = '@TOPLEVEL_VERSION@'
28 if version == '@' + 'TOPLEVEL_VERSION' + '@':
29 version = '(unknown version)' # uGUHGUHGHGUGH
33 ref_header_dict = {
34 'COM': 'composer',
35 'OPR': 'collection',
36 'OTL': 'title',
37 'OMV': 'subtitle',
38 'YOR': 'source',
39 'AGN': 'instrument',
40 'END': 'encodingdate',
41 'CDT': 'date',
42 'OCY': 'composedin',
43 'AST': 'genre',
44 'YEC': 'copyright',
45 'YEM': 'license',
46 'YEN': 'encodingcountry',
47 'EED': 'editor',
48 'SCA': 'opus',
49 'ONM': 'onm',
50 'ENC': 'musedataencoder',
51 'KEY': 'musedatakey',
52 'AFT': 'musedatastage'
56 class Ref_parser:
57 def __init__ (self, fn):
58 self.dict = {}
60 ls = open (fn).readlines ()
61 self.parse (ls)
62 def parse (self,ls):
63 for l in ls:
64 m = re.match('!!!([A-Z]+):[ \t]+(.*)$',l)
65 if m:
66 key = m.group(1)
67 val = m.group (2)
68 val = re.sub ('[ \t]+', ' ', val)
69 try:
71 key =ref_header_dict [key]
72 except KeyError:
73 sys.stderr.write ('\nUnknown ref key \`%s\'' % key)
74 s = ''
75 try:
76 s = self.dict[key]
77 except KeyError:
78 pass
80 s = s + val
81 self.dict[key] = s
82 def dump( self):
83 str = ''
84 for (k,v) in self.dict.items ():
85 str = str +' %s = "%s"\n' % (k,v)
86 str = '\\header {\n%s}' % str
87 return str
89 verbose = 0
92 actab = {-2: 'eses', -1: 'es', 0 : '', 1: 'is', 2:'isis'}
94 def pitch_to_lily_string (tup):
95 (o,n,a) = tup
97 nm = chr((n + 2) % 7 + ord ('a'))
98 nm = nm + actab[a]
99 if o > 0:
100 nm = nm + "'" * o
101 elif o < 0:
102 nm = nm + "," * -o
103 return nm
105 def get_key (s):
106 i = string.atoi (s)
107 return ''
109 def get_timesig (s):
110 return '\\time %s\n' % s
113 divisions = 4
114 def get_divisions_per_quarter (s):
115 divisions = string.atoi (s)
116 return ''
118 def get_directive (s):
119 return '%% %s\n' % s
121 def get_transposing (s):
122 return ''
124 def get_num_instruments (s):
125 return ''
127 def get_lilypond_notename (p, ac):
128 if p > 5:
129 p = p - 7
130 s = chr (p + ord ('c'))
131 infix = 'i'
132 if ac < 0:
133 infix = 'e'
134 ac = -ac
136 while ac:
137 s = s + infix + 's'
138 ac = ac - 1
139 return s
140 def get_clef ():
141 return ''
143 SPACES = ' '
144 DIGITS = "0123456789"
147 clef_dict = {
148 04: 'treble',
149 13 : 'alto',
150 22: 'bass',
152 attr_dict = {
153 'C' : get_clef,
154 'K' : get_key ,
155 'T' : get_timesig,
156 'Q' : get_divisions_per_quarter,
157 'D' : get_directive,
158 'X' : get_transposing,
159 'I': get_num_instruments,
162 class Attribute_set:
163 def __init__ (self, dict):
164 self.dict = dict
165 def dump (self):
166 s = ''
167 if self. dict.has_key ('T'):
168 s = s+ get_timesig (self.dict['T'])
170 return s
173 script_table = {
174 'v': '\\upbow',
175 'n': '\\downbow',
176 'o': '\\harmonic',
177 '0': '"openstring',
178 'Q': '\\thumb',
179 '>': '^',
180 'V': '^',
181 '.': '.',
182 '_': '-',
183 '=': '"det leg"',
184 'i': '|',
185 's': '"\\\\textsharp"',
186 'n': '"\\\\textnatural"',
187 'b': '"\\\\textflat"',
188 'F': '\\fermata',
189 'E': '\\fermata',
193 class Chord:
194 def __init__ (self):
195 self.pitches = []
196 self.grace = 0
197 self.cue = 0
198 self.slurstart = []
199 self.slurstop = []
200 self.scripts = []
201 self.syllables = []
202 self.dots = 0
203 self.basic_duration = 4
204 self.tied = 0
206 self.note_suffix = self.note_prefix = ''
207 self.chord_suffix = self.chord_prefix = ''
209 def add_script (self,s):
210 self.scripts.append (s)
211 def set_duration (self, d):
212 self.basic_duration = d
213 def add_syllable (self, s):
214 self.syllables.append (s)
215 def add_pitch (self,t):
216 self.pitches.append (t)
218 def dump (self):
219 str = ''
221 sd = ''
222 if self.basic_duration == 0.5:
223 sd = '\\breve'
224 else:
225 sd = '%d' % self.basic_duration
227 sd = sd + '.' * self.dots
229 str = ')' * len (self.slurstart) + str
231 for p in self.pitches:
232 if str:
233 str = str + ' '
234 str = str + pitch_to_lily_string (p) + sd
235 str = str + '(' * len (self.slurstart)
238 for s in self.scripts:
239 str = str + '-' + s
241 str = self.note_prefix +str + self.note_suffix
243 if len (self.pitches) > 1:
244 str = '<%s>' % str
245 elif len (self.pitches) == 0:
246 str = 'r' + sd
248 str = self.chord_prefix + str + self.chord_suffix
250 return str
252 class Measure_start:
253 def dump (self):
254 return ' |\n'
256 class Parser:
257 def append_entry (self, e):
258 self.entries.append (e)
259 def append_chord (self,c ):
260 self.chords.append (c)
261 self.entries.append (c)
262 def last_chord (self):
263 return self.chords[-1]
264 def __init__ (self, fn):
265 self.divs_per_q = 1
266 self.header_dict = {
267 'tagline' :'automatically converted from Musedata',
268 'copyright' : 'all rights reserved -- free for noncommercial use'
269 # musedata license (argh)
271 self.entries = []
272 self.chords = []
275 lines = open (fn).readlines ()
276 lines = map (lambda x: re.sub ("\r$", '', x), lines)
277 lines = self.parse_header (lines)
278 lines = self.append_lines (lines)
279 str = string.join (lines, '\n')
280 lines = re.split ('[\n\r]+', str)
281 self.parse_body (lines)
283 def parse_header (self, lines):
284 enter = string.split (lines[3], ' ')
285 self.header_dict['enteredby'] = string.join (enter[1:])
286 self.header_dict['enteredon'] = enter[0]
287 self.header_dict['opus'] = lines[4]
288 self.header_dict['source'] = lines[5]
289 self.header_dict['title'] = lines[6]
290 self.header_dict['subtitle'] = lines[7]
291 self.header_dict['instrument']= lines[8]
292 self.header_dict['musedatamisc'] =lines[9]
293 self.header_dict['musedatagroups'] =lines[10]
294 self.header_dict['musedatagroupnumber']=lines[11]
295 lines = lines[12:]
296 comment = 0
297 while lines:
298 if lines[0][0] == '$':
299 break
300 lines = lines[1:]
301 return lines
303 def parse_musical_attributes (self,l):
304 atts = re.split('([A-Z][0-9]?):', l)
305 atts = atts[1:]
306 found = {}
307 while len (atts):
308 id = atts[0]
309 val = atts[1]
310 atts = atts[2:]
311 found[id] = val
313 try:
314 self.divs_per_q = string.atoi (found['Q'])
315 except KeyError:
316 pass
318 self.append_entry (Attribute_set (found))
319 def append_entry (self, e):
320 self.entries.append (e)
322 def parse_line_comment (self,l):
323 pass
325 def parse_note_line (self,l):
326 ch = None
327 if verbose:
328 print DIGITS+DIGITS+DIGITS
329 print l
330 pi = l[0:5]
331 di = l[5:8]
332 tied = l[8:9] == '-'
334 cue = grace = 0
335 if (pi[0] == 'g'):
336 grace = 1
337 pi = pi[1:]
338 elif (pi[0] == 'c'):
339 cue = 1
340 pi = pi[1:]
342 if pi[0] == ' ':
343 ch = self.last_chord ()
344 pi = pi[1:]
345 else:
346 ch = Chord ()
347 self.append_chord (ch)
350 ch.cue = ch.cue or cue
351 ch.grace = ch.grace or grace
353 while pi[0] in SPACES:
354 pi = pi[1:]
356 if pi[0] <> 'r':
357 name = ((ord (pi[0]) -ord('A')) + 5) % 7
358 alter = 0
359 pi = pi[1:]
360 while pi and pi[0] in '#f':
361 if pi[0] == '#':
362 alter = alter + 1
363 else:
364 alter = alter - 1
365 pi = pi[1:]
367 oct = string.atoi (pi) - 3
369 pittup = (oct, name ,alter)
370 ch.add_pitch (pittup)
372 ch.dots = 0
374 dot_str = l[17:18]
375 if dot_str == '.':
376 ch.dots = 1
377 elif dot_str == ':':
378 ch.dots = 2
380 base_dur = None
381 if ch.cue or ch.grace:
382 c = di[2]
383 if c == '0':
384 ch.accaciatura = 1
385 elif c == 'A':
386 base_dur = 0.5
387 else:
388 base_dur = 1 << (9 - (ord (c) - ord ('0')))
389 else:
390 fact = (1,1)
391 if ch.dots == 1:
392 fact = (2,3)
393 elif ch.dots == 2:
394 fact = (4, 7)
396 base_dur = (4 * self.divs_per_q* fact[1]) / (string.atoi (di)* fact[0])
397 ch.set_duration (base_dur)
399 ch.tied = ch.tied or tied
401 if l[26:27] == '[':
402 ch.start_beam = 1
403 elif l[26:27] == ']':
404 ch.end_beam = 1
407 additional = l[32:44]
408 for c in additional:
409 if c in '([{z':
410 ch.slurstart.append( 0)
411 continue
412 elif c in ')]}x':
413 ch.slurstop.append( 0)
414 continue
416 if c == '*':
417 ch.start_tuplet = 1
418 continue
419 elif c == '!':
420 ch.stop_tuplet = 1
421 continue
423 if c in DIGITS:
424 ch.add_script (c)
425 continue
427 if c == ' ' :
428 continue
430 try:
431 scr = script_table[c]
432 ch.add_script (scr)
433 c = None
434 except KeyError:
435 sys.stderr.write ("\nFixme: script `%s' not done\n" % c)
437 text = l[40:81]
438 sylls = string.split (text,'|')
440 for syl in sylls:
441 ch.add_syllable (syl)
444 def parse_measure_line (self,l):
445 self.append_entry (Measure_start())
448 def parse_duration (l):
449 s = ''
450 while l[0] in '0123456789':
451 s = s + l[0]
452 l= l[1:]
453 print l
454 num = string.atoi (s)
455 den = 4 * divisions
457 current_dots = 0
458 try_dots = [3, 2, 1]
459 for d in try_dots:
460 f = 1 << d
461 multiplier = (2*f-1)
462 if num % multiplier == 0 and den % f == 0:
463 num = num / multiplier
464 den = den / f
465 current_dots = current_dots + d
467 if num <> 1:
468 sys.stderr.write ('huh. Durations left')
469 return '%s%s' % (den, '.' * current_dots)
471 def append_lines (self,ls):
472 nls = []
473 for l in ls:
474 if l[0] == 'a':
475 nls[-1] = nls[-1]+l[1:]
476 else:
477 nls.append(l)
478 return nls
479 def dump (self):
480 s = ''
481 ln = ''
482 for e in self.entries:
484 next = ' ' + e.dump()
485 if len (ln) + len (next) > 72:
486 s = s +ln + '\n'
487 ln = ''
488 ln = ln + next
490 s = s + ln
492 s = '\\notes {\n %s \n}' % s
493 return s
495 def parse_body (self,lines):
496 comment_switch = 0
497 for l in lines:
498 if not l:
499 continue
501 c = l[0]
502 if c == '&':
503 comment_switch = not comment_switch
504 continue
506 if comment_switch:
507 continue
509 if 0:
510 pass
511 elif c == '$':
512 self.parse_musical_attributes (l)
513 elif c == '@':
514 self.parse_line_comment (l)
515 elif c == '*':
516 self.parse_musical_directions (l)
517 elif c in 'ABCDEFGr ':
518 self.parse_note_line (l)
519 elif c == 'm':
520 self.parse_measure_line (l)
521 elif c == '/':
522 break
523 elif c in 'PS':
524 pass # ignore sound & print
525 else:
526 sys.stderr.write ("\nUnrecognized record `%s'\n"% l)
532 def help ():
533 sys.stdout.write (
534 """Usage: musedata2ly [OPTION]... FILE1 [FILE2 ...]
536 Convert musedata to LilyPond.
538 Options:
539 -h,--help this help
540 -o,--output=FILE set output filename to FILE
541 -v,--version version information
542 -r,--ref=REF read background information from ref-file REF
544 Musedata (http://www.ccarh.org/musedata/) is an electronic library of
545 classical music scores, currently comprising XXX scores. The music is
546 encoded in so-called Musedata format
547 (http://www.ccarh.org/publications/books/beyondmidi/online/musedata).
548 musedata2ly converts a set of musedata files to one .ly file, and will
549 include a \header field if a .ref file is supplied
551 This converter is not complete -- this is left to the user as an excercise.
553 Report bugs to bug-lilypond@gnu.org.
555 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
556 """)
559 def print_version ():
560 sys.stdout.write ("""musedata2ly (GNU LilyPond) %s
562 This is free software. It is covered by the GNU General Public License,
563 and you are welcome to change it and/or distribute copies of it under
564 certain conditions. Invoke as `midi2ly --warranty' for more information.
566 Copyright (c) 2000--2002 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
567 """ % version)
568 def identify():
569 sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
573 (options, files) = getopt.getopt (sys.argv[1:], 'r:vo:h', ['verbose', 'ref=', 'help','version', 'output='])
574 out_filename = None
575 ref_file = None
576 for opt in options:
577 o = opt[0]
578 a = opt[1]
579 if o== '--help' or o == '-h':
580 help ()
581 sys.exit (0)
582 elif o == '--version' or o == '-v':
583 print_version ()
584 sys.exit(0)
585 elif o == '--ref' or o == '-r':
586 ref_file = a
587 elif o == '--output' or o == '-o':
588 out_filename = a
589 elif o == '--verbose' :
590 verbose = 1
591 else:
592 print o
593 raise getopt.error
595 identify()
599 ly = ''
602 found_ids = ''
604 for f in files:
605 if f == '-':
606 f = ''
608 sys.stderr.write ('Processing `%s\'\n' % f)
610 e = Parser(f)
612 id = os.path.basename (f)
613 id = re.sub ('[^a-zA-Z0-9]', 'x', id)
615 def num2let (match):
616 return chr (ord (match.group ()) - ord('0') + ord('A'))
618 id = re.sub ('[0-9]', num2let, id)
620 id = 'voice%s' % id
621 ly =ly + '\n\n%s = \\context Staff = "%s" %s\n\n' % (id, id, e.dump ())
623 found_ids = found_ids + '\\%s\n' % id
625 found_ids = '\n\n\n\\score { < %s > } ' % found_ids
627 ly_head = ''
628 if ref_file:
629 head = Ref_parser (ref_file)
630 if not out_filename:
631 t = ''
632 st = ''
633 try:
634 t = head.dict['title']
635 st= head.dict['subtitle']
636 except KeyError:
637 pass
639 t = t + '-' +st
641 t = re.sub ("^ +(.*) +$", r"\1", t)
642 t = re.sub ("\\.", '', t)
643 out_filename = re.sub ('[^a-zA-Z0-9-]', '-', t)
644 out_filename = out_filename+ '.ly'
645 ly_head = head.dump ()
647 if not out_filename:
648 out_filename = 'musedata.ly'
650 sys.stderr.write ('Writing `%s\'\n' % out_filename)
652 fo = open (out_filename, 'w')
653 fo.write ('%% lily was here -- automatically converted by musedata.ly\n')
654 fo.write(ly_head + ly + found_ids)
655 fo.close ()