]> git.donarmstrong.com Git - lilypond.git/blob - scripts/musedata2ly.py
dd6eaabdb569a8d6492b10eba293256cec803372
[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                 for p in self.pitches:
230                         if str:
231                                 str = str + ' ' 
232                         str = str + pitch_to_lily_string (p) 
233                 
234                 if len (self.pitches) > 1:
235                         str = '<<%s>>' % str
236                 elif len (self.pitches) == 0:
237                         str = 'r'
238
239                 str = str + sd + '-(' * len (self.slurstart) + '-)' * len (self.slurstart) 
240                 for s in self.scripts:
241                         str = str + '-' + s
242
243                 str = self.note_prefix +str  + self.note_suffix
244                 str = self.chord_prefix + str + self.chord_suffix
245                 return str
246
247 class Measure_start:
248         def dump (self):
249                 return ' |\n'
250         
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)
265                         }
266                 self.entries = []
267                 self.chords = []
268
269                 
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)
277                 
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
297         
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
307
308                 try:
309                         self.divs_per_q = string.atoi (found['Q'])
310                 except KeyError:
311                         pass
312                 
313                 self.append_entry (Attribute_set (found))
314         def append_entry (self, e):
315                 self.entries.append (e)
316
317         def parse_line_comment (self,l):
318                 pass
319
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] == '-'
328
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:]
336                 
337                 if pi[0] == ' ':
338                         ch = self.last_chord ()
339                         pi = pi[1:]
340                 else:
341                         ch = Chord ()
342                         self.append_chord (ch)
343
344
345                 ch.cue = ch.cue or cue
346                 ch.grace = ch.grace or grace
347
348                 while pi[0] in SPACES:
349                         pi = pi[1:]
350
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:]
361
362                         oct = string.atoi (pi) - 3
363
364                         pittup = (oct, name ,alter)
365                         ch.add_pitch (pittup)
366
367                 ch.dots = 0
368                 
369                 dot_str = l[17:18]
370                 if dot_str  == '.':
371                         ch.dots = 1
372                 elif dot_str == ':':
373                         ch.dots = 2
374
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)
390                         
391                         base_dur =  (4 * self.divs_per_q* fact[1]) / (string.atoi (di)* fact[0])
392                         ch.set_duration (base_dur)
393
394                 ch.tied = ch.tied or tied 
395         
396                 if l[26:27] == '[':
397                         ch.start_beam = 1
398                 elif l[26:27] == ']':
399                         ch.end_beam = 1
400
401
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
410                         
411                         if c == '*':
412                                 ch.start_tuplet = 1
413                                 continue
414                         elif c == '!':
415                                 ch.stop_tuplet = 1
416                                 continue
417
418                         if c in DIGITS:
419                                 ch.add_script (c)
420                                 continue
421
422                         if c == ' ' :
423                                 continue
424                         
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)
431
432                 text = l[40:81]
433                 sylls = string.split (text,'|')
434
435                 for syl in sylls:
436                         ch.add_syllable (syl)
437
438                         
439         def parse_measure_line (self,l):
440                 self.append_entry (Measure_start())
441
442
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 
451
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
461
462                 if num <> 1:
463                         sys.stderr.write ('huh. Durations left')
464                 return '%s%s' % (den, '.' * current_dots)
465         
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:
478                         
479                         next = ' ' + e.dump()
480                         if len (ln) + len (next) > 72:
481                                 s = s +ln +  '\n'
482                                 ln = ''
483                         ln = ln + next
484                         
485                 s = s + ln
486
487                 s = '\\notes {\n %s \n}' % s
488                 return s
489         
490         def parse_body (self,lines):
491                 comment_switch = 0
492                 for l in lines:
493                         if not l:
494                                 continue
495                         
496                         c = l[0]
497                         if c == '&':
498                                 comment_switch = not comment_switch
499                                 continue
500                         
501                         if comment_switch:
502                                 continue
503
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)
522
523
524
525
526
527 def help ():
528         sys.stdout.write (
529 """Usage: musedata2ly [OPTION]... FILE1 [FILE2 ...]
530
531 Convert musedata to LilyPond.
532
533 Options:
534   -h,--help          this help
535   -o,--output=FILE   set output filename to FILE
536   -v,--version       version information
537   -r,--ref=REF       read background information from ref-file REF     
538
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 
545
546 This converter is not complete -- this is left to the user as an excercise.
547
548 Report bugs to bug-lilypond@gnu.org.
549
550 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
551 """)
552
553
554 def print_version ():
555         sys.stdout.write ("""musedata2ly (GNU LilyPond) %s
556
557 This is free software.  It is covered by the GNU General Public License,
558 and you are welcome to change it and/or distribute copies of it under
559 certain conditions.  Invoke as `midi2ly --warranty' for more information.
560
561 Copyright (c) 2000--2002 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
562 """ % version)
563 def identify():
564         sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
565
566
567
568 (options, files) = getopt.getopt (sys.argv[1:], 'r:vo:h', ['verbose', 'ref=', 'help','version', 'output='])
569 out_filename = None
570 ref_file = None
571 for opt in options:
572         o = opt[0]
573         a = opt[1]
574         if o== '--help' or o == '-h':
575                 help ()
576                 sys.exit (0)
577         elif o == '--version' or o == '-v':
578                 print_version ()
579                 sys.exit(0)
580         elif o == '--ref' or o == '-r':
581                 ref_file = a 
582         elif o == '--output' or o == '-o':
583                 out_filename = a
584         elif o == '--verbose' :
585                 verbose = 1
586         else:
587                 print o
588                 raise getopt.error
589
590 identify()
591
592
593
594 ly = ''
595
596
597 found_ids = ''
598
599 for f in files:
600         if f == '-':
601                 f = ''
602
603         sys.stderr.write ('Processing `%s\'\n' % f)
604         
605         e = Parser(f)
606
607         id = os.path.basename (f)
608         id = re.sub ('[^a-zA-Z0-9]', 'x', id)
609
610         def num2let (match):
611                 return chr (ord (match.group ()) - ord('0') + ord('A'))
612                 
613         id = re.sub ('[0-9]', num2let, id)
614         
615         id = 'voice%s' % id
616         ly =ly + '\n\n%s = \\context Staff = "%s" %s\n\n' % (id, id, e.dump ())
617
618         found_ids = found_ids + '\\%s\n' % id
619
620 found_ids = '\n\n\n\\score { < %s > } ' % found_ids 
621
622 ly_head = ''
623 if ref_file:
624         head = Ref_parser (ref_file)
625         if not out_filename:
626                 t = ''
627                 st = ''
628                 try:
629                         t = head.dict['title']
630                         st= head.dict['subtitle']
631                 except KeyError:
632                         pass
633                         
634                 t = t + '-' +st
635                 
636                 t = re.sub ("^ +(.*) +$", r"\1", t)
637                 t = re.sub ("\\.", '', t)
638                 out_filename = re.sub ('[^a-zA-Z0-9-]', '-', t)
639                 out_filename = out_filename+ '.ly'
640         ly_head = head.dump ()
641         
642 if not out_filename:
643         out_filename = 'musedata.ly'
644         
645 sys.stderr.write ('Writing `%s\'\n' % out_filename)
646
647 fo = open (out_filename, 'w')
648 fo.write ('%% lily was here -- automatically converted by musedata.ly\n')
649 fo.write(ly_head + ly + found_ids)
650 fo.close ()
651