release commit
[lilypond.git] / scripts / pmx2ly.py
blob1e3a26f0a0dd413c0e752e0dca049a1bae5c4a3d
1 #!@PYTHON@
3 # PMX is a Musixtex preprocessor written by Don Simons, see
4 # http://www.gmd.de/Misc/Music/musixtex/software/pmx/
6 # TODO:
7 # * block openings aren't parsed.
9 import os
10 import string
11 import sys
12 import re
13 import getopt
15 program_name = 'pmx2ly'
16 version = '@TOPLEVEL_VERSION@'
17 if version == '@' + 'TOPLEVEL_VERSION' + '@':
18 version = '(unknown version)' # uGUHGUHGHGUGH
21 def encodeint (i):
22 return chr ( i + ord ('A'))
25 actab = {-2: 'eses', -1: 'es', 0 : '', 1: 'is', 2:'isis'}
27 def pitch_to_lily_string (tup):
28 (o,n,a) = tup
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
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
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)
59 def rat_multiply (a,b):
60 (x,y) = a
61 (p,q) = b
63 return rat_simplify ((x*p, y*q))
65 def rat_divide (a,b):
66 (p,q) = b
67 return rat_multiply (a, (q,p))
69 tuplet_table = {
70 2: 3,
71 3: 2,
72 5: 4
76 def rat_add (a,b):
77 (x,y) = a
78 (p,q) = b
80 return rat_simplify ((x*q + p*y, y*q))
82 def rat_neg (a):
83 (p,q) = a
84 return (-p,q)
87 def rat_larger (a,b):
88 return rat_subtract (a, b )[0] > 0
90 def rat_subtract (a,b ):
91 return rat_add (a, rat_neg (b))
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
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)
109 class Barcheck :
110 def __init__ (self):
111 pass
112 def dump (self):
113 return '|\n'
116 class Meter :
117 def __init__ (self,nums):
118 self.nums = nums
119 def dump (self):
120 return ' %{ FIXME: meter change %} '
122 class Beam:
123 def __init__ (self, ch):
124 self.char = ch
125 def dump (self):
126 return self.char
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
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")
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):
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)
163 def last_chord (self):
164 return self.chords[-1]
166 def add_chord (self, ch):
167 self.chords.append (ch)
168 self.entries.append (ch)
169 def add_nonchord (self, nch):
170 self.entries.append (nch)
172 def idstring (self):
173 return 'staff%svoice%s ' % (encodeint (self.staff.number) , encodeint(self.number))
174 def dump (self):
175 str = ''
176 ln = ''
177 for e in self.entries:
178 next = ' ' + e.dump ()
179 if next[-1] == '\n':
180 str = str + ln + next
181 ln = ''
182 continue
184 if len (ln) +len (next) > 72:
185 str = str+ ln + '\n'
186 ln = ''
187 ln = ln + next
190 str = str + ln
191 id = self.idstring ()
193 str = '%s = \\notes { \n %s }\n '% (id, str)
194 return str
195 def calculate_graces (self):
196 lastgr = 0
197 lastc = None
198 for c in self.chords:
199 if c.grace and not lastgr:
200 c.chord_prefix = c.chord_prefix + '\\grace { '
201 elif not c.grace and lastgr:
202 lastc.chord_suffix = lastc.chord_suffix + ' } '
203 lastgr = c.grace
204 lastc = c
205 def calculate (self):
206 self.calculate_graces ()
207 for s in self.slurs:
208 s.calculate ()
210 class Clef:
211 def __init__ (self, cl):
212 self.type = cl
213 def dump(self):
214 return '\\clef %s' % self.type
216 class Key:
217 def __init__ (self, key):
218 self.type = key
219 def dump(self):
220 return '\\key %s' % self.type
222 clef_table = {
223 'b':'bass' ,
224 'r':'baritone',
225 'n':'tenor',
226 'a':'alto',
227 'm':'mezzosoprano',
228 's':'soprano',
229 't':'treble',
230 'f':'frenchviolin',
232 key_table = {
233 '+0':'c \major',
234 '+1':'g \major',
235 '+2':'d \major',
236 '+3':'a \major',
237 '+4':'e \major',
238 '+5':'b \major',
239 '+6':'fis \major',
240 '-1':'f \major',
241 '-2':'bes \major',
242 '-3':'ees \major',
243 '-4':'aes \major',
244 '-5':'des \major',
245 '-6':'ges \major'
247 class Staff:
248 def __init__ (self):
249 self.voices = (Voice (), Voice())
250 self.clef = None
251 self.instrument = 0
252 self.voice_idx = 0
253 self.number = None
254 self.key = 0
256 i = 0
257 for v in self.voices:
258 v.staff = self
259 v.number = i
260 i = i+1
261 def set_clef (self, letter):
262 if clef_table.has_key (letter):
263 clstr = clef_table[letter]
264 self.voices[0].add_nonchord (Clef (clstr))
265 else:
266 sys.stderr.write ("Clef type `%c' unknown\n" % letter)
268 def current_voice (self):
269 return self.voices[self.voice_idx]
270 def next_voice (self):
271 self.voice_idx = (self.voice_idx + 1)%len (self.voices)
273 def calculate (self):
274 for v in self.voices:
275 v.calculate ()
276 def idstring (self):
277 return 'staff%s' % encodeint (self.number)
278 def dump (self):
279 str = ''
281 refs = ''
282 for v in self.voices:
283 str = str + v.dump()
284 refs = refs + '\\' + v.idstring ()+ ' '
286 str = str + '\n\n%s = \\context Staff = %s \n << \n %s >>\n\n\n'% (self.idstring (), self.idstring (), refs)
287 return str
289 class Tuplet:
290 def __init__ (self, number, base, dots):
291 self.chords = []
292 self.number = number
293 self.replaces = tuplet_table[number]
294 self.base = base
295 self.dots = dots
297 length = (1,base)
298 if dots == 1:
299 length = rat_multiply (length, (3,2))
300 elif dots == 2:
301 length = rat_multiply (length, (7,4))
303 length = rat_multiply (length, (1,self.replaces))
305 (nb,nd) =rat_to_duration (length)
307 self.note_base = nb
308 self.note_dots = nd
310 def add_chord (self, ch):
311 ch.dots = self.note_dots
312 ch.basic_duration = self.note_base
313 self.chords.append (ch)
315 if len (self.chords) == 1:
316 ch.chord_prefix = '\\times %d/%d { ' % (self.replaces, self.number)
317 elif len (self.chords) == self.number:
318 ch.chord_suffix = ' }'
320 class Chord:
321 def __init__ (self):
322 self.pitches = []
323 self.dots = 0
324 self.basic_duration = 0
325 self.scripts = []
326 self.grace = 0
327 self.chord_prefix = ''
328 self.chord_suffix = ''
329 self.note_prefix = ''
330 self.note_suffix = ''
332 def dump (self):
333 str = ''
335 sd = ''
336 if self.basic_duration == 0.5:
337 sd = '\\breve'
338 else:
339 sd = '%d' % self.basic_duration
340 sd = sd + '.' * self.dots
341 for p in self.pitches:
342 if str:
343 str = str + ' '
344 str = str + pitch_to_lily_string (p)
346 if len (self.pitches) > 1:
347 str = '<%s>' % str
348 elif len (self.pitches) == 0:
349 str = 'r'
351 str = str + sd
352 for s in self.scripts:
353 str = str + '-' + s
355 str = self.note_prefix + str + self.note_suffix
356 str = self.chord_prefix + str + self.chord_suffix
358 return str
360 SPACE=' \t\n'
361 DIGITS ='0123456789'
362 basicdur_table = {
363 9: 0.5,
364 0: 0 ,
365 2: 2 ,
366 4: 4 ,
367 8: 8 ,
368 1: 16,
369 3: 32,
370 6: 64
374 ornament_table = {
375 't': '\\prall',
376 'm': '\\mordent',
377 'x': '"x"',
378 '+': '+',
379 'u': '"pizz"',
380 'p': '|',
381 '(': '"paren"',
382 ')': '"paren"',
383 'g': '"segno"',
384 '.': '.',
385 'fd': '\\fermata',
386 'f': '\\fermata',
387 '_': '-',
388 'T': '\\trill',
389 '>': '>',
390 '^': '^',
393 class Parser:
394 def __init__ (self, filename):
395 self.staffs = []
396 self.forced_duration = None
397 self.last_name = 0
398 self.last_oct = 0
399 self.tuplets_expected = 0
400 self.tuplets = []
401 self.last_basic_duration = 4
403 self.parse (filename)
405 def set_staffs (self, number):
406 self.staffs = map (lambda x: Staff (), range(0, number))
408 self.staff_idx = 0
410 i =0
411 for s in self.staffs:
412 s.number = i
413 i = i+1
414 def current_staff (self):
415 return self.staffs[self.staff_idx]
417 def current_voice (self):
418 return self.current_staff ().current_voice ()
420 def next_staff (self):
421 self.staff_idx = (self.staff_idx + 1)% len (self.staffs)
423 def parse_note (self, str):
424 name = None
425 ch = None
427 grace = 0
428 if str[0] == 'G':
429 grace = 1
430 str = str[1:]
432 if str[0] == 'z':
433 ch = self.current_voice().last_chord()
434 str = str[1:]
435 else:
436 ch = Chord ()
437 self.current_voice().add_chord (ch)
439 # what about 's'?
440 if str[0] <> 'r':
441 name = (ord (str[0]) - ord('a') + 5) % 7
443 str = str[1:]
445 ch.grace = ch.grace or grace
447 forced_duration = 0
448 alteration = 0
449 dots = 0
450 oct = None
451 durdigit = None
452 multibar = 0
453 tupnumber = 0
454 extra_oct = 0
455 while str[0] in 'dsfmnul0123456789.,+-':
456 c = str[0]
457 str = str[1:]
458 if c == 'f':
459 alteration = alteration -1
460 elif c == 'n':
461 alteration = 0
462 elif c == 'm':
463 multibar = 1
464 elif c == 's':
465 alteration = alteration +1
466 elif c == 'd':
467 dots = dots + 1
468 elif c in DIGITS and durdigit == None and \
469 self.tuplets_expected == 0:
470 durdigit = string.atoi (c)
471 elif c in DIGITS:
472 oct = string.atoi (c) - 3
473 elif c == '+':
474 extra_oct = extra_oct + 1
475 elif c == '-':
476 extra_oct = extra_oct - 1
477 elif c == '.':
478 dots = dots+ 1
479 forced_duration = 2
480 elif c == ',':
481 forced_duration = 2
483 if str[0] == 'x':
484 str = str[1:]
485 tupnumber = string.atoi (str[0])
486 str = str[1:]
487 str=re.sub (r'^n?f?[+-0-9.]+', '' , str)
490 if durdigit:
491 try:
492 basic_duration = basicdur_table[durdigit]
493 self.last_basic_duration = basic_duration
494 except KeyError:
495 sys.stderr.write ("""
496 Huh? expected duration, found %d Left was `%s'""" % (durdigit, str[:20]))
498 basic_duration = 4
499 else:
500 basic_duration = self.last_basic_duration
504 if name <> None and oct == None:
505 e = 0
506 if self.last_name < name and name -self.last_name > 3:
507 e = -1
508 elif self.last_name > name and self.last_name -name > 3:
509 e = 1
511 oct = self.last_oct +e + extra_oct
513 if name <> None:
514 self.last_oct = oct
515 self.last_name = name
517 if name <> None:
518 ch.pitches.append ((oct, name, alteration))
520 # do before adding to tuplet.
521 ch.basic_duration = basic_duration
522 ch.dots = dots
524 if forced_duration:
525 self.forced_duration = ch.basic_duration / forced_duration
527 if tupnumber:
528 tup =Tuplet (tupnumber, basic_duration, dots)
529 self.tuplets_expected = tupnumber
530 self.tuplets.append (tup)
532 if self.tuplets_expected > 0:
533 self.tuplets[-1].add_chord (ch)
534 self.tuplets_expected = self.tuplets_expected - 1
536 return str
537 def parse_basso_continuo (self, str):
538 while str[0] in DIGITS +'#n-':
539 scr = str[0]
541 if scr == '#':
542 scr = '\\\\textsharp'
544 if len(scr)>1 or scr not in DIGITS:
545 scr = '"%s"' % scr
547 self.current_voice().last_chord ().scripts.append (scr)
548 str=str[1:]
549 return str
550 def parse_beams (self,str):
551 c = str[0]
552 # self.current_voice().add_nonchord (Beam(c))
553 if str[0] == '[':
554 str = str[1:]
555 while str[0] in '+-0123456789':
556 str=str[1:]
557 else:
558 str = str[1:]
560 return str
562 def parse_key (self, str):
563 key = ""
564 #The key is changed by a string of the form K[+-]<num>[+-]<num>
565 #where the first number is the transposition and the second number is the
566 #new key signature. For now, we won't bother with the transposition.
567 if str[2] != '0':
568 sys.stderr.write("Transposition not implemented yet: ")
569 while str[0] in '+-0123456789':
570 str = str[1:]
571 else:
572 str=str[3:]
573 key = ''
574 while str[0] in '+-0123456789':
575 key=key + str[0]
576 str=str[1:]
577 keystr = key_table[key]
578 self.current_voice().add_nonchord (Key(keystr))
579 return(str)
582 def parse_header (self, ls):
583 def atonum(a):
584 if re.search('\\.', a):
585 return string.atof (a)
586 else:
587 return string.atoi (a)
589 number_count = 12
590 numbers = []
592 while len (numbers) < number_count:
593 opening = ls[0]
594 ls = ls[1:]
596 opening = re.sub ('[ \t\n]+', ' ', opening)
597 opening = re.sub ('^ ', '', opening)
598 opening = re.sub (' $', '', opening)
599 if opening == '':
600 continue
601 opening = string.split (opening, ' ')
603 numbers = numbers + map (atonum, opening)
605 (no_staffs, no_instruments, timesig_num, timesig_den, ptimesig_num,
606 esig_den, pickup_beats,keysig_number) = tuple (numbers[0:8])
607 (no_pages,no_systems, musicsize, fracindent) = tuple (numbers[8:])
609 # ignore this.
610 # opening = map (string.atoi, re.split ('[\t ]+', opening))
612 instruments = []
613 while len (instruments) < no_instruments:
614 instruments.append (ls[0])
615 ls = ls[1:]
617 l = ls[0]
618 ls = ls[1:]
620 self.set_staffs (no_staffs)
622 for s in self.staffs:
623 s.set_clef(l[0])
624 l = l[1:]
626 # dump path
627 ls = ls[1:]
629 # dump more ?
630 return ls
632 def parse_ornament (self, left):
633 left = left[1:]
634 e = self.current_voice ().last_chord ()
636 id = left[0]
637 left = left[1:]
638 if left[0] == 'd':
639 id = id +'d'
640 left = left [1:]
642 orn = '"orn"'
643 try:
644 orn = ornament_table[id]
645 except KeyError:
646 sys.stderr.write ("unknown ornament `%s'\n" % id)
648 e.scripts.append (orn)
649 return left
650 def parse_barcheck (self, left):
651 self.current_voice ().add_nonchord (Barcheck ())
653 return left [1:]
655 def parse_slur (self, left):
656 left = left[1:]
658 id = None
660 if re.match ('[A-Z0-9]', left[0]):
661 id = left[0]
662 left= left[1:]
663 while left[0] in 'uld0123456789+-.':
664 left= left[1:]
666 self.current_voice ().toggle_slur (id)
667 return left
669 def parse_mumbo_jumbo (self,left):
670 left = left[1:]
671 while left and left[0] <> '\\':
672 left = left[1:]
674 left = left[1:]
675 return left
676 def parsex (self,left):
677 left = left[1:]
678 while left[0] in DIGITS:
679 left = left[1:]
681 return left
683 def parse_body (self, left):
684 preamble = 1
686 while left:
687 c = left[0]
688 if c == '%':
689 f = string.find (left, '\n')
690 if f < 0:
691 left = ''
692 left = left[f+1:]
693 elif c == 'm':
694 left = left[1:]
695 m = re.match ('([o0-9]/[o0-9]/[o0-9]/[o0-9])', left)
696 if m:
697 nums = m.group (1)
698 left = left[len (nums):]
699 nums = map (string.atoi , nums)
700 self.current_voice ().add_nonchord (Meter (nums))
701 continue
703 m= re.match ('([0-9o]+)', left)
704 if m:
705 nums = m.group (1)
706 self.current_voice ().add_nonchord (Meter (map (string.atoi (nums))))
707 continue
709 elif left[0] in 'lh':
710 f = string.find (left, '\n')
711 if f <0 :
712 left = ''
713 else:
714 left = left[f+1:]
716 f = string.find (left, '\n')
717 title = left[:f]
718 left=left[f+1:]
719 elif c in 'Gzabcdefgr':
720 left = self.parse_note (left)
721 elif c in DIGITS + 'n#-':
722 left = self.parse_basso_continuo (left)
723 elif c in SPACE:
724 left = left[1:]
725 elif c == 's':
726 left = self.parse_slur (left)
727 elif c == '|':
728 left = self.parse_barcheck (left)
729 elif c == 'o':
730 left = self.parse_ornament (left)
731 elif c == 'x':
732 left = self.parsex (left)
733 elif c == 'C':
734 self.current_staff().set_clef(str(left[1]))
735 left = left[2:]
736 elif c == 'K':
737 left = self.parse_key (left)
738 elif c in "[]":
739 left = self.parse_beams (left)
740 elif left[:2] == "//":
741 self.current_staff().next_voice ()
742 left = left[2:]
743 elif c == '/':
744 self.next_staff ()
745 left = left[1:]
746 elif c == '\\':
747 left = self.parse_mumbo_jumbo(left)
748 elif c == '\r':
749 left = left[1:]
750 else:
751 sys.stderr.write ("""
752 Huh? Unknown directive `%s', before `%s'""" % (c, left[:20] ))
753 left = left[1:]
755 def dump (self):
756 str = ''
758 refs = ''
759 for s in self.staffs:
760 str = str + s.dump ()
761 refs = '\\' + s.idstring() + refs
763 str = str + "\n\n\\score { <<\n %s\n >> }" % refs
764 return str
767 def parse (self,fn):
768 ls = open (fn).readlines ()
769 def subst(s):
770 return re.sub ('%.*$', '', s)
772 ls = map (subst, ls)
773 ls = filter (lambda x: x <> '\n', ls)
774 ls = self.parse_header (ls)
775 left = string.join (ls, ' ')
777 # print left
778 self.parse_body (left)
779 for c in self.staffs:
780 c.calculate ()
786 def help ():
787 sys.stdout.write (
788 """Usage: pmx2ly [OPTIONS]... PMX-FILE
790 Convert PMX to LilyPond.
792 Options:
793 -h, --help print this help
794 -o, --output=FILE set output filename to FILE
795 -v, --version shown version information
797 PMX is a Musixtex preprocessor written by Don Simons, see
798 http://www.gmd.de/Misc/Music/musixtex/software/pmx/.
800 Report bugs to bug-lilypond@gnu.org.
802 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>.
804 """)
807 def print_version ():
808 sys.stdout.write ("""pmx2ly (GNU LilyPond) %s
810 This is free software. It is covered by the GNU General Public License,
811 and you are welcome to change it and/or distribute copies of it under
812 certain conditions. Invoke as `midi2ly --warranty' for more information.
814 Copyright (c) 2000--2003 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
815 """ % version)
816 def identify():
817 sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
821 (options, files) = getopt.getopt (sys.argv[1:], 'vo:h', ['help','version', 'output='])
822 out_filename = None
823 for opt in options:
824 o = opt[0]
825 a = opt[1]
826 if o== '--help' or o == '-h':
827 help ()
828 sys.exit (0)
829 if o == '--version' or o == '-v':
830 print_version ()
831 sys.exit(0)
833 if o == '--output' or o == '-o':
834 out_filename = a
835 else:
836 print o
837 raise getopt.error
839 identify()
841 for f in files:
842 if f == '-':
843 f = ''
845 sys.stderr.write ('Processing `%s\'\n' % f)
846 e = Parser(f)
847 if not out_filename:
848 out_filename = os.path.basename (re.sub ('(?i).pmx$', '.ly', f))
850 if out_filename == f:
851 out_filename = os.path.basename (f + '.ly')
853 sys.stderr.write ('Writing `%s\'' % out_filename)
854 ly = e.dump()
858 fo = open (out_filename, 'w')
859 fo.write ('%% lily was here -- automatically converted by pmx2ly from %s\n' % f)
860 fo.write(ly)
861 fo.close ()