]> git.donarmstrong.com Git - lilypond.git/blob - scripts/abc2ly.py
1b7b0687e588b04324067d2ece7418ba50a42ee0
[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
433         while str[0] == ' ':
434             str = str [1:]
435         
436         while str[0] == '>':
437                 str = str [1:]
438                 current_dots = current_dots + 1;
439                 parser_state.next_den = parser_state.next_den * 2
440
441         while str[0] == '<':
442                 str = str [1:]
443                 den = den * 2
444                 parser_state.next_dots = parser_state.next_dots + 1
445
446
447
448         try_dots = [3, 2, 1]
449         for d in try_dots:
450                 f = 1 << d
451                 multiplier = (2*f-1)
452                 if num % multiplier == 0 and den % f == 0:
453                         num = num / multiplier
454                         den = den / f
455                         current_dots = current_dots + d
456                 
457         return (str, num,den,current_dots)
458
459
460 def try_parse_rest (str, parser_state):
461         if not str or str[0] <> 'z':
462                 return str
463
464         str = str[1:]
465
466         (str, num,den,d) = parse_duration (str, parser_state)
467         voices_append ('r%s' % duration_to_mudela_duration ((num,den), default_len, d))
468
469         return str
470
471 def try_parse_articulation (str, state):
472         
473         if str[:1] =='.':
474                 state.next_articulation = state.next_articulation + '-.'
475                 str = str[1:]
476
477         if str[:1] =='~':
478                 state.next_articulation = state.next_articulation + '-\\trill'
479                 str = str[1:]
480                 
481         if str[:1] =='H':
482                 state.next_articulation = state.next_articulation + '-\\fermata'
483                 str = str[1:]
484
485         # s7m2 input doesnt care about spaces
486         if re.match('[ \t]*\(', str):
487                 str = string.lstrip (str)
488
489         slur_begin =0
490         while str[:1] =='(' and str[1] not in DIGITS:
491                 slur_begin = slur_begin + 1
492                 state.next_articulation = state.next_articulation + '('
493                 str = str[1:]
494
495         return str
496                 
497 # WAT IS ABC EEN ONTZETTENDE PROGRAMMEERPOEP  !
498 def try_parse_note (str, parser_state):
499         mud = ''
500
501         slur_begin =0
502         if not str:
503                 return str
504
505         articulation =''
506         acc = 0
507         if str[0] in '^=_':
508                 c = str[0]
509                 str = str[1:]
510                 if c == '^':
511                         acc = 1
512                 if c == '=':
513                         acc = 0
514                 if c == '_':
515                         acc = -1
516
517         octave = 0;
518         if str[0] in "ABCDEFG":
519                 str = string.lower (str[0]) + str[1:]
520                 octave = -1
521
522
523         notename = 0
524         if str[0] in "abcdefg":
525                 notename = (ord(str[0]) - ord('a') + 5)%7
526                 str = str[1:]
527         else:
528                 return str              # failed; not a note!
529
530         while str[0] == ',':
531                  octave = octave - 1
532                  str = str[1:]
533         while str[0] == '\'':
534                  octave = octave + 1
535                  str = str[1:]
536
537         (str, num,den,current_dots) = parse_duration (str, parser_state)
538
539
540         if re.match('[ \t]*\)', str):
541                 str = string.lstrip (str)
542         
543         slur_end =0
544         while str[:1] ==')':
545                 slur_end = slur_end + 1
546                 str = str[1:]
547
548         
549         if slur_end:
550                 voices_append ('%s' % ')' *slur_end )
551         voices_append ("%s%s%s" %
552                 (pitch_to_mudela_name (notename, acc + global_key[notename]),
553                                         octave_to_mudela_quotes (octave),
554                  duration_to_mudela_duration ((num,den), default_len, current_dots)))
555         if parser_state.next_articulation:
556                 articulation = articulation + parser_state.next_articulation
557                 parser_state.next_articulation = ''
558
559         voices_append (articulation)
560         if slur_begin:
561                 voices_append ('%s' % '(' * slur_begin )
562
563
564         return str
565
566 def junk_space (str):
567         while str and str[0] in '\t\n ':
568                 str = str[1:]
569
570         return str
571
572
573 def try_parse_guitar_chord (str, state):
574         if str[:1] =='"':
575                 str = str[1:]
576                 gc = ''
577                 while str and str[0] != '"':
578                         gc = gc + str[0]
579                         str = str[1:]
580                         
581                 if str:
582                         str = str[1:]
583
584                 state.next_articulation = "-\"%s\"" % gc
585         return str
586
587 def try_parse_escape (str):
588         if not str or str [0] != '\\':
589                 return str
590         
591         str = str[1:]
592         if str[:1] =='K':
593                 key_table = compute_key ()
594
595         return str
596
597 #
598 # |] thin-thick double bar line
599 # || thin-thin double bar line
600 # [| thick-thin double bar line
601 # :| left repeat
602 # |: right repeat
603 # :: left-right repeat
604 # |1 volta 1
605 # |2 volta 2
606 bar_dict = {
607 '|]' : '|.',
608 '||' : '||',
609 '[|' : '||',
610 ':|' : ':|',
611 '|:' : '|:',
612 '::' : '::',
613 '|1' : '|',
614 '|2' : '|',
615 ':|2' : ':|'
616 }
617
618
619 warn_about = ['|:', '::', ':|', '|1', ':|2', '|2']
620
621 def try_parse_bar (str,state):
622         bs = None
623
624         # first try the longer one
625         for trylen in [3,2]:
626                 if str[:trylen] and bar_dict.has_key (str[:trylen]):
627                         s = str[:trylen]
628                         bs = "\\bar \"%s\";" % bar_dict[s]
629                         if s in warn_about:
630                                 sys.stderr.write('Warning kludging for barline `%s\'\n' % s)
631                         str = str[trylen:]
632                         break
633
634         if str[:1] == '|':
635                 bs = '|\n'
636                 str = str[1:]
637         
638         if bs <> None:
639                 if state.parsing_tuplet:
640                         state.parsing_tuplet =0
641                         voices_append ('} ')
642                 
643                 voices_append (bs)
644
645         return str
646
647 def try_parse_tie (str):
648         if str[:1] =='-':
649                 str = str[1:]
650                 voices_append (' ~ ')
651         return str
652
653 def try_parse_chord_delims (str):
654         if str[:1] =='[':
655                 str = str[1:]
656                 voices_append ('<')
657
658         ch = ''
659         if str[:1] ==']':
660                 str = str[1:]
661                 ch = '>'
662
663         end = 0
664         while str[:1] ==')':
665                 end = end + 1
666                 str = str[1:]
667
668         
669         voices_append ("\\spanrequest \\stop \"slur\"" * end);
670         voices_append (ch)
671         return str
672
673 def try_parse_grace_delims (str):
674         if str[:1] =='{':
675                 str = str[1:]
676                 voices_append ('\\grace { ')
677
678         if str[:1] =='}':
679                 str = str[1:]
680                 voices_append ('}')
681
682         return str
683
684
685 happy_count = 100
686 def parse_file (fn):
687         f = open (fn)
688         ls = f.readlines ()
689
690         state = Parser_state ()
691         lineno = 0
692         sys.stderr.write ("Line ... ")
693         sys.stderr.flush ()
694         
695         for ln in ls:
696                 lineno = lineno + 1
697
698                 if not (lineno % happy_count):
699                         sys.stderr.write ('[%d]'% lineno)
700                         sys.stderr.flush ()
701                 if re.match ('^[\t ]*(%.*)?$', ln):
702                         continue
703                 m = re.match  ('^(.*?)%(.*)$',ln)
704                 if m:
705                         voices_append ('%% %s\n' % m.group(2))
706                         ln = m.group (1)
707
708                 orig_ln = ln
709                 
710                 ln = try_parse_header_line (ln)
711
712                 # Try nibbling characters off until the line doesn't change.
713                 prev_ln = ''
714                 while ln != prev_ln:
715                         prev_ln = ln
716                         ln = try_parse_chord_delims (ln)
717                         ln = try_parse_rest (ln, state)
718                         ln = try_parse_articulation (ln,state)
719                         ln = try_parse_note  (ln, state)
720                         ln = try_parse_bar (ln, state)
721                         ln = try_parse_tie (ln)
722                         ln = try_parse_escape (ln)
723                         ln = try_parse_guitar_chord (ln, state)
724                         ln = try_parse_tuplet_begin (ln, state)
725                         ln = try_parse_group_end (ln, state)
726                         ln = try_parse_grace_delims (ln)
727                         ln = junk_space (ln)
728
729                 if ln:
730                         msg = "%s: %d: Huh?  Don't understand\n" % (fn, lineno)
731                         sys.stderr.write (msg)
732                         left = orig_ln[0:-len (ln)]
733                         sys.stderr.write (left + '\n')
734                         sys.stderr.write (' ' *  len (left) + ln + '\n')        
735
736
737 def identify():
738         sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
739
740 def help ():
741         print r"""
742 Convert ABC to Mudela.
743
744 Usage: abc2ly [OPTION]... ABC-FILE
745
746 Options:
747   -h, --help          this help
748   -o, --output=FILE   set output filename to FILE
749 """
750
751
752 identify()
753 (options, files) = getopt.getopt (sys.argv[1:], 'o:h', ['help', 'output='])
754 out_filename = ''
755
756 for opt in options:
757         o = opt[0]
758         a = opt[1]
759         if o== '--help' or o == '-h':
760                 help ()
761         if o == '--output' or o == '-o':
762                 out_filename = a
763         else:
764                 print o
765                 raise getopt.error
766
767
768 header['tagline'] = 'Lily was here %s -- automatically converted from ABC' % version
769 for f in files:
770         if f == '-':
771                 f = ''
772
773         sys.stderr.write ('Parsing... [%s]\n' % f)
774         parse_file (f)
775
776         if not out_filename:
777                 out_filename = os.path.basename (os.path.splitext (f)[0]) + ".ly"
778         sys.stderr.write ('Ly output to: %s...' % out_filename)
779         outf = open (out_filename, 'w')
780
781 #       dump_global (outf)
782         dump_lyrics (outf)
783         dump_voices (outf)
784         dump_score (outf)
785         sys.stderr.write ('\n')
786