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