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