lilypond-1.3.128
[lilypond.git] / scripts / pmx2ly.py
blobb8d289b861612497507797b70ed512dd57e8dc3c
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 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',
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
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))
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)
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 = ''
254 refs = ''
255 for v in self.voices:
256 str = str + v.dump()
257 refs = refs + '\\' + v.idstring ()+ ' '
259 str = str + '\n\n%s = \\context Staff = %s \n < \n %s >\n\n\n'% (self.idstring (), self.idstring (), refs)
260 return str
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
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))
276 length = rat_multiply (length, (1,self.replaces))
278 (nb,nd) =rat_to_duration (length)
280 self.note_base = nb
281 self.note_dots = nd
283 def add_chord (self, ch):
284 ch.dots = self.note_dots
285 ch.basic_duration = self.note_base
286 self.chords.append (ch)
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 = ' }'
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 = ''
305 def dump (self):
306 str = ''
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
319 for s in self.scripts:
320 str = str + '-' + s
322 str = self.note_prefix +str + self.note_suffix
324 if len (self.pitches) > 1:
325 str = '<%s>' % str
326 elif len (self.pitches) == 0:
327 str = 'r' + sd
329 str = self.chord_prefix + str + self.chord_suffix
331 return str
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
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 '^': '^',
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
376 self.parse (filename)
378 def set_staffs (self, number):
379 self.staffs = map (lambda x: Staff (), range(0, number))
381 self.staff_idx = 0
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]
390 def current_voice (self):
391 return self.current_staff ().current_voice ()
393 def next_staff (self):
394 self.staff_idx = (self.staff_idx + 1)% len (self.staffs)
396 def parse_note (self, str):
397 name = None
398 ch = None
400 grace = 0
401 if str[0] == 'G':
402 grace = 1
403 str = str[1:]
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 if str[0] <> 'r':
412 name = (ord (str[0]) - ord('a') + 5) % 7
414 str = str[1:]
416 ch.grace = ch.grace or grace
418 forced_duration = 0
419 alteration = 0
420 dots = 0
421 oct = None
422 durdigit = None
423 multibar = 0
424 tupnumber = 0
425 extra_oct = 0
426 while str[0] in 'dsfmnul0123456789.,+-':
427 c = str[0]
428 str = str[1:]
429 if c == 'f':
430 alteration = alteration -1
431 elif c == 'n':
432 alteration = 0
433 elif c == 'm':
434 multibar = 1
435 elif c == 's':
436 alteration = alteration +1
437 elif c == 'd':
438 dots = dots + 1
439 elif c in DIGITS and durdigit == None and \
440 self.tuplets_expected == 0:
441 durdigit = string.atoi (c)
442 elif c in DIGITS:
443 oct = string.atoi (c) - 3
444 elif c == '+':
445 extra_oct = extra_oct + 1
446 elif c == '-':
447 extra_oct = extra_oct - 1
448 elif c == '.':
449 dots = dots+ 1
450 forced_duration = 2
451 elif c == ',':
452 forced_duration = 2
454 if str[0] == 'x':
455 str = str[1:]
456 tupnumber = string.atoi (str[0])
457 str = str[1:]
458 str=re.sub (r'^n?f?[+-0-9.]+', '' , str)
461 if durdigit:
462 try:
463 basic_duration = basicdur_table[durdigit]
464 self.last_basic_duration = basic_duration
465 except KeyError:
466 sys.stderr.write ("""
467 Huh? expected duration, found %d Left was `%s'""" % (durdigit, str[:20]))
469 basic_duration = 4
470 else:
471 basic_duration = self.last_basic_duration
475 if name <> None and oct == None:
476 e = 0
477 if self.last_name < name and name -self.last_name > 3:
478 e = -1
479 elif self.last_name > name and self.last_name -name > 3:
480 e = 1
482 oct = self.last_oct +e + extra_oct
484 if name <> None:
485 self.last_oct = oct
486 self.last_name = name
488 if name <> None:
489 ch.pitches.append ((oct, name, alteration))
491 # do before adding to tuplet.
492 ch.basic_duration = basic_duration
493 ch.dots = dots
495 if forced_duration:
496 self.forced_duration = ch.basic_duration / forced_duration
498 if tupnumber:
499 tup =Tuplet (tupnumber, basic_duration, dots)
500 self.tuplets_expected = tupnumber
501 self.tuplets.append (tup)
503 if self.tuplets_expected > 0:
504 self.tuplets[-1].add_chord (ch)
505 self.tuplets_expected = self.tuplets_expected - 1
507 return str
508 def parse_basso_continuo (self, str):
509 while str[0] in DIGITS +'#n-':
510 scr = str[0]
512 if scr == '#':
513 scr = '\\\\textsharp'
515 if len(scr)>1 or scr not in DIGITS:
516 scr = '"%s"' % scr
518 self.current_voice().last_chord ().scripts.append (scr)
519 str=str[1:]
520 return str
521 def parse_beams (self,str):
522 c = str[0]
523 # self.current_voice().add_nonchord (Beam(c))
524 if str[0] == '[':
525 str = str[1:]
526 while str[0] in '+-0123456789':
527 str=str[1:]
528 else:
529 str = str[1:]
531 return str
534 def parse_header (self, ls):
535 while ls[0][0] == '%':
536 ls = ls[1:]
538 opening = ls[0]
539 ls = ls[1:]
541 opening = re.sub ('^[ \t]+', '', opening)
542 opening = re.sub ('[ \t]+$', '', opening)
543 opening = re.split ('[\t ]+', opening)
545 (no_staffs, no_instruments, timesig_num,timesig_den, ptimesig_num,
546 ptimesig_den, pickup_beats,keysig_number) = opening
547 (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]))
548 try:
549 pickup_beats = string.atoi (pickup_beats)
550 except ValueError:
551 pickup_beats = string.atof (pickup_beats)
554 while ls[0][0] == '%':
555 ls = ls[1:]
557 opening = ls[0]
558 ls = ls[1:]
560 # ignore this.
561 # opening = map (string.atoi, re.split ('[\t ]+', opening))
562 # (no_pages,no_systems, musicsize, fracindent) = tuple (opening)
564 instruments = []
565 while len (instruments) < no_instruments:
566 instruments.append (ls[0])
567 ls = ls[1:]
570 while ls[0][0] == '%':
571 ls = ls[1:]
573 l = ls[0]
574 ls = ls[1:]
576 self.set_staffs (no_staffs)
578 for s in self.staffs:
579 s.set_clef(l[0])
580 l = l[1:]
582 # dump path
583 while ls[0][0] == '%':
584 ls = ls[1:]
586 ls = ls[1:]
588 # dump more ?
589 return ls
591 def parse_ornament (self, left):
592 left = left[1:]
593 e = self.current_voice ().last_chord ()
595 id = left[0]
596 left = left[1:]
597 if left[0] == 'd':
598 id = id +'d'
599 left = left [1:]
601 orn = '"orn"'
602 try:
603 orn = ornament_table[id]
604 except KeyError:
605 sys.stderr.write ("unknown ornament `%s'\n" % id)
607 e.scripts.append (orn)
608 return left
609 def parse_barcheck (self, left):
610 self.current_voice ().add_nonchord (Barcheck ())
612 return left [1:]
614 def parse_slur (self, left):
615 left = left[1:]
617 id = None
619 if re.match ('[A-Z0-9]', left[0]):
620 id = left[0]
621 left= left[1:]
622 while left[0] in 'uld0123456789+-.':
623 left= left[1:]
625 self.current_voice ().toggle_slur (id)
626 return left
628 def parse_mumbo_jumbo (self,left):
629 left = left[1:]
630 while left and left[0] <> '\\':
631 left = left[1:]
633 left = left[1:]
634 return left
635 def parsex (self,left):
636 left = left[1:]
637 while left[0] in DIGITS:
638 left = left[1:]
640 return left
642 def parse_body (self, left):
643 preamble = 1
645 while left:
646 c = left[0]
647 if c == '%':
648 f = string.find (left, '\n')
649 if f < 0:
650 left = ''
651 left = left[f+1:]
652 elif c == 'm':
653 left = left[1:]
654 m = re.match ('([o0-9]/[o0-9]/[o0-9]/[o0-9])', left)
655 if m:
656 nums = m.group (1)
657 left = left[len (nums):]
658 nums = map (string.atoi , nums)
659 self.current_voice ().add_nonchord (Meter (nums))
660 continue
662 m= re.match ('([0-9o]+)', left)
663 if m:
664 nums = m.group (1)
665 self.current_voice ().add_nonchord (Meter (map (string.atoi (nums))))
666 continue
668 elif left[0] in 'lh':
669 f = string.find (left, '\n')
670 if f <0 :
671 left = ''
672 else:
673 left = left[f+1:]
675 f = string.find (left, '\n')
676 title = left[:f]
677 left=left[f+1:]
678 elif c in 'Gzabcdefgr':
679 left = self.parse_note (left)
680 elif c in DIGITS + 'n#-':
681 left = self.parse_basso_continuo (left)
682 elif c in SPACE:
683 left = left[1:]
684 elif c == 's':
685 left = self.parse_slur (left)
686 elif c == '|':
687 left = self.parse_barcheck (left)
688 elif c == 'o':
689 left = self.parse_ornament (left)
690 elif c == 'x':
691 left = self.parsex (left)
692 elif c in "[]":
693 left = self.parse_beams (left)
694 elif left[:2] == "//":
695 self.current_staff().next_voice ()
696 left = left[2:]
697 elif c == '/':
698 self.next_staff ()
699 left = left[1:]
700 elif c == '\\':
701 left = self.parse_mumbo_jumbo(left)
702 elif c == '\r':
703 left = left[1:]
704 else:
705 sys.stderr.write ("""
706 Huh? Unknown directive `%s', before `%s'""" % (c, left[:20] ))
707 left = left[1:]
709 def dump (self):
710 str = ''
712 refs = ''
713 for s in self.staffs:
714 str = str + s.dump ()
715 refs = '\\' + s.idstring() + refs
717 str = str + "\n\n\\score { <\n %s\n > }" % refs
718 return str
721 def parse (self,fn):
722 ls = open (fn).readlines ()
723 ls = self.parse_header (ls)
724 left = string.join (ls, ' ')
726 print left
727 self.parse_body (left)
728 for c in self.staffs:
729 c.calculate ()
735 def help ():
736 sys.stdout.write (
737 """Usage: pmx2ly [OPTION]... PMX-FILE
739 Convert PMX to LilyPond.
741 Options:
742 -h, --help this help
743 -o, --output=FILE set output filename to FILE
744 -v, --version version information
746 PMX is a Musixtex preprocessor written by Don Simons, see
747 http://www.gmd.de/Misc/Music/musixtex/software/pmx/
749 Report bugs to bug-gnu-music@gnu.org.
751 Written by Han-Wen Nienhuys <hanwen@cs.uu.nl>
752 """)
755 def print_version ():
756 sys.stdout.write ("""pmx2ly (GNU LilyPond) %s
758 This is free software. It is covered by the GNU General Public License,
759 and you are welcome to change it and/or distribute copies of it under
760 certain conditions. Invoke as `midi2ly --warranty' for more information.
762 Copyright (c) 2000 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
763 """ % version)
764 def identify():
765 sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
769 (options, files) = getopt.getopt (sys.argv[1:], 'vo:h', ['help','version', 'output='])
770 out_filename = None
771 for opt in options:
772 o = opt[0]
773 a = opt[1]
774 if o== '--help' or o == '-h':
775 help ()
776 sys.exit (0)
777 if o == '--version' or o == '-v':
778 print_version ()
779 sys.exit(0)
781 if o == '--output' or o == '-o':
782 out_filename = a
783 else:
784 print o
785 raise getopt.error
787 identify()
789 for f in files:
790 if f == '-':
791 f = ''
793 sys.stderr.write ('Processing `%s\'\n' % f)
794 e = Parser(f)
795 if not out_filename:
796 out_filename = os.path.basename (re.sub ('(?i).pmx$', '.ly', f))
798 if out_filename == f:
799 out_filename = os.path.basename (f + '.ly')
801 sys.stderr.write ('Writing `%s\'' % out_filename)
802 ly = e.dump()
806 fo = open (out_filename, 'w')
807 fo.write ('%% lily was here -- automatically converted by pmx2ly from %s\n' % f)
808 fo.write(ly)
809 fo.close ()