]> git.donarmstrong.com Git - lilypond.git/blob - scripts/pmx2ly.py
* scripts/musedata2ly.py (): idem
[lilypond.git] / scripts / pmx2ly.py
1 #!@PYTHON@
2
3 # PMX is a Musixtex preprocessor written by Don Simons, see
4 # http://www.gmd.de/Misc/Music/musixtex/software/pmx/
5
6 # TODO:
7 #  * block openings aren't parsed.
8
9 import os
10 import string
11 import sys
12 import re
13 import getopt
14
15 program_name = 'pmx2ly'
16 version = '@TOPLEVEL_VERSION@'
17 if version == '@' + 'TOPLEVEL_VERSION' + '@':
18         version = '(unknown version)'      # uGUHGUHGHGUGH
19
20
21 def encodeint (i):
22         return chr ( i  + ord ('A'))
23
24         
25 actab = {-2: 'eses', -1: 'es', 0 : '', 1: 'is', 2:'isis'}
26
27 def pitch_to_lily_string (tup):
28         (o,n,a) = tup
29
30         nm = chr((n + 2) % 7 + ord ('a'))
31         nm = nm + actab[a]
32         if o > 0:
33                 nm = nm + "'" * o
34         elif o < 0:
35                 nm = nm + "," * -o
36         return nm
37
38 def gcd (a,b):
39         if b == 0:
40                 return a
41         c = a
42         while c: 
43                 c = a % b
44                 a = b
45                 b = c
46         return a
47
48 def rat_simplify (r):
49         (n,d) = r
50         if d < 0:
51                 d = -d
52                 n = -n
53         if n == 0:
54                 return (0,1)
55         else:
56                 g = gcd (n, d)
57                 return (n/g, d/g)
58         
59 def rat_multiply (a,b):
60         (x,y) = a
61         (p,q) = b
62
63         return rat_simplify ((x*p, y*q))
64
65 def rat_divide (a,b):
66         (p,q) = b
67         return rat_multiply (a, (q,p))
68
69 tuplet_table = {
70         2: 3,
71         3: 2,
72         5: 4
73 }
74
75
76 def rat_add (a,b):
77         (x,y) = a
78         (p,q) = b
79
80         return rat_simplify ((x*q + p*y, y*q))
81
82 def rat_neg (a):
83         (p,q) = a
84         return (-p,q)
85
86
87 def rat_larger (a,b):
88         return rat_subtract (a, b )[0] > 0
89
90 def rat_subtract (a,b ):
91         return rat_add (a, rat_neg (b))
92
93 def rat_to_duration (frac):
94         log = 1
95         d = (1,1)
96         while rat_larger (d, frac):
97                 d = rat_multiply (d, (1,2))
98                 log = log << 1
99
100         frac = rat_subtract (frac, d)
101         dots = 0
102         if frac == rat_multiply (d, (1,2)):
103                 dots = 1
104         elif frac == rat_multiply (d, (3,4)):
105                 dots = 2
106         return (log, dots)      
107
108
109 class Barcheck :
110         def __init__ (self):
111                 pass
112         def dump (self):
113                 return '|\n'
114
115
116 class Meter :
117         def __init__ (self,nums):
118                 self.nums = nums
119         def dump (self):
120                 return ' %{ FIXME: meter change %} '
121                 
122 class Beam:
123         def __init__ (self, ch):
124                 self.char = ch
125         def dump (self):
126                 return self.char
127
128 class Slur:
129         def __init__ (self,id):
130                 self.id = id
131                 self.start_chord = None
132                 self.end_chord = None
133         def calculate (self):
134                 s =self.start_chord
135                 e= self.end_chord
136
137                 if e and s:
138                         s.note_suffix = s.note_suffix + '-('
139                         e.note_prefix = e.note_suffix + '-)' 
140                 else:
141                         sys.stderr.write ("\nOrphaned slur")
142
143
144 class Voice:
145         def __init__ (self):
146                 self.entries = []
147                 self.chords = []
148                 self.staff = None
149                 self.current_slurs = []
150                 self.slurs = []
151         def toggle_slur (self, id):
152                 
153                 for s in self.current_slurs:
154                         if s.id == id:
155                                 self.current_slurs.remove (s)
156                                 s.end_chord = self.chords[-1]
157                                 return
158                 s = Slur (id)
159                 s.start_chord = self.chords[-1]
160                 self.current_slurs.append (s)
161                 self.slurs.append (s)
162                 
163         def last_chord (self):
164                 return self.chords[-1]
165         def add_chord (self, ch):
166                 self.chords.append (ch)
167                 self.entries.append (ch)
168         def add_nonchord (self, nch):
169                 self.entries.append (nch)
170
171         def idstring (self):
172                 return 'staff%svoice%s ' % (encodeint (self.staff.number) , encodeint(self.number))
173         def dump (self):
174                 str = ''
175                 ln = ''
176                 for e in self.entries:
177                         next = ' ' + e.dump ()
178                         if next[-1] == '\n':
179                                 str  = str + ln + next
180                                 ln = ''
181                                 continue
182                         
183                         if len (ln) +len (next) > 72:
184                                 str = str+ ln + '\n'
185                                 ln = ''
186                         ln = ln + next
187                         
188                         
189                 str = str  + ln
190                 id = self.idstring ()
191                         
192                 str = '%s =  \\notes { \n %s }\n '% (id, str)
193                 return str
194         def calculate_graces (self):
195                 lastgr = 0
196                 lastc = None
197                 for c in self.chords:
198                         if c.grace and  not lastgr:
199                                 c.chord_prefix = c.chord_prefix + '\\grace { '
200                         elif not c.grace and lastgr:
201                                 lastc.chord_suffix = lastc.chord_suffix + ' } '
202                         lastgr = c.grace
203                         lastc = c
204         def calculate (self):
205                 self.calculate_graces ()
206                 for s in self.slurs:
207                         s.calculate ()
208
209 class Clef:
210         def __init__ (self, cl):
211                 self.type = cl
212         def dump(self):
213                 return '\\clef %s' % self.type
214
215 class Key:
216         def __init__ (self, key):
217                 self.type = key
218         def dump(self):
219                 return '\\key %s' % self.type
220
221 clef_table = {
222         'b':'bass'  ,
223         'r':'baritone',
224         'n':'tenor',
225         'a':'alto',
226         'm':'mezzosoprano',
227         's':'soprano',
228         't':'treble',
229         'f':'frenchviolin',
230         }
231 key_table = {
232         '+0':'c \major',
233         '+1':'g \major',
234         '+2':'d \major',
235         '+3':'a \major',
236         '+4':'e \major',
237         '+5':'b \major',
238         '+6':'fis \major',
239         '-1':'f \major',
240         '-2':'bes \major',
241         '-3':'ees \major',
242         '-4':'aes \major',
243         '-5':'des \major',
244         '-6':'ges \major'
245         }
246 class Staff:
247         def __init__ (self): 
248                 self.voices = (Voice (), Voice())
249                 self.clef = None
250                 self.instrument = 0
251                 self.voice_idx = 0
252                 self.number = None
253                 self.key = 0
254                 
255                 i = 0
256                 for v  in self.voices:
257                         v.staff = self
258                         v.number = i
259                         i = i+1
260         def set_clef (self, letter):
261                 clstr = clef_table[letter]
262                 self.voices[0].add_nonchord (Clef (clstr))
263
264         
265         def current_voice (self):
266                 return self.voices[self.voice_idx]
267         def next_voice (self):
268                 self.voice_idx = (self.voice_idx + 1)%len (self.voices)
269
270         def calculate (self):
271                 for v in self.voices:
272                         v.calculate ()
273         def idstring (self):
274                 return 'staff%s' % encodeint (self.number)
275         def dump (self):
276                 str = ''
277
278                 refs = ''
279                 for v in self.voices:
280                         str = str + v.dump()
281                         refs = refs + '\\' + v.idstring ()+  ' '
282                 
283                 str = str + '\n\n%s = \\context Staff = %s \n  < \n %s >\n\n\n'% (self.idstring (), self.idstring (), refs)
284                 return str
285
286 class Tuplet:
287         def __init__ (self, number, base, dots):
288                 self.chords = []
289                 self.number = number
290                 self.replaces = tuplet_table[number]
291                 self.base = base
292                 self.dots = dots
293                 
294                 length = (1,base)
295                 if dots == 1:
296                         length = rat_multiply (length, (3,2))
297                 elif dots == 2:
298                         length = rat_multiply (length, (7,4))
299
300                 length = rat_multiply (length, (1,self.replaces))
301
302                 (nb,nd) =rat_to_duration (length)
303
304                 self.note_base = nb
305                 self.note_dots = nd
306
307         def add_chord (self, ch):
308                 ch.dots = self.note_dots
309                 ch.basic_duration = self.note_base
310                 self.chords.append (ch)
311
312                 if len (self.chords) == 1:
313                         ch.chord_prefix = '\\times %d/%d { ' % (self.replaces, self.number)
314                 elif len (self.chords) == self.number:
315                         ch.chord_suffix = ' }' 
316                 
317 class Chord:
318         def __init__ (self):
319                 self.pitches = []
320                 self.dots = 0
321                 self.basic_duration = 0
322                 self.scripts = []
323                 self.grace = 0
324                 self.chord_prefix = ''
325                 self.chord_suffix = ''
326                 self.note_prefix = ''
327                 self.note_suffix = ''
328                 
329         def dump (self):
330                 str = ''
331
332                 sd = ''
333                 if self.basic_duration == 0.5:
334                         sd = '\\breve'
335                 else:
336                         sd = '%d' % self.basic_duration
337                 sd = sd + '.' * self.dots 
338                 for p in self.pitches:
339                         if str:
340                                 str = str + ' ' 
341                         str = str + pitch_to_lily_string (p) 
342
343                 if len (self.pitches) > 1:
344                         str = '<<%s>>' % str
345                 elif len (self.pitches) == 0:
346                         str = 'r'
347
348                 str = str + sd
349                 for s in self.scripts:
350                         str = str + '-' + s
351
352                 str = self.note_prefix + str + self.note_suffix
353                 str = self.chord_prefix + str + self.chord_suffix
354                 
355                 return str
356                 
357 SPACE=' \t\n'
358 DIGITS ='0123456789'
359 basicdur_table = {
360         9: 0.5,
361         0: 0 ,
362         2: 2 ,
363         4: 4 ,
364         8: 8 ,
365         1: 16,
366         3: 32,
367         6: 64
368         }
369
370
371 ornament_table = {
372         't': '\\prall',
373         'm': '\\mordent',
374         'x': '"x"',
375         '+': '+',
376         'u': '"pizz"',
377         'p': '|',
378         '(': '"paren"',
379         ')': '"paren"',
380         'g': '"segno"',
381         '.': '.',
382         'fd': '\\fermata',
383         'f': '\\fermata',
384         '_': '-',
385         'T': '\\trill',
386         '>': '>',
387         '^': '^',
388         }
389
390 class Parser:
391         def __init__ (self, filename):
392                 self.staffs = []
393                 self.forced_duration = None
394                 self.last_name = 0
395                 self.last_oct = 0               
396                 self.tuplets_expected = 0
397                 self.tuplets = []
398                 self.last_basic_duration = 4
399
400                 self.parse (filename)
401                 
402         def set_staffs (self, number):
403                 self.staffs = map (lambda x: Staff (), range(0, number))
404                 
405                 self.staff_idx = 0
406
407                 i =0
408                 for s in self.staffs:
409                         s.number = i
410                         i = i+1
411         def current_staff (self):
412                 return self.staffs[self.staff_idx]
413
414         def current_voice (self):
415                 return self.current_staff ().current_voice ()
416         
417         def next_staff (self):
418                 self.staff_idx = (self.staff_idx + 1)% len (self.staffs)
419                 
420         def parse_note (self, str):
421                 name = None
422                 ch = None
423
424                 grace = 0
425                 if str[0] == 'G':
426                         grace = 1
427                         str = str[1:]
428                         
429                 if str[0] == 'z':
430                         ch = self.current_voice().last_chord()
431                         str = str[1:]
432                 else:
433                         ch = Chord ()
434                         self.current_voice().add_chord (ch)
435
436                 # what about 's'?
437                 if str[0] <> 'r':
438                         name = (ord (str[0]) - ord('a') + 5) % 7
439
440                 str = str[1:]
441
442                 ch.grace = ch.grace or grace 
443                 
444                 forced_duration  = 0
445                 alteration = 0
446                 dots = 0
447                 oct = None
448                 durdigit = None
449                 multibar = 0
450                 tupnumber = 0
451                 extra_oct = 0
452                 while str[0] in 'dsfmnul0123456789.,+-':
453                         c = str[0]
454                         str = str[1:]
455                         if c == 'f':
456                                 alteration = alteration -1
457                         elif c == 'n':
458                                 alteration = 0
459                         elif c == 'm':
460                                 multibar = 1
461                         elif c == 's':
462                                 alteration = alteration +1
463                         elif c == 'd':
464                                 dots = dots + 1
465                         elif c in DIGITS and durdigit == None and \
466                              self.tuplets_expected == 0:
467                                 durdigit = string.atoi (c)
468                         elif c in DIGITS:
469                                 oct = string.atoi (c) - 3
470                         elif c == '+':
471                                 extra_oct = extra_oct + 1
472                         elif c == '-':
473                                 extra_oct = extra_oct - 1
474                         elif c == '.':
475                                 dots = dots+ 1
476                                 forced_duration = 2
477                         elif c == ',':
478                                 forced_duration = 2
479
480                 if str[0] == 'x':
481                         str = str[1:]
482                         tupnumber = string.atoi (str[0])
483                         str = str[1:]
484                         str=re.sub (r'^n?f?[+-0-9.]+', '' , str)
485
486                 
487                 if durdigit:
488                         try:
489                                 basic_duration =  basicdur_table[durdigit]
490                                 self.last_basic_duration = basic_duration
491                         except KeyError:
492                                 sys.stderr.write ("""
493 Huh? expected duration, found %d Left was `%s'""" % (durdigit, str[:20]))
494
495                                 basic_duration = 4
496                 else:
497                         basic_duration = self.last_basic_duration
498
499
500                 
501                 if name <> None and oct == None:
502                         e = 0
503                         if self.last_name < name and name -self.last_name > 3:
504                                 e = -1
505                         elif self.last_name > name and self.last_name -name > 3:
506                                 e = 1
507
508                         oct = self.last_oct  +e + extra_oct
509
510                 if name <> None:
511                         self.last_oct = oct
512                         self.last_name = name
513                                 
514                 if name <> None:
515                         ch.pitches.append ((oct, name,  alteration))
516
517                 # do before adding to tuplet.
518                 ch.basic_duration = basic_duration
519                 ch.dots = dots
520
521                 if forced_duration:
522                         self.forced_duration = ch.basic_duration / forced_duration
523
524                 if tupnumber:
525                         tup =Tuplet (tupnumber, basic_duration, dots)
526                         self.tuplets_expected = tupnumber
527                         self.tuplets.append (tup)
528
529                 if self.tuplets_expected > 0:
530                         self.tuplets[-1].add_chord (ch)
531                         self.tuplets_expected = self.tuplets_expected - 1
532                         
533                 return str
534         def parse_basso_continuo (self, str):
535                 while str[0] in DIGITS +'#n-':
536                         scr = str[0]
537
538                         if scr == '#':
539                                 scr = '\\\\textsharp'
540                         
541                         if len(scr)>1 or scr not in DIGITS:
542                                 scr = '"%s"' % scr
543                                 
544                         self.current_voice().last_chord ().scripts.append (scr)
545                         str=str[1:]
546                 return str
547         def parse_beams (self,str):
548                 c = str[0]
549         #       self.current_voice().add_nonchord (Beam(c))
550                 if str[0] == '[':
551                         str = str[1:]
552                         while str[0] in '+-0123456789':
553                                 str=str[1:]
554                 else:
555                         str = str[1:]
556                                         
557                 return str
558
559         def parse_key (self, str):
560                 key = ""
561                 #The key is changed by a string of the form K[+-]<num>[+-]<num>
562                 #where the first number is the transposition and the second number is the
563                 #new key signature.  For now, we won't bother with the transposition.
564                 if str[2] != '0':
565                         sys.stderr.write("Transposition not implemented yet: ")
566                         while str[0] in '+-0123456789':
567                                 str = str[1:]
568                 else:
569                         str=str[3:]
570                         key = ''
571                         while str[0] in '+-0123456789':
572                                 key=key + str[0]
573                                 str=str[1:]
574                         keystr = key_table[key]
575                         self.current_voice().add_nonchord (Key(keystr))
576                 return(str)
577
578
579         def parse_header  (self, ls):
580                 def atonum(a):
581                         if re.search('\\.', a):
582                                 return string.atof (a)
583                         else:
584                                 return string.atoi (a)
585
586                 number_count = 12
587                 numbers = []
588
589                 while len (numbers) < number_count:
590                         opening = ls[0]
591                         ls = ls[1:]
592
593                         opening = re.sub ('[ \t\n]+', ' ', opening)
594                         opening = re.sub ('^ ', '', opening)
595                         opening = re.sub (' $', '', opening)                                            
596                         if opening == '':
597                                 continue
598                         opening = string.split (opening, ' ')
599
600                         numbers = numbers + map (atonum, opening)
601
602                 (no_staffs, no_instruments, timesig_num, timesig_den, ptimesig_num,
603                  ptimesig_den, pickup_beats,keysig_number) = tuple (numbers[0:8])
604                 (no_pages,no_systems, musicsize, fracindent) = tuple (numbers[8:])
605
606                 # ignore this.
607                 # opening = map (string.atoi, re.split ('[\t ]+', opening))
608                 
609                 instruments = []
610                 while len (instruments) < no_instruments:
611                         instruments.append (ls[0])
612                         ls = ls[1:]
613
614                 l = ls[0]
615                 ls = ls[1:]
616
617                 self.set_staffs (no_staffs)
618
619                 for s in self.staffs:
620                         s.set_clef(l[0])
621                         l = l[1:]
622
623                 # dump path 
624                 ls = ls[1:] 
625
626                 # dump more ?
627                 return ls
628
629         def parse_ornament (self, left):
630                 left = left[1:]
631                 e = self.current_voice ().last_chord ()
632
633                 id = left[0]
634                 left = left[1:]
635                 if left[0] == 'd':
636                         id = id +'d'
637                         left = left [1:]
638
639                 orn = '"orn"'
640                 try:
641                         orn = ornament_table[id]
642                 except KeyError:
643                         sys.stderr.write ("unknown ornament `%s'\n" % id)
644                         
645                 e.scripts.append (orn)
646                 return left
647         def parse_barcheck (self, left):
648                 self.current_voice ().add_nonchord (Barcheck ())
649                 
650                 return left [1:]
651
652         def parse_slur (self, left):
653                 left = left[1:]
654
655                 id = None
656
657                 if re.match ('[A-Z0-9]', left[0]):
658                         id = left[0]
659                         left= left[1:]
660                 while left[0] in 'uld0123456789+-.':
661                         left= left[1:]
662                         
663                 self.current_voice ().toggle_slur (id)
664                 return left
665
666         def parse_mumbo_jumbo (self,left):
667                 left = left[1:]
668                 while left and  left[0] <> '\\':
669                         left = left[1:]
670
671                 left  = left[1:]
672                 return left
673         def parsex (self,left):
674                 left = left[1:]
675                 while left[0] in DIGITS:
676                         left = left[1:]
677
678                 return left
679         
680         def parse_body (self, left):
681                 preamble = 1
682                 
683                 while left:
684                         c = left[0]
685                         if c == '%':
686                                 f = string.find (left, '\n')
687                                 if f < 0:
688                                         left = ''
689                                 left = left[f+1:]
690                         elif c == 'm':
691                                 left = left[1:]
692                                 m = re.match ('([o0-9]/[o0-9]/[o0-9]/[o0-9])', left)
693                                 if m:
694                                         nums = m.group (1)
695                                         left = left[len (nums):]
696                                         nums = map (string.atoi , nums)
697                                         self.current_voice ().add_nonchord (Meter (nums))
698                                         continue
699
700                                 m= re.match ('([0-9o]+)', left)
701                                 if m:
702                                         nums = m.group (1)
703                                         self.current_voice ().add_nonchord (Meter (map (string.atoi (nums))))
704                                         continue
705                                 
706                         elif left[0] in 'lh':
707                                 f = string.find (left, '\n')
708                                 if f <0 :
709                                         left = ''
710                                 else:
711                                         left = left[f+1:]
712                                         
713                                 f = string.find (left, '\n')
714                                 title = left[:f]
715                                 left=left[f+1:]
716                         elif c in 'Gzabcdefgr':
717                                 left = self.parse_note (left)
718                         elif c in DIGITS + 'n#-':
719                                 left = self.parse_basso_continuo (left)
720                         elif c in SPACE:
721                                 left = left[1:]
722                         elif c == 's':
723                                 left = self.parse_slur (left)
724                         elif c == '|':
725                                 left = self.parse_barcheck (left)
726                         elif c == 'o':
727                                 left = self.parse_ornament (left)
728                         elif c == 'x':
729                                 left = self.parsex (left)
730                         elif c == 'C':
731                                 self.current_staff().set_clef(str(left[1]))
732                                 left = left[2:]
733                         elif c == 'K':
734                                 left = self.parse_key (left)
735                         elif c in "[]":
736                                 left = self.parse_beams (left)
737                         elif left[:2] == "//":
738                                 self.current_staff().next_voice ()
739                                 left = left[2:]
740                         elif c == '/':
741                                 self.next_staff ()
742                                 left = left[1:]
743                         elif c == '\\':
744                                 left = self.parse_mumbo_jumbo(left)
745                         elif c == '\r':
746                                 left = left[1:]
747                         else:
748                                 sys.stderr.write ("""
749 Huh? Unknown directive `%s', before `%s'""" % (c, left[:20] ))
750                                 left = left[1:]
751
752         def dump (self):
753                 str = ''
754
755                 refs = ''
756                 for s in self.staffs:
757                         str = str +  s.dump ()
758                         refs = '\\' + s.idstring() + refs
759
760                 str = str + "\n\n\\score { <\n %s\n > }" % refs 
761                 return str
762                         
763
764         def parse (self,fn):
765                 ls = open (fn).readlines ()
766                 def subst(s):
767                         return re.sub ('%.*$', '', s)
768                 ls = map (subst, ls)
769                 ls = self.parse_header (ls)
770                 left = string.join (ls, ' ')
771
772                 print left
773                 self.parse_body (left)
774                 for c in self.staffs:
775                         c.calculate ()
776
777                 
778
779
780
781 def help ():
782         sys.stdout.write (
783 """Usage: pmx2ly [OPTION]... PMX-FILE
784
785 Convert PMX to LilyPond.
786
787 Options:
788   -h, --help          this help
789   -o, --output=FILE   set output filename to FILE
790   -v, --version       version information
791
792 PMX is a Musixtex preprocessor written by Don Simons, see
793 http://www.gmd.de/Misc/Music/musixtex/software/pmx/
794
795 Report bugs to bug-lilypond@gnu.org.
796
797 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
798 """)
799
800
801 def print_version ():
802         sys.stdout.write ("""pmx2ly (GNU LilyPond) %s
803
804 This is free software.  It is covered by the GNU General Public License,
805 and you are welcome to change it and/or distribute copies of it under
806 certain conditions.  Invoke as `midi2ly --warranty' for more information.
807
808 Copyright (c) 2000--2002 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
809 """ % version)
810 def identify():
811         sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
812
813
814
815 (options, files) = getopt.getopt (sys.argv[1:], 'vo:h', ['help','version', 'output='])
816 out_filename = None
817 for opt in options:
818         o = opt[0]
819         a = opt[1]
820         if o== '--help' or o == '-h':
821                 help ()
822                 sys.exit (0)
823         if o == '--version' or o == '-v':
824                 print_version ()
825                 sys.exit(0)
826                 
827         if o == '--output' or o == '-o':
828                 out_filename = a
829         else:
830                 print o
831                 raise getopt.error
832
833 identify()
834
835 for f in files:
836         if f == '-':
837                 f = ''
838
839         sys.stderr.write ('Processing `%s\'\n' % f)
840         e = Parser(f)
841         if not out_filename:
842                 out_filename = os.path.basename (re.sub ('(?i).pmx$', '.ly', f))
843                 
844         if out_filename == f:
845                 out_filename = os.path.basename (f + '.ly')
846                 
847         sys.stderr.write ('Writing `%s\'' % out_filename)
848         ly = e.dump()
849
850         
851         
852         fo = open (out_filename, 'w')
853         fo.write ('%% lily was here -- automatically converted by pmx2ly from %s\n' % f)
854         fo.write(ly)
855         fo.close ()
856         
857