]> git.donarmstrong.com Git - lilypond.git/blob - scripts/musedata2ly.py
99d74fb764298e516d08a4bac2a29d3dd6b9b800
[lilypond.git] / scripts / musedata2ly.py
1 #!@PYTHON@
2
3 # musedata = musedata.stanford.edu
4 # musedata = COBOL for musicians.
5
6
7 # TODO
8 #
9 # * clefs,
10 # * keys,
11 # * staffs,
12 # * multiple voices (they use `Backspace' (shudder)
13 # * tuplets
14 #
15
16 #
17 # I completely forgot how this was supposed to work --hwn 5/2002 
18 #
19 #
20
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
30
31
32
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'
53         }
54
55
56 class Ref_parser:
57         def __init__ (self, fn):
58                 self.dict = {}
59                 
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:
70                                         
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
79
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
88         
89 verbose = 0
90
91
92 actab = {-2: 'eses', -1: 'es', 0 : '', 1: 'is', 2:'isis'}
93
94 def pitch_to_lily_string (tup):
95         (o,n,a) = tup
96
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
104
105 def get_key (s):
106         i = string.atoi (s)
107         return ''
108
109 def get_timesig (s):
110         return '\\time %s\n' % s
111
112
113 divisions = 4
114 def get_divisions_per_quarter (s):
115         divisions = string.atoi (s) 
116         return ''
117
118 def get_directive (s):
119         return '%% %s\n' % s
120
121 def get_transposing (s):
122         return ''
123
124 def get_num_instruments (s):
125         return ''
126
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
135
136         while ac:
137                 s = s + infix + 's'
138                 ac = ac - 1
139         return s
140 def get_clef ():
141         return ''
142
143 SPACES = ' '
144 DIGITS = "0123456789"
145
146
147 clef_dict = {
148 04: 'treble',
149 13 : 'alto',
150 22: 'bass',
151 }
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,
160         }
161
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'])
169                 
170                 return s
171
172
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',
190 }
191
192
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
205
206                 self.note_suffix = self.note_prefix = ''
207                 self.chord_suffix = self.chord_prefix = ''
208                 
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)
217                 
218         def dump (self):
219                 str = ''
220
221                 sd = ''
222                 if self.basic_duration == 0.5:
223                         sd = '\\breve'
224                 else:
225                         sd = '%d' % self.basic_duration
226
227                 sd = sd + '.' * self.dots
228
229                 str = ')' * len (self.slurstart) + str
230                 
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)
236                 
237
238                 for s in self.scripts:
239                         str = str + '-' + s
240
241                 str = self.note_prefix +str  + self.note_suffix
242                 
243                 if len (self.pitches) > 1:
244                         str = '<%s>' % str
245                 elif len (self.pitches) == 0:
246                         str = 'r' + sd
247
248                 str = self.chord_prefix + str + self.chord_suffix
249                 
250                 return str
251
252 class Measure_start:
253         def dump (self):
254                 return ' |\n'
255         
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)
270                         }
271                 self.entries = []
272                 self.chords = []
273
274                 
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)
282                 
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
302         
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
312
313                 try:
314                         self.divs_per_q = string.atoi (found['Q'])
315                 except KeyError:
316                         pass
317                 
318                 self.append_entry (Attribute_set (found))
319         def append_entry (self, e):
320                 self.entries.append (e)
321
322         def parse_line_comment (self,l):
323                 pass
324
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] == '-'
333
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:]
341                 
342                 if pi[0] == ' ':
343                         ch = self.last_chord ()
344                         pi = pi[1:]
345                 else:
346                         ch = Chord ()
347                         self.append_chord (ch)
348
349
350                 ch.cue = ch.cue or cue
351                 ch.grace = ch.grace or grace
352
353                 while pi[0] in SPACES:
354                         pi = pi[1:]
355
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:]
366
367                         oct = string.atoi (pi) - 3
368
369                         pittup = (oct, name ,alter)
370                         ch.add_pitch (pittup)
371
372                 ch.dots = 0
373                 
374                 dot_str = l[17:18]
375                 if dot_str  == '.':
376                         ch.dots = 1
377                 elif dot_str == ':':
378                         ch.dots = 2
379
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)
395                         
396                         base_dur =  (4 * self.divs_per_q* fact[1]) / (string.atoi (di)* fact[0])
397                         ch.set_duration (base_dur)
398
399                 ch.tied = ch.tied or tied 
400         
401                 if l[26:27] == '[':
402                         ch.start_beam = 1
403                 elif l[26:27] == ']':
404                         ch.end_beam = 1
405
406
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
415                         
416                         if c == '*':
417                                 ch.start_tuplet = 1
418                                 continue
419                         elif c == '!':
420                                 ch.stop_tuplet = 1
421                                 continue
422
423                         if c in DIGITS:
424                                 ch.add_script (c)
425                                 continue
426
427                         if c == ' ' :
428                                 continue
429                         
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)
436
437                 text = l[40:81]
438                 sylls = string.split (text,'|')
439
440                 for syl in sylls:
441                         ch.add_syllable (syl)
442
443                         
444         def parse_measure_line (self,l):
445                 self.append_entry (Measure_start())
446
447
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 
456
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
466
467                 if num <> 1:
468                         sys.stderr.write ('huh. Durations left')
469                 return '%s%s' % (den, '.' * current_dots)
470         
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:
483                         
484                         next = ' ' + e.dump()
485                         if len (ln) + len (next) > 72:
486                                 s = s +ln +  '\n'
487                                 ln = ''
488                         ln = ln + next
489                         
490                 s = s + ln
491
492                 s = '\\notes {\n %s \n}' % s
493                 return s
494         
495         def parse_body (self,lines):
496                 comment_switch = 0
497                 for l in lines:
498                         if not l:
499                                 continue
500                         
501                         c = l[0]
502                         if c == '&':
503                                 comment_switch = not comment_switch
504                                 continue
505                         
506                         if comment_switch:
507                                 continue
508
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)
527
528
529
530
531
532 def help ():
533         sys.stdout.write (
534 """Usage: musedata2ly [OPTION]... FILE1 [FILE2 ...]
535
536 Convert musedata to LilyPond.
537
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     
543
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 
550
551 This converter is not complete -- this is left to the user as an excercise.
552
553 Report bugs to bug-lilypond@gnu.org.
554
555 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
556 """)
557
558
559 def print_version ():
560         sys.stdout.write ("""musedata2ly (GNU LilyPond) %s
561
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.
565
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))
570
571
572
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
594
595 identify()
596
597
598
599 ly = ''
600
601
602 found_ids = ''
603
604 for f in files:
605         if f == '-':
606                 f = ''
607
608         sys.stderr.write ('Processing `%s\'\n' % f)
609         
610         e = Parser(f)
611
612         id = os.path.basename (f)
613         id = re.sub ('[^a-zA-Z0-9]', 'x', id)
614
615         def num2let (match):
616                 return chr (ord (match.group ()) - ord('0') + ord('A'))
617                 
618         id = re.sub ('[0-9]', num2let, id)
619         
620         id = 'voice%s' % id
621         ly =ly + '\n\n%s = \\context Staff = "%s" %s\n\n' % (id, id, e.dump ())
622
623         found_ids = found_ids + '\\%s\n' % id
624
625 found_ids = '\n\n\n\\score { < %s > } ' % found_ids 
626
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
638                         
639                 t = t + '-' +st
640                 
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 ()
646         
647 if not out_filename:
648         out_filename = 'musedata.ly'
649         
650 sys.stderr.write ('Writing `%s\'\n' % out_filename)
651
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 ()
656