release commit
[lilypond.git] / scripts / musedata2ly.py
blob039da2d1661006b075b79e20da60c0f329757620
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 for p in self.pitches:
230 if str:
231 str = str + ' '
232 str = str + pitch_to_lily_string (p)
234 if len (self.pitches) > 1:
235 str = '<%s>' % str
236 elif len (self.pitches) == 0:
237 str = 'r'
239 str = str + sd + '(' * len (self.slurstart) + ')' * len (self.slurstart)
240 for s in self.scripts:
241 str = str + '-' + s
243 str = self.note_prefix +str + self.note_suffix
244 str = self.chord_prefix + str + self.chord_suffix
245 return str
247 class Measure_start:
248 def dump (self):
249 return ' |\n'
251 class Parser:
252 def append_entry (self, e):
253 self.entries.append (e)
254 def append_chord (self,c ):
255 self.chords.append (c)
256 self.entries.append (c)
257 def last_chord (self):
258 return self.chords[-1]
259 def __init__ (self, fn):
260 self.divs_per_q = 1
261 self.header_dict = {
262 'tagline' :'automatically converted from Musedata',
263 'copyright' : 'all rights reserved -- free for noncommercial use'
264 # musedata license (argh)
266 self.entries = []
267 self.chords = []
270 lines = open (fn).readlines ()
271 lines = map (lambda x: re.sub ("\r$", '', x), lines)
272 lines = self.parse_header (lines)
273 lines = self.append_lines (lines)
274 str = string.join (lines, '\n')
275 lines = re.split ('[\n\r]+', str)
276 self.parse_body (lines)
278 def parse_header (self, lines):
279 enter = string.split (lines[3], ' ')
280 self.header_dict['enteredby'] = string.join (enter[1:])
281 self.header_dict['enteredon'] = enter[0]
282 self.header_dict['opus'] = lines[4]
283 self.header_dict['source'] = lines[5]
284 self.header_dict['title'] = lines[6]
285 self.header_dict['subtitle'] = lines[7]
286 self.header_dict['instrument']= lines[8]
287 self.header_dict['musedatamisc'] =lines[9]
288 self.header_dict['musedatagroups'] =lines[10]
289 self.header_dict['musedatagroupnumber']=lines[11]
290 lines = lines[12:]
291 comment = 0
292 while lines:
293 if lines[0][0] == '$':
294 break
295 lines = lines[1:]
296 return lines
298 def parse_musical_attributes (self,l):
299 atts = re.split('([A-Z][0-9]?):', l)
300 atts = atts[1:]
301 found = {}
302 while len (atts):
303 id = atts[0]
304 val = atts[1]
305 atts = atts[2:]
306 found[id] = val
308 try:
309 self.divs_per_q = string.atoi (found['Q'])
310 except KeyError:
311 pass
313 self.append_entry (Attribute_set (found))
314 def append_entry (self, e):
315 self.entries.append (e)
317 def parse_line_comment (self,l):
318 pass
320 def parse_note_line (self,l):
321 ch = None
322 if verbose:
323 print DIGITS+DIGITS+DIGITS
324 print l
325 pi = l[0:5]
326 di = l[5:8]
327 tied = l[8:9] == '-'
329 cue = grace = 0
330 if (pi[0] == 'g'):
331 grace = 1
332 pi = pi[1:]
333 elif (pi[0] == 'c'):
334 cue = 1
335 pi = pi[1:]
337 if pi[0] == ' ':
338 ch = self.last_chord ()
339 pi = pi[1:]
340 else:
341 ch = Chord ()
342 self.append_chord (ch)
345 ch.cue = ch.cue or cue
346 ch.grace = ch.grace or grace
348 while pi[0] in SPACES:
349 pi = pi[1:]
351 if pi[0] <> 'r':
352 name = ((ord (pi[0]) -ord('A')) + 5) % 7
353 alter = 0
354 pi = pi[1:]
355 while pi and pi[0] in '#f':
356 if pi[0] == '#':
357 alter = alter + 1
358 else:
359 alter = alter - 1
360 pi = pi[1:]
362 oct = string.atoi (pi) - 3
364 pittup = (oct, name ,alter)
365 ch.add_pitch (pittup)
367 ch.dots = 0
369 dot_str = l[17:18]
370 if dot_str == '.':
371 ch.dots = 1
372 elif dot_str == ':':
373 ch.dots = 2
375 base_dur = None
376 if ch.cue or ch.grace:
377 c = di[2]
378 if c == '0':
379 ch.accaciatura = 1
380 elif c == 'A':
381 base_dur = 0.5
382 else:
383 base_dur = 1 << (9 - (ord (c) - ord ('0')))
384 else:
385 fact = (1,1)
386 if ch.dots == 1:
387 fact = (2,3)
388 elif ch.dots == 2:
389 fact = (4, 7)
391 base_dur = (4 * self.divs_per_q* fact[1]) / (string.atoi (di)* fact[0])
392 ch.set_duration (base_dur)
394 ch.tied = ch.tied or tied
396 if l[26:27] == '[':
397 ch.start_beam = 1
398 elif l[26:27] == ']':
399 ch.end_beam = 1
402 additional = l[32:44]
403 for c in additional:
404 if c in '([{z':
405 ch.slurstart.append( 0)
406 continue
407 elif c in ')]}x':
408 ch.slurstop.append( 0)
409 continue
411 if c == '*':
412 ch.start_tuplet = 1
413 continue
414 elif c == '!':
415 ch.stop_tuplet = 1
416 continue
418 if c in DIGITS:
419 ch.add_script (c)
420 continue
422 if c == ' ' :
423 continue
425 try:
426 scr = script_table[c]
427 ch.add_script (scr)
428 c = None
429 except KeyError:
430 sys.stderr.write ("\nFixme: script `%s' not done\n" % c)
432 text = l[40:81]
433 sylls = string.split (text,'|')
435 for syl in sylls:
436 ch.add_syllable (syl)
439 def parse_measure_line (self,l):
440 self.append_entry (Measure_start())
443 def parse_duration (l):
444 s = ''
445 while l[0] in '0123456789':
446 s = s + l[0]
447 l= l[1:]
448 print l
449 num = string.atoi (s)
450 den = 4 * divisions
452 current_dots = 0
453 try_dots = [3, 2, 1]
454 for d in try_dots:
455 f = 1 << d
456 multiplier = (2*f-1)
457 if num % multiplier == 0 and den % f == 0:
458 num = num / multiplier
459 den = den / f
460 current_dots = current_dots + d
462 if num <> 1:
463 sys.stderr.write ('huh. Durations left')
464 return '%s%s' % (den, '.' * current_dots)
466 def append_lines (self,ls):
467 nls = []
468 for l in ls:
469 if l[0] == 'a':
470 nls[-1] = nls[-1]+l[1:]
471 else:
472 nls.append(l)
473 return nls
474 def dump (self):
475 s = ''
476 ln = ''
477 for e in self.entries:
479 next = ' ' + e.dump()
480 if len (ln) + len (next) > 72:
481 s = s +ln + '\n'
482 ln = ''
483 ln = ln + next
485 s = s + ln
487 s = '\\notes {\n %s \n}' % s
488 return s
490 def parse_body (self,lines):
491 comment_switch = 0
492 for l in lines:
493 if not l:
494 continue
496 c = l[0]
497 if c == '&':
498 comment_switch = not comment_switch
499 continue
501 if comment_switch:
502 continue
504 if 0:
505 pass
506 elif c == '$':
507 self.parse_musical_attributes (l)
508 elif c == '@':
509 self.parse_line_comment (l)
510 elif c == '*':
511 self.parse_musical_directions (l)
512 elif c in 'ABCDEFGr ':
513 self.parse_note_line (l)
514 elif c == 'm':
515 self.parse_measure_line (l)
516 elif c == '/':
517 break
518 elif c in 'PS':
519 pass # ignore sound & print
520 else:
521 sys.stderr.write ("\nUnrecognized record `%s'\n"% l)
527 def help ():
528 sys.stdout.write (
529 """Usage: musedata2ly [OPTIONS]... FILE1 [FILE2 ...]
531 Convert musedata to LilyPond.
533 Options:
534 -h,--help print this help
535 -o,--output=FILE set output filename to FILE
536 -v,--version show version information
537 -r,--ref=REF read background information from ref-file REF
539 Musedata (http://www.ccarh.org/musedata/) is an electronic library of
540 classical music scores, currently comprising XXX scores. The music is
541 encoded in so-called Musedata format
542 (http://www.ccarh.org/publications/books/beyondmidi/online/musedata).
543 musedata2ly converts a set of musedata files to one .ly file, and will
544 include a \header field if a .ref file is supplied.
546 This converter is not complete -- this is left to the user as an excercise.
548 Report bugs to bug-lilypond@gnu.org.
550 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>.
552 """)
555 def print_version ():
556 sys.stdout.write ("""musedata2ly (GNU LilyPond) %s
558 This is free software. It is covered by the GNU General Public License,
559 and you are welcome to change it and/or distribute copies of it under
560 certain conditions. Invoke as `midi2ly --warranty' for more information.
562 Copyright (c) 2000--2003 by Han-Wen Nienhuys <hanwen@cs.uu.nl>.
563 """ % version)
564 def identify():
565 sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
569 (options, files) = getopt.getopt (sys.argv[1:], 'r:vo:h', ['verbose', 'ref=', 'help','version', 'output='])
570 out_filename = None
571 ref_file = None
572 for opt in options:
573 o = opt[0]
574 a = opt[1]
575 if o== '--help' or o == '-h':
576 help ()
577 sys.exit (0)
578 elif o == '--version' or o == '-v':
579 print_version ()
580 sys.exit(0)
581 elif o == '--ref' or o == '-r':
582 ref_file = a
583 elif o == '--output' or o == '-o':
584 out_filename = a
585 elif o == '--verbose' :
586 verbose = 1
587 else:
588 print o
589 raise getopt.error
591 identify()
595 ly = ''
598 found_ids = ''
600 for f in files:
601 if f == '-':
602 f = ''
604 sys.stderr.write ('Processing `%s\'\n' % f)
606 e = Parser(f)
608 id = os.path.basename (f)
609 id = re.sub ('[^a-zA-Z0-9]', 'x', id)
611 def num2let (match):
612 return chr (ord (match.group ()) - ord('0') + ord('A'))
614 id = re.sub ('[0-9]', num2let, id)
616 id = 'voice%s' % id
617 ly =ly + '\n\n%s = \\context Staff = "%s" %s\n\n' % (id, id, e.dump ())
619 found_ids = found_ids + '\\%s\n' % id
621 found_ids = '\n\n\n\\score { << %s >> } ' % found_ids
623 ly_head = ''
624 if ref_file:
625 head = Ref_parser (ref_file)
626 if not out_filename:
627 t = ''
628 st = ''
629 try:
630 t = head.dict['title']
631 st= head.dict['subtitle']
632 except KeyError:
633 pass
635 t = t + '-' +st
637 t = re.sub ("^ +(.*) +$", r"\1", t)
638 t = re.sub ("\\.", '', t)
639 out_filename = re.sub ('[^a-zA-Z0-9-]', '-', t)
640 out_filename = out_filename+ '.ly'
641 ly_head = head.dump ()
643 if not out_filename:
644 out_filename = 'musedata.ly'
646 sys.stderr.write ('Writing `%s\'\n' % out_filename)
648 fo = open (out_filename, 'w')
649 fo.write ('%% lily was here -- automatically converted by musedata.ly\n')
650 fo.write(ly_head + ly + found_ids)
651 fo.close ()