release commit
[lilypond.git] / scripts / pmx2ly.py
blob412906db09cefafd35d2115a870553b01b315569
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_prefix
140 else:
141 sys.stderr.write ("\nOrphaned slur")
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):
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)
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)
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
182 if len (ln) +len (next) > 72:
183 str = str+ ln + '\n'
184 ln = ''
185 ln = ln + next
188 str = str + ln
189 id = self.idstring ()
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 ()
208 class Clef:
209 def __init__ (self, cl):
210 self.type = cl
211 def dump(self):
212 return '\\clef %s' % self.type
214 class Key:
215 def __init__ (self, key):
216 self.type = key
217 def dump(self):
218 return '\\key %s' % self.type
220 clef_table = {
221 'b':'bass' ,
222 'r':'baritone',
223 'n':'tenor',
224 'a':'alto',
225 'm':'mezzosoprano',
226 's':'soprano',
227 't':'treble',
228 'f':'frenchviolin',
230 key_table = {
231 '+0':'c \major',
232 '+1':'g \major',
233 '+2':'d \major',
234 '+3':'a \major',
235 '+4':'e \major',
236 '+5':'b \major',
237 '+6':'fis \major',
238 '-1':'f \major',
239 '-2':'bes \major',
240 '-3':'ees \major',
241 '-4':'aes \major',
242 '-5':'des \major',
243 '-6':'ges \major'
245 class Staff:
246 def __init__ (self):
247 self.voices = (Voice (), Voice())
248 self.clef = None
249 self.instrument = 0
250 self.voice_idx = 0
251 self.number = None
252 self.key = 0
254 i = 0
255 for v in self.voices:
256 v.staff = self
257 v.number = i
258 i = i+1
259 def set_clef (self, letter):
260 clstr = clef_table[letter]
261 self.voices[0].add_nonchord (Clef (clstr))
264 def current_voice (self):
265 return self.voices[self.voice_idx]
266 def next_voice (self):
267 self.voice_idx = (self.voice_idx + 1)%len (self.voices)
269 def calculate (self):
270 for v in self.voices:
271 v.calculate ()
272 def idstring (self):
273 return 'staff%s' % encodeint (self.number)
274 def dump (self):
275 str = ''
277 refs = ''
278 for v in self.voices:
279 str = str + v.dump()
280 refs = refs + '\\' + v.idstring ()+ ' '
282 str = str + '\n\n%s = \\context Staff = %s \n < \n %s >\n\n\n'% (self.idstring (), self.idstring (), refs)
283 return str
285 class Tuplet:
286 def __init__ (self, number, base, dots):
287 self.chords = []
288 self.number = number
289 self.replaces = tuplet_table[number]
290 self.base = base
291 self.dots = dots
293 length = (1,base)
294 if dots == 1:
295 length = rat_multiply (length, (3,2))
296 elif dots == 2:
297 length = rat_multiply (length, (7,4))
299 length = rat_multiply (length, (1,self.replaces))
301 (nb,nd) =rat_to_duration (length)
303 self.note_base = nb
304 self.note_dots = nd
306 def add_chord (self, ch):
307 ch.dots = self.note_dots
308 ch.basic_duration = self.note_base
309 self.chords.append (ch)
311 if len (self.chords) == 1:
312 ch.chord_prefix = '\\times %d/%d { ' % (self.replaces, self.number)
313 elif len (self.chords) == self.number:
314 ch.chord_suffix = ' }'
316 class Chord:
317 def __init__ (self):
318 self.pitches = []
319 self.dots = 0
320 self.basic_duration = 0
321 self.scripts = []
322 self.grace = 0
323 self.chord_prefix = ''
324 self.chord_suffix = ''
325 self.note_prefix = ''
326 self.note_suffix = ''
328 def dump (self):
329 str = ''
331 sd = ''
332 if self.basic_duration == 0.5:
333 sd = '\\breve'
334 else:
335 sd = '%d' % self.basic_duration
336 sd = sd + '.' * self.dots
337 for p in self.pitches:
338 if str:
339 str = str + ' '
340 str = str + pitch_to_lily_string (p) + sd
342 for s in self.scripts:
343 str = str + '-' + s
345 str = self.note_prefix +str + self.note_suffix
347 if len (self.pitches) > 1:
348 str = '<%s>' % str
349 elif len (self.pitches) == 0:
350 str = 'r' + sd
352 str = self.chord_prefix + str + self.chord_suffix
354 return str
356 SPACE=' \t\n'
357 DIGITS ='0123456789'
358 basicdur_table = {
359 9: 0.5,
360 0: 0 ,
361 2: 2 ,
362 4: 4 ,
363 8: 8 ,
364 1: 16,
365 3: 32,
366 6: 64
370 ornament_table = {
371 't': '\\prall',
372 'm': '\\mordent',
373 'x': '"x"',
374 '+': '+',
375 'u': '"pizz"',
376 'p': '|',
377 '(': '"paren"',
378 ')': '"paren"',
379 'g': '"segno"',
380 '.': '.',
381 'fd': '\\fermata',
382 'f': '\\fermata',
383 '_': '-',
384 'T': '\\trill',
385 '>': '>',
386 '^': '^',
389 class Parser:
390 def __init__ (self, filename):
391 self.staffs = []
392 self.forced_duration = None
393 self.last_name = 0
394 self.last_oct = 0
395 self.tuplets_expected = 0
396 self.tuplets = []
397 self.last_basic_duration = 4
399 self.parse (filename)
401 def set_staffs (self, number):
402 self.staffs = map (lambda x: Staff (), range(0, number))
404 self.staff_idx = 0
406 i =0
407 for s in self.staffs:
408 s.number = i
409 i = i+1
410 def current_staff (self):
411 return self.staffs[self.staff_idx]
413 def current_voice (self):
414 return self.current_staff ().current_voice ()
416 def next_staff (self):
417 self.staff_idx = (self.staff_idx + 1)% len (self.staffs)
419 def parse_note (self, str):
420 name = None
421 ch = None
423 grace = 0
424 if str[0] == 'G':
425 grace = 1
426 str = str[1:]
428 if str[0] == 'z':
429 ch = self.current_voice().last_chord()
430 str = str[1:]
431 else:
432 ch = Chord ()
433 self.current_voice().add_chord (ch)
435 # what about 's'?
436 if str[0] <> 'r':
437 name = (ord (str[0]) - ord('a') + 5) % 7
439 str = str[1:]
441 ch.grace = ch.grace or grace
443 forced_duration = 0
444 alteration = 0
445 dots = 0
446 oct = None
447 durdigit = None
448 multibar = 0
449 tupnumber = 0
450 extra_oct = 0
451 while str[0] in 'dsfmnul0123456789.,+-':
452 c = str[0]
453 str = str[1:]
454 if c == 'f':
455 alteration = alteration -1
456 elif c == 'n':
457 alteration = 0
458 elif c == 'm':
459 multibar = 1
460 elif c == 's':
461 alteration = alteration +1
462 elif c == 'd':
463 dots = dots + 1
464 elif c in DIGITS and durdigit == None and \
465 self.tuplets_expected == 0:
466 durdigit = string.atoi (c)
467 elif c in DIGITS:
468 oct = string.atoi (c) - 3
469 elif c == '+':
470 extra_oct = extra_oct + 1
471 elif c == '-':
472 extra_oct = extra_oct - 1
473 elif c == '.':
474 dots = dots+ 1
475 forced_duration = 2
476 elif c == ',':
477 forced_duration = 2
479 if str[0] == 'x':
480 str = str[1:]
481 tupnumber = string.atoi (str[0])
482 str = str[1:]
483 str=re.sub (r'^n?f?[+-0-9.]+', '' , str)
486 if durdigit:
487 try:
488 basic_duration = basicdur_table[durdigit]
489 self.last_basic_duration = basic_duration
490 except KeyError:
491 sys.stderr.write ("""
492 Huh? expected duration, found %d Left was `%s'""" % (durdigit, str[:20]))
494 basic_duration = 4
495 else:
496 basic_duration = self.last_basic_duration
500 if name <> None and oct == None:
501 e = 0
502 if self.last_name < name and name -self.last_name > 3:
503 e = -1
504 elif self.last_name > name and self.last_name -name > 3:
505 e = 1
507 oct = self.last_oct +e + extra_oct
509 if name <> None:
510 self.last_oct = oct
511 self.last_name = name
513 if name <> None:
514 ch.pitches.append ((oct, name, alteration))
516 # do before adding to tuplet.
517 ch.basic_duration = basic_duration
518 ch.dots = dots
520 if forced_duration:
521 self.forced_duration = ch.basic_duration / forced_duration
523 if tupnumber:
524 tup =Tuplet (tupnumber, basic_duration, dots)
525 self.tuplets_expected = tupnumber
526 self.tuplets.append (tup)
528 if self.tuplets_expected > 0:
529 self.tuplets[-1].add_chord (ch)
530 self.tuplets_expected = self.tuplets_expected - 1
532 return str
533 def parse_basso_continuo (self, str):
534 while str[0] in DIGITS +'#n-':
535 scr = str[0]
537 if scr == '#':
538 scr = '\\\\textsharp'
540 if len(scr)>1 or scr not in DIGITS:
541 scr = '"%s"' % scr
543 self.current_voice().last_chord ().scripts.append (scr)
544 str=str[1:]
545 return str
546 def parse_beams (self,str):
547 c = str[0]
548 # self.current_voice().add_nonchord (Beam(c))
549 if str[0] == '[':
550 str = str[1:]
551 while str[0] in '+-0123456789':
552 str=str[1:]
553 else:
554 str = str[1:]
556 return str
558 def parse_key (self, str):
559 key = ""
560 #The key is changed by a string of the form K[+-]<num>[+-]<num>
561 #where the first number is the transposition and the second number is the
562 #new key signature. For now, we won't bother with the transposition.
563 if str[2] != '0':
564 sys.stderr.write("Transposition not implemented yet: ")
565 while str[0] in '+-0123456789':
566 str = str[1:]
567 else:
568 str=str[3:]
569 key = ''
570 while str[0] in '+-0123456789':
571 key=key + str[0]
572 str=str[1:]
573 keystr = key_table[key]
574 self.current_voice().add_nonchord (Key(keystr))
575 return(str)
578 def parse_header (self, ls):
579 def atonum(a):
580 if re.search('\\.', a):
581 return string.atof (a)
582 else:
583 return string.atoi (a)
585 number_count = 12
586 numbers = []
588 while len (numbers) < number_count:
589 opening = ls[0]
590 ls = ls[1:]
592 opening = re.sub ('[ \t\n]+', ' ', opening)
593 opening = re.sub ('^ ', '', opening)
594 opening = re.sub (' $', '', opening)
595 if opening == '':
596 continue
597 opening = string.split (opening, ' ')
599 numbers = numbers + map (atonum, opening)
601 (no_staffs, no_instruments, timesig_num, timesig_den, ptimesig_num,
602 ptimesig_den, pickup_beats,keysig_number) = tuple (numbers[0:8])
603 (no_pages,no_systems, musicsize, fracindent) = tuple (numbers[8:])
605 # ignore this.
606 # opening = map (string.atoi, re.split ('[\t ]+', opening))
608 instruments = []
609 while len (instruments) < no_instruments:
610 instruments.append (ls[0])
611 ls = ls[1:]
613 l = ls[0]
614 ls = ls[1:]
616 self.set_staffs (no_staffs)
618 for s in self.staffs:
619 s.set_clef(l[0])
620 l = l[1:]
622 # dump path
623 ls = ls[1:]
625 # dump more ?
626 return ls
628 def parse_ornament (self, left):
629 left = left[1:]
630 e = self.current_voice ().last_chord ()
632 id = left[0]
633 left = left[1:]
634 if left[0] == 'd':
635 id = id +'d'
636 left = left [1:]
638 orn = '"orn"'
639 try:
640 orn = ornament_table[id]
641 except KeyError:
642 sys.stderr.write ("unknown ornament `%s'\n" % id)
644 e.scripts.append (orn)
645 return left
646 def parse_barcheck (self, left):
647 self.current_voice ().add_nonchord (Barcheck ())
649 return left [1:]
651 def parse_slur (self, left):
652 left = left[1:]
654 id = None
656 if re.match ('[A-Z0-9]', left[0]):
657 id = left[0]
658 left= left[1:]
659 while left[0] in 'uld0123456789+-.':
660 left= left[1:]
662 self.current_voice ().toggle_slur (id)
663 return left
665 def parse_mumbo_jumbo (self,left):
666 left = left[1:]
667 while left and left[0] <> '\\':
668 left = left[1:]
670 left = left[1:]
671 return left
672 def parsex (self,left):
673 left = left[1:]
674 while left[0] in DIGITS:
675 left = left[1:]
677 return left
679 def parse_body (self, left):
680 preamble = 1
682 while left:
683 c = left[0]
684 if c == '%':
685 f = string.find (left, '\n')
686 if f < 0:
687 left = ''
688 left = left[f+1:]
689 elif c == 'm':
690 left = left[1:]
691 m = re.match ('([o0-9]/[o0-9]/[o0-9]/[o0-9])', left)
692 if m:
693 nums = m.group (1)
694 left = left[len (nums):]
695 nums = map (string.atoi , nums)
696 self.current_voice ().add_nonchord (Meter (nums))
697 continue
699 m= re.match ('([0-9o]+)', left)
700 if m:
701 nums = m.group (1)
702 self.current_voice ().add_nonchord (Meter (map (string.atoi (nums))))
703 continue
705 elif left[0] in 'lh':
706 f = string.find (left, '\n')
707 if f <0 :
708 left = ''
709 else:
710 left = left[f+1:]
712 f = string.find (left, '\n')
713 title = left[:f]
714 left=left[f+1:]
715 elif c in 'Gzabcdefgr':
716 left = self.parse_note (left)
717 elif c in DIGITS + 'n#-':
718 left = self.parse_basso_continuo (left)
719 elif c in SPACE:
720 left = left[1:]
721 elif c == 's':
722 left = self.parse_slur (left)
723 elif c == '|':
724 left = self.parse_barcheck (left)
725 elif c == 'o':
726 left = self.parse_ornament (left)
727 elif c == 'x':
728 left = self.parsex (left)
729 elif c == 'C':
730 self.current_staff().set_clef(str(left[1]))
731 left = left[2:]
732 elif c == 'K':
733 left = self.parse_key (left)
734 elif c in "[]":
735 left = self.parse_beams (left)
736 elif left[:2] == "//":
737 self.current_staff().next_voice ()
738 left = left[2:]
739 elif c == '/':
740 self.next_staff ()
741 left = left[1:]
742 elif c == '\\':
743 left = self.parse_mumbo_jumbo(left)
744 elif c == '\r':
745 left = left[1:]
746 else:
747 sys.stderr.write ("""
748 Huh? Unknown directive `%s', before `%s'""" % (c, left[:20] ))
749 left = left[1:]
751 def dump (self):
752 str = ''
754 refs = ''
755 for s in self.staffs:
756 str = str + s.dump ()
757 refs = '\\' + s.idstring() + refs
759 str = str + "\n\n\\score { <\n %s\n > }" % refs
760 return str
763 def parse (self,fn):
764 ls = open (fn).readlines ()
765 def subst(s):
766 return re.sub ('%.*$', '', s)
767 ls = map (subst, ls)
768 ls = self.parse_header (ls)
769 left = string.join (ls, ' ')
771 print left
772 self.parse_body (left)
773 for c in self.staffs:
774 c.calculate ()
780 def help ():
781 sys.stdout.write (
782 """Usage: pmx2ly [OPTION]... PMX-FILE
784 Convert PMX to LilyPond.
786 Options:
787 -h, --help this help
788 -o, --output=FILE set output filename to FILE
789 -v, --version version information
791 PMX is a Musixtex preprocessor written by Don Simons, see
792 http://www.gmd.de/Misc/Music/musixtex/software/pmx/
794 Report bugs to bug-lilypond@gnu.org.
796 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
797 """)
800 def print_version ():
801 sys.stdout.write ("""pmx2ly (GNU LilyPond) %s
803 This is free software. It is covered by the GNU General Public License,
804 and you are welcome to change it and/or distribute copies of it under
805 certain conditions. Invoke as `midi2ly --warranty' for more information.
807 Copyright (c) 2000--2002 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
808 """ % version)
809 def identify():
810 sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
814 (options, files) = getopt.getopt (sys.argv[1:], 'vo:h', ['help','version', 'output='])
815 out_filename = None
816 for opt in options:
817 o = opt[0]
818 a = opt[1]
819 if o== '--help' or o == '-h':
820 help ()
821 sys.exit (0)
822 if o == '--version' or o == '-v':
823 print_version ()
824 sys.exit(0)
826 if o == '--output' or o == '-o':
827 out_filename = a
828 else:
829 print o
830 raise getopt.error
832 identify()
834 for f in files:
835 if f == '-':
836 f = ''
838 sys.stderr.write ('Processing `%s\'\n' % f)
839 e = Parser(f)
840 if not out_filename:
841 out_filename = os.path.basename (re.sub ('(?i).pmx$', '.ly', f))
843 if out_filename == f:
844 out_filename = os.path.basename (f + '.ly')
846 sys.stderr.write ('Writing `%s\'' % out_filename)
847 ly = e.dump()
851 fo = open (out_filename, 'w')
852 fo.write ('%% lily was here -- automatically converted by pmx2ly from %s\n' % f)
853 fo.write(ly)
854 fo.close ()