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