]> git.donarmstrong.com Git - lilypond.git/blob - scripts/abc2ly.py
release: 1.1.69
[lilypond.git] / scripts / abc2ly.py
1 #!@PYTHON@
2
3 # once upon a rainy monday afternoon.
4 #
5 #   ...
6 #
7 # (not finished.)
8 # ABC standard v1.6:  http://www.gre.ac.uk/~c.walshaw/abc2mtex/abc.txt
9
10
11
12 program_name = 'abc2ly'
13 version = '@TOPLEVEL_VERSION@'
14 if version == '@' + 'TOPLEVEL_VERSION' + '@':
15         version = '1.2.0'
16 import __main__
17 import getopt
18 import sys
19 import re
20 import string
21 import os
22 try:
23         import mpz
24 except:
25         sys.stderr.write ("This script needs Python 1.5.1\n")
26         sys.exit (1)
27
28
29 voice_idx_dict = {}
30
31
32 header = {}
33 lyrics = []
34 voices = []
35 current_voice_idx = -1
36 current_lyric_idx = -1
37
38 def select_voice (name):
39         if not voice_idx_dict.has_key (name):
40                 voices.append ('')              
41                 voice_idx_dict[name] = len (voices) -1
42         __main__.current_voice_idx =  voice_idx_dict[name]
43         
44 #       assert 0
45 # current_voice_idx >= 0
46
47 default_len = 8
48 global_key = [0] * 7                    # UGH
49 names = ["One", "Two", "Three"]
50 DIGITS='0123456789'
51 HSPACE=' \t'
52
53 def gcd (a, b):
54         while  a % b:
55                 a,b = b, a % b
56         return b
57         
58 class Rational:
59         def __init__ (self, n, d = 1):
60                 self.num = n
61                 self.den = d
62
63         def simplify (self):
64                 g = gcd (self.num, self.den)
65                 self.num = self.num /  g
66                 self.den = self.den /g
67                 if self.den < 0:
68                         self.den = - self.den
69                         self.num = - self.num
70
71         def __sub__ (self, other):
72                 pass
73         
74
75
76 def dump_header (outf,hdr):
77         outf.write ('\\header {')
78         ks = hdr.keys ()
79         ks.sort ()
80         for k in ks:
81                 outf.write ('\n%s = "%s";\n'% (k,hdr[k]))
82         outf.write ('}')
83
84 def dump_lyrics (outf):
85         for i in range (len (lyrics)):
86                 outf.write ("\nverse%s = \\lyrics {" % names [i])
87                 outf.write ("\n" + lyrics [i])
88                 outf.write ("\n}")
89
90 def dump_voices (outf):
91         ks = voice_idx_dict.keys()
92         ks.sort ()
93         for k in ks:
94                 outf.write ("\nvoice%s = \\notes {" % k)
95                 outf.write ("\n" + voices [voice_idx_dict[k]])
96                 outf.write ("\n}")
97         
98 def dump_score (outf):
99         outf.write (r"""\score{
100         \notes <
101 """)
102
103         ks  = voice_idx_dict.keys ();
104         ks.sort ()
105         for k in  ks:
106                 outf.write ("\n        \\context Staff=\"%s\" \\$voice%s " % (k,k))# ugh
107         for i in range (len (lyrics)):
108                 j = i
109                 if j >= len (voices):
110                         j = len (voices) - 1
111                 outf.write ("\n        \\context Lyrics=\"%s\" \\addlyrics \\$voice%s \\$verse%s " % 
112                         (names [i], names [j], names [i]))
113         outf.write ("\n    >")
114         dump_header (outf ,header)
115         outf.write (r"""
116 \paper {}
117 \midi {}
118 }""")
119
120 def set_default_length (s):
121         m =  re.search ('1/([0-9]+)', s)
122         if m:
123                 __main__.default_len = string.atoi ( m.group (1))
124
125 def set_default_len_from_time_sig (s):
126         m =  re.search ('([0-9]+)/([0-9]+)', s)
127         if m:
128                 n = string.atoi (m.group (1))
129                 d = string.atoi (m.group (2))
130                 if (n * 1.0 )/(d * 1.0) <  0.75:
131                         default_len =  16
132                 else:
133                         default_len = 8
134
135 def gulp_file(f):
136         try:
137                 i = open(f)
138                 i.seek (0, 2)
139                 n = i.tell ()
140                 i.seek (0,0)
141         except:
142                 sys.stderr.write ("can't open file: %s\n" % f)
143                 return ''
144         s = i.read (n)
145         if len (s) <= 0:
146                 sys.stderr.write ("gulped emty file: %s\n" % f)
147         i.close ()
148         return s
149
150
151 # pitch manipulation. Tuples are (name, alteration).
152 # 0 is (central) C. Alteration -1 is a flat, Alteration +1 is a sharp
153 # pitch in semitones. 
154 def semitone_pitch  (tup):
155         p =0
156
157         t = tup[0]
158         p = p + 12 * (t / 7)
159         t = t % 7
160         
161         if t > 2:
162                 p = p- 1
163                 
164         p = p + t* 2 + tup[1]
165         return p
166
167 def fifth_above_pitch (tup):
168         (n, a)  = (tup[0] + 4, tup[1])
169
170         difference = 7 - (semitone_pitch ((n,a)) - semitone_pitch (tup))
171         a = a + difference
172         
173         return (n,a)
174
175 def sharp_keys ():
176         p = (0,0)
177         l = []
178         k = 0
179         while 1:
180                 l.append (p)
181                 (t,a) = fifth_above_pitch (p)
182                 if semitone_pitch((t,a)) % 12 == 0:
183                         break
184
185                 p = (t % 7, a)
186         return l
187
188 def flat_keys ():
189         p = (0,0)
190         l = []
191         k = 0
192         while 1:
193                 l.append (p)
194                 (t,a) = quart_above_pitch (p)
195                 if semitone_pitch((t,a)) % 12 == 0:
196                         break
197
198                 p = (t % 7, a)
199         return l
200
201 def quart_above_pitch (tup):
202         (n, a)  = (tup[0] + 3, tup[1])
203
204         difference = 5 - (semitone_pitch ((n,a)) - semitone_pitch (tup))
205         a = a + difference
206         
207         return (n,a)
208
209
210 def compute_key (k):
211         k = string.lower (k)
212         intkey = (ord (k[0]) - ord('a') + 5) % 7
213         intkeyacc =0
214         k = k[1:]
215         
216         if k and k[0] == 'b':
217                 intkeyacc = -1
218                 k = k[1:]
219         elif  k and k[0] == '#':
220                 intkeyacc = 1
221                 k = k[1:]
222
223         keytup = (intkey, intkeyacc)
224         
225         sharp_key_seq = sharp_keys ()
226         flat_key_seq = flat_keys ()
227
228         accseq = None
229         accsign = 0
230         if keytup in sharp_key_seq:
231                 accsign = 1
232                 key_count = sharp_key_seq.index (keytup)
233                 accseq = map (lambda x: (4*x -1 ) % 7, range (1, key_count + 1))
234
235         elif keytup in flat_key_seq:
236                 accsign = -1
237                 key_count = flat_key_seq.index (keytup)
238                 accseq = map (lambda x: (3*x + 3 ) % 7, range (1, key_count + 1))
239         else:
240                 raise "Huh"
241         
242         key_table = [0] * 7
243         for a in accseq:
244                  key_table[a] = key_table[a] + accsign
245                 
246
247         return key_table
248
249 tup_lookup = {
250         '2' : '3/2',
251         '3' : '2/3',
252         '4' : '4/3',
253         '5' : '4/5',
254         '6' : '4/6',
255         '7' : '6/7',
256         '9' : '8/9',
257         }
258
259
260 def try_parse_tuplet_begin (str, state):
261         if re.match ('\([0-9]', str):
262                 dig = str[1]
263                 str = str[2:]
264                 state.parsing_tuplet = 1
265                 
266                 voices_append ("\\times %s {" % tup_lookup[dig])
267         return str
268
269 def  try_parse_group_end (str, state):
270         if str and str[0] in HSPACE:
271                 str = str[1:]
272                 if state.parsing_tuplet:
273                         state.parsing_tuplet = 0
274                         voices_append ("}")
275         return str
276
277 def header_append (key, a):
278         s = ''
279         if header.has_key (key):
280                 s = header[key] + "\n"
281         header [key] = s + a
282
283 def stuff_append (stuff, idx, a):
284         if not stuff:
285                 stuff.append ('')
286
287         v = stuff[idx]
288
289         #wordwrap
290         linelen = len (v) - string.rfind(v, '\n')
291         if linelen + len (a) > 80:
292                 v = v + '\n'
293         v = v + a + ' '
294         stuff [idx] = v
295
296
297
298 def voices_append(a):
299         if current_voice_idx < 0:
300                 select_voice ('default')
301
302         stuff_append (voices, current_voice_idx, a)
303
304 def lyrics_append(a):
305         stuff_append (lyrics, current_lyric_idx, a)
306
307
308 def try_parse_header_line (ln):
309         m = re.match ('^(.): *(.*)$', ln)
310
311         if m:
312                 g =m.group (1)
313                 a = m.group (2)
314                 a = re.sub ('"', '\\"', a)
315                 if g == 'T':
316                         header['title'] =  a
317                 if g == 'M':
318                         if a == 'C':
319                                 a = '4/4'
320 #                       global_voice_stuff.append ('\\time %s;' % a)
321                         set_default_len_from_time_sig (a)
322                         voices_append ('\\time %s;' % a)
323                 if g == 'K':
324                         __main__.global_key  =compute_key (a)# ugh.
325                         voices_append ('\\key %s;' % a)
326
327                 if g == 'O': 
328                         header ['origin'] = a
329                 if g == 'X': 
330                         header ['crossRefNumber'] = a
331                 if g == 'A':
332                         header ['area'] = a
333                 if g == 'H':
334                         header_append ('history', a)
335                 if g == 'B':
336                         header ['book'] = a
337                 if g == 'C':
338                         header ['composer'] = a
339                 if g == 'S':
340                         header ['subtitle'] = a
341                 if g == 'L':
342                         set_default_length (ln)
343                 if g == 'V':
344                         a = re.sub (' .*$', '', a)
345                         select_voice (a)
346                 if g == 'W':
347                         if not len (a):
348                                 lyrics.append ('')
349                         else:
350                                 lyrics_append (a);
351
352                 return ''
353         return ln
354
355 def pitch_to_mudela_name (name, acc):
356         s = ''
357         if acc < 0:
358                 s = 'es'
359                 acc = -acc
360         elif acc > 0:
361                 s = 'is'
362
363         if name > 4:
364                 name = name -7
365         return chr (name  + ord('c'))  + s * acc
366
367 def octave_to_mudela_quotes (o):
368         o = o + 2
369         s =''
370         if o < 0:
371                 o = -o
372                 s=','
373         else:
374                 s ='\''
375
376         return s * o
377
378 def parse_num (str):
379         durstr = ''
380         while str and str[0] in DIGITS:
381                 durstr = durstr + str[0]
382                 str = str[1:]
383
384         n = None
385         if durstr:
386                 n  =string.atoi (durstr) 
387         return (str,n)
388
389
390 def duration_to_mudela_duration  (multiply_tup, defaultlen, dots):
391         base = 1
392
393         # (num /  den)  / defaultlen < 1/base
394         while base * multiply_tup[0] < multiply_tup[1]:
395                 base = base * 2
396
397
398         return '%d%s' % ( base, '.'* dots)
399
400 class Parser_state:
401         def __init__ (self):
402                 self.next_articulation = ''
403                 self.next_dots = 0
404                 self.next_den = 1
405                 self.parsing_tuplet = 0
406
407
408
409 # return (str, num,den,dots) 
410 def parse_duration (str, parser_state):
411         num = 0
412         den = parser_state.next_den
413         parser_state.next_den = 1
414
415         (str, num) = parse_num (str)
416         if not num:
417                 num = 1
418         
419         if str[0] == '/':
420                 while str[:1] == '/':
421                         str= str[1:]
422                         d = 2
423                         if str[0] in DIGITS:
424                                 (str, d) =parse_num (str)
425
426                         den = den * d
427
428         den = den * default_len
429         
430         current_dots = parser_state.next_dots
431         parser_state.next_dots = 0
432         while str[0] == '>':
433                 str = str [1:]
434                 current_dots = current_dots + 1;
435                 parser_state.next_den = parser_state.next_den * 2
436         
437         while str[0] == '<':
438                 str = str [1:]
439                 den = den * 2
440                 parser_state.next_dots = parser_state.next_dots + 1
441
442
443
444         try_dots = [3, 2, 1]
445         for d in try_dots:
446                 f = 1 << d
447                 multiplier = (2*f-1)
448                 if num % multiplier == 0 and den % f == 0:
449                         num = num / multiplier
450                         den = den / f
451                         current_dots = current_dots + d
452                 
453         return (str, num,den,current_dots)
454
455
456 def try_parse_rest (str, parser_state):
457         if not str or str[0] <> 'z':
458                 return str
459
460         str = str[1:]
461
462         (str, num,den,d) = parse_duration (str, parser_state)
463         voices_append ('r%s' % duration_to_mudela_duration ((num,den), default_len, d))
464
465         return str
466
467 def try_parse_articulation (str, state):
468         
469         if str[:1] =='.':
470                 state.next_articulation = state.next_articulation + '-.'
471                 str = str[1:]
472
473         if str[:1] =='~':
474                 state.next_articulation = state.next_articulation + '-\\trill'
475                 str = str[1:]
476                 
477         # s7m2 input doesnt care about spaces
478         if re.match('[ \t]*\(', str):
479                 str = string.lstrip (str)
480
481         slur_begin =0
482         while str[:1] =='(' and str[1] not in DIGITS:
483                 slur_begin = slur_begin + 1
484                 state.next_articulation = state.next_articulation + '('
485                 str = str[1:]
486
487         return str
488                 
489 # WAT IS ABC EEN ONTZETTENDE PROGRAMMEERPOEP  !
490 def try_parse_note (str, parser_state):
491         mud = ''
492
493         slur_begin =0
494         if not str:
495                 return str
496
497         articulation =''
498         acc = 0
499         if str[0] in '^=_':
500                 c = str[0]
501                 str = str[1:]
502                 if c == '^':
503                         acc = 1
504                 if c == '=':
505                         acc = 0
506                 if c == '_':
507                         acc = -1
508
509         octave = 0;
510         if str[0] in "ABCDEFG":
511                 str = string.lower (str[0]) + str[1:]
512                 octave = -1
513
514
515         notename = 0
516         if str[0] in "abcdefg":
517                 notename = (ord(str[0]) - ord('a') + 5)%7
518                 str = str[1:]
519         else:
520                 return str              # failed; not a note!
521
522         while str[0] == ',':
523                  octave = octave - 1
524                  str = str[1:]
525         while str[0] == '\'':
526                  octave = octave + 1
527                  str = str[1:]
528
529         (str, num,den,current_dots) = parse_duration (str, parser_state)
530
531
532         if re.match('[ \t]*\)', str):
533                 str = string.lstrip (str)
534         
535         slur_end =0
536         while str[:1] ==')':
537                 slur_end = slur_end + 1
538                 str = str[1:]
539
540         
541         if slur_end:
542                 voices_append ('%s' % ')' *slur_end )
543         voices_append ("%s%s%s" %
544                 (pitch_to_mudela_name (notename, acc + global_key[notename]),
545                                         octave_to_mudela_quotes (octave),
546                  duration_to_mudela_duration ((num,den), default_len, current_dots)))
547         if parser_state.next_articulation:
548                 articulation = articulation + parser_state.next_articulation
549                 parser_state.next_articulation = ''
550
551         voices_append (articulation)
552         if slur_begin:
553                 voices_append ('%s' % '(' * slur_begin )
554
555
556         return str
557
558 def junk_space (str):
559         while str and str[0] in '\t\n ':
560                 str = str[1:]
561
562         return str
563
564
565 def try_parse_guitar_chord (str, state):
566         if str[:1] =='"':
567                 str = str[1:]
568                 gc = ''
569                 while str and str[0] != '"':
570                         gc = gc + str[0]
571                         str = str[1:]
572                         
573                 if str:
574                         str = str[1:]
575
576                 state.next_articulation = "-\"%s\"" % gc
577         return str
578
579 def try_parse_escape (str):
580         if not str or str [0] != '\\':
581                 return str
582         
583         str = str[1:]
584         if str[:1] =='K':
585                 key_table = compute_key ()
586
587         return str
588
589 #
590 # |] thin-thick double bar line
591 # || thin-thin double bar line
592 # [| thick-thin double bar line
593 # :| left repeat
594 # |: right repeat
595 # :: left-right repeat
596 # |1 volta 1
597 # |2 volta 2
598 bar_dict = {
599 '|]' : '|.',
600 '||' : '||',
601 '[|' : '||',
602 ':|' : ':|',
603 '|:' : '|:',
604 '::' : '::',
605 '|1' : '|',
606 '|2' : '|',
607 ':|2' : ':|'
608 }
609
610
611 warn_about = ['|:', '::', ':|', '|1', ':|2', '|2']
612
613 def try_parse_bar (str,state):
614         bs = None
615
616         # first try the longer one
617         for trylen in [3,2]:
618                 if str[:trylen] and bar_dict.has_key (str[:trylen]):
619                         s = str[:trylen]
620                         bs = "\\bar \"%s\";" % bar_dict[s]
621                         if s in warn_about:
622                                 sys.stderr.write('Warning kludging for barline `%s\'\n' % s)
623                         str = str[trylen:]
624                         break
625
626         if str[:1] == '|':
627                 bs = '|\n'
628                 str = str[1:]
629         
630         if bs <> None:
631                 if state.parsing_tuplet:
632                         state.parsing_tuplet =0
633                         voices_append ('} ')
634                 
635                 voices_append (bs)
636
637         return str
638
639 def try_parse_tie (str):
640         if str[:1] =='-':
641                 str = str[1:]
642                 voices_append (' ~ ')
643         return str
644
645 def try_parse_chord_delims (str):
646         if str[:1] =='[':
647                 str = str[1:]
648                 voices_append ('<')
649
650         ch = ''
651         if str[:1] ==']':
652                 str = str[1:]
653                 ch = '>'
654
655         end = 0
656         while str[:1] ==')':
657                 end = end + 1
658                 str = str[1:]
659
660         
661         voices_append ("\\spanrequest \\stop \"slur\"" * end);
662         voices_append (ch)
663         return str
664
665 def try_parse_grace_delims (str):
666         if str[:1] =='{':
667                 str = str[1:]
668                 voices_append ('\\grace { ')
669
670         if str[:1] =='}':
671                 str = str[1:]
672                 voices_append ('}')
673
674         return str
675
676
677 happy_count = 100
678 def parse_file (fn):
679         f = open (fn)
680         ls = f.readlines ()
681
682         state = Parser_state ()
683         lineno = 0
684         sys.stderr.write ("Line ... ")
685         sys.stderr.flush ()
686         
687         for ln in ls:
688                 lineno = lineno + 1
689
690                 if not (lineno % happy_count):
691                         sys.stderr.write ('[%d]'% lineno)
692                         sys.stderr.flush ()
693                 if re.match ('^[\t ]*(%.*)?$', ln):
694                         continue
695                 m = re.match  ('^(.*?)%(.*)$',ln)
696                 if m:
697                         voices_append ('%% %s\n' % m.group(2))
698                         ln = m.group (1)
699
700                 orig_ln = ln
701                 
702                 ln = try_parse_header_line (ln)
703
704                 # Try nibbling characters off until the line doesn't change.
705                 prev_ln = ''
706                 while ln != prev_ln:
707                         prev_ln = ln
708                         ln = try_parse_chord_delims (ln)
709                         ln = try_parse_rest (ln, state)
710                         ln = try_parse_articulation (ln,state)
711                         ln = try_parse_note  (ln, state)
712                         ln = try_parse_bar (ln, state)
713                         ln = try_parse_tie (ln)
714                         ln = try_parse_escape (ln)
715                         ln = try_parse_guitar_chord (ln, state)
716                         ln = try_parse_tuplet_begin (ln, state)
717                         ln = try_parse_group_end (ln, state)
718                         ln = try_parse_grace_delims (ln)
719                         ln = junk_space (ln)
720
721                 if ln:
722                         msg = "%s: %d: Huh?  Don't understand\n" % (fn, lineno)
723                         sys.stderr.write (msg)
724                         left = orig_ln[0:-len (ln)]
725                         sys.stderr.write (left + '\n')
726                         sys.stderr.write (' ' *  len (left) + ln + '\n')        
727
728
729 def identify():
730         sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
731
732 def help ():
733         print r"""
734 Convert ABC to Mudela.
735
736 Usage: abc2ly [OPTION]... ABC-FILE
737
738 Options:
739   -h, --help          this help
740   -o, --output=FILE   set output filename to FILE
741 """
742
743
744 identify()
745 (options, files) = getopt.getopt (sys.argv[1:], 'o:h', ['help', 'output='])
746 out_filename = ''
747
748 for opt in options:
749         o = opt[0]
750         a = opt[1]
751         if o== '--help' or o == '-h':
752                 help ()
753         if o == '--output' or o == '-o':
754                 out_filename = a
755         else:
756                 print o
757                 raise getopt.error
758
759
760 header['tagline'] = 'Lily was here %s -- automatically converted from ABC' % version
761 for f in files:
762         if f == '-':
763                 f = ''
764
765         sys.stderr.write ('Parsing... [%s]\n' % f)
766         parse_file (f)
767
768         if not out_filename:
769                 out_filename = os.path.basename (os.path.splitext (f)[0]) + ".ly"
770         sys.stderr.write ('Ly output to: %s...' % out_filename)
771         outf = open (out_filename, 'w')
772
773 #       dump_global (outf)
774         dump_lyrics (outf)
775         dump_voices (outf)
776         dump_score (outf)
777         sys.stderr.write ('\n')
778