]> git.donarmstrong.com Git - lilypond.git/blobdiff - scripts/etf2ly.py
patch::: 1.3.96.jcn9
[lilypond.git] / scripts / etf2ly.py
index 292e983fb823bde772234d2070bffe81848b3439..563e0308dcb55dae5a4a54d3fb659cfeac1e3a0f 100644 (file)
 #  * slurs
 #  * lyrics
 #  * articulation
-# 
+#  * grace notes
+#  * tuplets
+
 
 # todo:
 #  * slur/stem directions
 #  * voices (2nd half of frame?)
 #  * more intelligent lyrics
 #  * beams (better use autobeam?)
+#  * more robust: try entertainer.etf (freenote)
+#  * dynamics
+#  * automatic `deletion' of invalid items
+#
+
 
 program_name = 'etf2ly'
 version = '@TOPLEVEL_VERSION@'
@@ -31,10 +38,17 @@ import re
 import string
 import os
 
-finale_clefs= ['treble', 'alto', 'tenor', 'bass', 'percussion', 'treble8vb', 'bass8vb', 'baritone']
+finale_clefs= ['treble', 'alto', 'tenor', 'bass', 'percussion', 'treble_8', 'bass_8', 'baritone']
 
 def lily_clef (fin):
-       return finale_clefs[fin]
+       try:
+               return finale_clefs[fin]
+       except IndexError:
+               sys.stderr.write ( '\nHuh? Found clef number %d\n' % fin)
+
+       return 'treble'
+       
+       
 
 def gulp_file(f):
        return open (f).read ()
@@ -82,6 +96,36 @@ def find_scale (transposition):
        trscale = map(lambda x, k=transposition: transpose(x, k), cscale)
 
        return trscale
+def EDU_to_duration (edu):
+       log = 1
+       d = 4096
+       while d > edu:
+               d = d >> 1
+               log = log << 1
+
+       edu = edu - d
+       dots = 0
+       if edu == d /2:
+               dots = 1
+       elif edu == d*3/4:
+               dots = 2
+       return (log, dots)      
+
+def rat_to_lily_duration (rat):
+       (n,d) = rat
+
+       basedur = 1
+       while d and  d % 2 == 0:
+               basedur = basedur << 1
+               d = d >> 1
+
+       str = 's%d' % basedur
+       if n <> 1:
+               str = str + '*%d' % n
+       if d <> 1:
+               str = str + '/%d' % d
+
+       return str
 
 def gcd (a,b):
        if b == 0:
@@ -121,6 +165,8 @@ def rat_neg (a):
        (p,q) = a
        return (-p,q)
 
+
+
 def rat_subtract (a,b ):
        return rat_add (a, rat_neg (b))
 
@@ -140,6 +186,44 @@ def lily_notename (tuple2):
        return nn
 
 
+class Tuplet:
+       def __init__ (self, number):
+               self.start_note = number
+               self.finale = []
+
+       def append_finale (self, fin):
+               self.finale.append (fin)
+
+       def factor (self):
+               n = self.finale[0][2]*self.finale[0][3]
+               d = self.finale[0][0]*self.finale[0][1]
+               return rat_simplify( (n, d))
+       
+       def dump_start (self):
+               return '\\times %d/%d { ' % self.factor ()
+       
+       def dump_end (self):
+               return ' }'
+
+       def calculate (self, chords):
+               edu_left = self.finale[0][0] * self.finale[0][1]
+
+               startch = chords[self.start_note]
+               c = startch
+               while c and edu_left:
+                       c.tuplet = self
+                       if c == startch:
+                               c.chord_prefix = self.dump_start () + c.chord_prefix 
+
+                       if not c.grace:
+                               edu_left = edu_left - c.EDU_duration ()
+                       if edu_left == 0:
+                               c.chord_suffix = c.chord_suffix+ self.dump_end ()
+                       c = c.next
+
+               if edu_left:
+                       sys.stderr.write ("\nHuh? Tuplet starting at entry %d was too short." % self.start_note)
+               
 class Slur:
        def __init__ (self, number):
                self.number = number
@@ -151,11 +235,18 @@ class Slur:
        def calculate (self, chords):
                startnote = self.finale[0][5]
                endnote = self.finale[3][2]
+               try:
+                       cs = chords[startnote]
+                       ce = chords[endnote]
 
-               cs = chords[startnote]
-               cs.note_suffix = '(' + cs.note_suffix 
-               ce = chords[endnote]
-               ce.prefix = ce.prefix + ')'
+                       if not cs or not ce:
+                               raise IndexError
+                       
+                       cs.note_suffix = '(' + cs.note_suffix 
+                       ce.note_prefix = ce.note_prefix + ')'
+               except IndexError:
+                       sys.stderr.write ("""\nHuh? Slur no %d between (%d,%d), with %d notes""" % (self.number,  startnote, endnote, len (chords)))
+                                        
                
 class Global_measure:
        def __init__ (self, number):
@@ -166,6 +257,9 @@ class Global_measure:
 
                self.finale = []
 
+       def __str__ (self):
+               return `self.finale `
+       
        def set_timesig (self, finale):
                (beats, fdur) = finale
                (log, dots) = EDU_to_duration (fdur)
@@ -185,6 +279,9 @@ articulation_dict ={
        11: '\\prall',
        12: '\\mordent',
        8: '\\fermata',
+       4: '^',
+       1: '.',
+       3: '>',
        18: '"arp"' , # arpeggio
 };
 
@@ -198,6 +295,8 @@ class Articulation:
                try:
                        a = articulation_dict[self.type]
                except KeyError:
+                       sys.stderr.write ("\nUnknown articulation no. %d on note no. %d" % (self.type, self.notenumber))
+                       sys.stderr.write ("\nPlease add an entry to articulation_dict in the Python source")
                        a = '"art"'
                        
                c.note_suffix = '-' + a + c.note_suffix
@@ -248,24 +347,35 @@ class Verse:
 class Measure:
        def __init__(self, no):
                self.number = no
-               self.frames = []
+               self.frames = [0] * 4
                self.flags = 0
                self.clef = 0
                self.finale = []
                self.global_measure = None
+               self.staff = None
+               self.valid = 1
                
        def add_finale_entry (self, entry):
                self.finale.append (entry)
 
+       def valid (self):
+               return self.valid
        def calculate (self):
-               f0  = self.finale[0]
-               f1 = self.finale[1]
-               
-               self.clef = string.atoi (f0[0])
-               self.flags = string.atoi (f0[1])
-               fs = map (string.atoi, list (f0[2:]) + [f1[0]])
+               fs = []
+
+               if len (self.finale) < 2:
+                       fs = self.finale[0]
+                       fs = map (string.atoi, list (fs))
+                       self.clef = fs[1]
+                       self.frames = [fs[0]]
+               else:
+                       fs = self.finale[0] + self.finale[1]
+                       
+                       fs = map (string.atoi, list (fs))
+                       self.clef = fs[0]
+                       self.flags = fs[1]
+                       self.frames = fs[2:]
 
-               self.frames = fs
 
 class Frame:
        def __init__ (self, finale):
@@ -280,21 +390,43 @@ class Frame:
        def set_measure (self, m):
                self.measure = m
 
+       def calculate (self):
+
+               # do grace notes.
+               lastch = None
+               for c in self.chords:
+                       if c.grace and (lastch == None or (not lastch.grace)):
+                               c.chord_prefix = r'\grace {' + c.chord_prefix
+                       elif not c.grace and lastch and lastch.grace:
+                               lastch.chord_suffix = lastch.chord_suffix + ' } '
+
+                       lastch = c
+                       
+
+               
        def dump (self):
-               str = ''
+               str = '%% FR(%d)\n' % self.number
                left = self.measure.global_measure.length ()
+
+               
+               ln = ''
                for c in self.chords:
-                       str = str + c.ly_string () + ' '
+                       add = c.ly_string () + ' '
+                       if len (ln) + len(add) > 72:
+                               str = str + ln + '\n'
+                               ln = ''
+                       ln = ln + add
                        left = rat_subtract (left, c.length ())
+
+               str = str + ln 
+               
                if left[0] < 0:
-                       print self.number
-                       print self.start, self.end
-                       print left
-                       raise 'bla'
+                       sys.stderr.write ("""\nHuh? Going backwards in frame no %d, start/end (%d,%d)""" % (self.number, self.start, self.end))
+                       left = (0,1)
                if left[0]:
-                       str = str + 's*%d/%d' % left
-                       
-               str = str + '\n'
+                       str = str + rat_to_lily_duration (left)
+
+               str = str + '  | \n'
                return str
                
 def encodeint (i):
@@ -310,7 +442,10 @@ class Staff:
                        self.measures = self.measures + [None]* (1 + no - len (self.measures))
 
                if self.measures[no] == None:
-                       self.measures [no] = Measure (no)
+                       m = Measure (no)
+                       self.measures [no] =m
+                       m.staff = self
+
 
                return self.measures[no]
        def staffid (self):
@@ -325,24 +460,30 @@ class Staff:
                last_clef = None
                gap = (0,1)
                for m in self.measures[1:]:
+                       if not m or not m.valid:
+                               continue # ugh.
+                       
                        g = m.global_measure
                        e = ''
-                       if last_key <> g.keysignature:
+                       if g and last_key <> g.keysignature:
                                e = e + "\\key %s \\major; " % lily_notename (g.keysignature)
                                last_key = g.keysignature
-                       if last_time <> g.timesig :
+                       if g and last_time <> g.timesig :
                                e = e + "\\time %d/%d; " % g.timesig
                                last_time = g.timesig
+
+                       
                        if last_clef <> m.clef :
-                               e = e + '\\clef %s;' % lily_clef (m.clef)
+                               e = e + '\\clef "%s";' % lily_clef (m.clef)
                                last_clef = m.clef
                        if e:
                                if gap <> (0,1):
-                                       k = k +' s1*%d/%d \n ' % gap
+                                       k = k +' ' + rat_to_lily_duration (gap) + '\n'
                                gap = (0,1)
                                k = k + e
-                       
-                       gap = rat_add (gap, g.length ())
+                               
+                       if g:
+                               gap = rat_add (gap, g.length ())
 
                                
                k = '%sglobal = \\notes  { %s }\n\n ' % (self.staffid (), k)
@@ -359,16 +500,31 @@ class Staff:
                        first_frame = None
                        gap = (0,1)
                        for m in self.measures[1:]:
-                               fr = m.frames[x]
+                               if not m or not m.valid:
+                                       sys.stderr.write ("Skipping non-existant measure")
+                                       continue
+
+                               fr = None
+                               try:
+                                       fr = m.frames[x]
+                               except IndexError:
+                                       
+                                       sys.stderr.write ("Skipping nonexistent frame")
+                                       laystr = laystr + "% FOOBAR ! \n"
+                                       print laystr
                                if fr:
                                        first_frame = fr
                                        if gap <> (0,1):
-                                               laystr = laystr +'} s1*%d/%d {\n ' % gap
+                                               laystr = laystr +'} %s {\n ' % rat_to_lily_duration (gap)
                                                gap = (0,1)
                                        laystr = laystr + fr.dump ()
                                else:
-                                       gap = rat_add (gap, m.global_measure.length ())
-
+                                       if m.global_measure :
+                                               gap = rat_add (gap, m.global_measure.length ())
+                                       else:
+                                               sys.stderr.write ( \
+                                                       "No global measure for staff %d measure %d\n"
+                                                       % (self.number, m.number))
                        if first_frame:
                                l = self.layerid (x)
                                laystr = '%s =  \\notes { { %s } }\n\n' % (l, laystr)
@@ -386,20 +542,7 @@ class Staff:
                return str
 
                                
-def EDU_to_duration (edu):
-       log = 1
-       d = 4096
-       while d > edu:
-               d = d >> 1
-               log = log << 1
 
-       edu = edu - d
-       dots = 0
-       if edu == d /2:
-               dots = 1
-       elif edu == d*3/4:
-               dots = 2
-       return (log, dots)      
 
 class Chord:
        def __init__ (self, finale_entry):
@@ -413,6 +556,8 @@ class Chord:
                self.note_suffix = ''
                self.chord_suffix = ''
                self.chord_prefix = ''
+               self.tuplet = None
+               self.grace = 0
                
        def measure (self):
                if not self.frame:
@@ -420,19 +565,36 @@ class Chord:
                return self.frame.measure
 
        def length (self):
+               if self.grace:
+                       return (0,1)
+               
                l = (1, self.duration[0])
 
                d = 1 << self.duration[1]
 
                dotfact = rat_subtract ((2,1), (1,d))
-               return rat_multiply (dotfact, l)
+               mylen =  rat_multiply (dotfact, l)
+
+               if self.tuplet:
+                       mylen = rat_multiply (mylen, self.tuplet.factor())
+               return mylen
                
        def number (self):
                return self.finale[0][0]
+
+       def EDU_duration (self):
+               return self.finale[0][3]
        def set_duration (self):
-               ((no, prev, next, dur, pos, entryflag, extended, follow),
-                notelist) = self.finale
-               self.duration = EDU_to_duration(dur)
+               self.duration = EDU_to_duration(self.EDU_duration ())
+       def calculate (self):
+               self.find_realpitch ()
+               self.set_duration ()
+
+               flag = self.finale[0][5]
+               if Chord.GRACE_MASK & flag:
+                       self.grace = 1
+               
+               
        def find_realpitch (self):
                
                ((no, prev, next, dur, pos, entryflag, extended, follow), notelist) = self.finale
@@ -463,6 +625,8 @@ class Chord:
                
        REST_MASK = 0x40000000L
        TIE_START_MASK = 0x40000000L
+       GRACE_MASK = 0x00800000L
+       
        def ly_string (self):
                s = ''
 
@@ -511,14 +675,18 @@ IMre = re.compile (r"""^\^IM\(([0-9-]+),([0-9-]+)\) ([0-9-]+) ([0-9-]+) ([0-9-]+
 vere = re.compile(r"""^\^(ve|ch|se)\(([0-9-]+),([0-9-]+)\) ([0-9-]+) ([0-9-]+) ([0-9-]+) ([0-9-]+) ([0-9-]+)""")
 versere = re.compile(r"""^\^verse\(([0-9]+)\)(.*)\^end""")
 
+TPre = re.compile(r"""^\^TP\(([0-9]+),([0-9]+)\) *([0-9-]+) ([0-9-]+) ([0-9-]+) ([0-9-]+)""")
+
+
 class Etf_file:
        def __init__ (self, name):
                self.measures = [None]
                self.entries = [None]
                self.chords = [None]
                self.frames = [None]
+               self.tuplets = [None]
                self.staffs = [None]
-               self.slurs = [None]
+               self.slur_dict = {}
                self.articulations = [None]
                self.syllables = [None]
                self.verses = [None]
@@ -555,7 +723,17 @@ class Etf_file:
                        bn = string.atoi (m.group (1))
                        where = string.atoi (m.group (2)) / 1024.0
                return m
+       def try_TP(self, l):
+               m = TPre.match (l)
+               if m:
+                       (nil, num) = map (string.atoi, (m.groups ()[0:2]))
+                       entries = map (string.atoi, (m.groups ()[2:]))
+
+                       if self.tuplets[-1] == None or num <> self.tuplets[-1].start_note:
+                               self.tuplets.append (Tuplet (num))
 
+                       self.tuplets[-1].append_finale (entries)
+                       
        def try_IM (self, l):
                m = IMre.match (l)
                if m:
@@ -596,9 +774,13 @@ class Etf_file:
                          = tuple (map (string.atoi, [no,prev,next,dur,pos,extended,follow]))
 
                        entryflag = string.atol (entryflag,16)
-                       assert (no==len (self.entries))
+                       if len (self.entries) <= no:
+                               # missing entries seem to be quite common.
+                               # we fill'em up with None.
+                               self.entries = self.entries + [None] * (no - len (self.entries) + 1)
+                                       
                        current_entry = ((no, prev, next, dur, pos, entryflag, extended, follow), [])
-                       self.entries.append (current_entry)
+                       self.entries[no] = current_entry
                return m
 
        def try_Sx(self,l):
@@ -606,12 +788,16 @@ class Etf_file:
                if m:
                        slurno = string.atoi (m.group (1))
 
-                       if len (self.slurs) == slurno:
-                               self.slurs.append (Slur (slurno))
+                       sl = None
+                       try:
+                               sl = self.slur_dict[slurno]
+                       except KeyError:
+                               sl = Slur (slurno)
+                               self.slur_dict[slurno] = sl
 
                        params = list (m.groups ()[1:])
                        params = map (string.atoi, params)
-                       self.slurs[-1].append_entry (params)
+                       sl.append_entry (params)
 
                return m        
        def try_GF(self, l):
@@ -632,9 +818,13 @@ class Etf_file:
                if m:
                        (frameno, startnote, endnote, foo, bar) = m.groups ()
                        (frameno, startnote, endnote)  = tuple (map (string.atoi, [frameno, startnote, endnote]))
-                       self.frames.append (Frame ((frameno, startnote, endnote)))
+                       if len (self.frames) <= frameno:
+                               self.frames = self.frames + [None]  * (frameno - len(self.frames) + 1)
+                       
+                       self.frames[frameno] = Frame ((frameno, startnote, endnote))
                        
                return m
+       
        def try_MS (self, l):
                m = MSre.match (l)
                if m:
@@ -660,8 +850,12 @@ class Etf_file:
        def parse (self, name):
                sys.stderr.write ('parsing ...')
                sys.stderr.flush ()
+
+               gulp = open (name).read ()
+
+               gulp = re.sub ('[\n\r]+', '\n',  gulp)
+               ls = string.split (gulp, '\n')
                
-               ls = open (name).readlines ()
                for l in ls:
                        m = None
                        if not m: 
@@ -678,6 +872,8 @@ class Etf_file:
                                m = self.try_IM (l)
                        if not m:
                                m = self.try_Sx (l)
+                       if not m:
+                               m = self.try_TP (l)
                        if not m:
                                m = self.try_verse (l)
 
@@ -691,9 +887,16 @@ class Etf_file:
                                continue
                        mno = 1
                        for m in st.measures[1:]:
-                               m.global_measure = self.measures[mno]
+                               if not m:
+                                       continue
+                               
                                m.calculate()
-
+                               try:
+                                       m.global_measure = self.measures[mno]
+                               except IndexError:
+                                       sys.stderr.write ("Non-existent global measure %d" % mno)
+                                       continue
+                               
                                frame_obj_list = [None]
                                for frno in m.frames:
                                        fr = self.frames[frno]
@@ -712,10 +915,17 @@ class Etf_file:
                                mno = mno + 1
 
                for c in self.chords[1:]:
-                       c.find_realpitch ()
-                       c.set_duration ()
+                       if c:
+                               c.calculate()
+
+               for f in self.frames[1:]:
+                       if f:
+                               f.calculate ()
                        
-               for s in self.slurs [1:]:
+               for t in self.tuplets[1:]:
+                       t.calculate (self.chords)
+                       
+               for s in self.slur_dict.values():
                        s.calculate (self.chords)
                for s in self.articulations[1:]:
                        s.calculate (self.chords)
@@ -723,7 +933,15 @@ class Etf_file:
        def get_thread (self, startno, endno):
 
                thread = []
-               c = self.chords[startno]
+
+               c = None
+               try:
+                       c = self.chords[startno]
+               except IndexError:
+                       sys.stderr.write ("Huh? Frame has invalid bounds (%d,%d)\n" % (startno, endno))
+                       return []
+
+               
                while c and c.number () <> endno:
                        thread.append (c)
                        c = c.next
@@ -741,8 +959,6 @@ class Etf_file:
                                str = str + '\n\n' + s.dump () 
                                staffs.append ('\\' + s.staffid ())
 
-               if staffs:
-                       str = str + '\\score { < %s > } ' % string.join (staffs)
 
                # should use \addlyrics ?
 
@@ -752,49 +968,60 @@ class Etf_file:
                if len (self.verses) > 1:
                        sys.stderr.write ("\nLyrics found; edit to use \\addlyrics to couple to a staff\n")
                        
+               if staffs:
+                       str = str + '\\score { < %s > } ' % string.join (staffs)
+                       
                return str
 
 
        def __str__ (self):
-               return self.dump ()
+               return 'ETF FILE %s %s' % (self.measures,  self.entries)
        
        def unthread_entries (self):
                self.chords = [None]
                for e in self.entries[1:]:
-                       self.chords.append (Chord (e))
-
+                       ch = None
+                       if e:           
+                               ch = Chord (e)
+                       self.chords.append (ch)
+                               
                for e in self.chords[1:]:
+                       if not e:
+                               continue
                        e.prev = self.chords[e.finale[0][1]]
                        e.next = self.chords[e.finale[0][2]]
 
-
-        
-
-
-
 def identify():
        sys.stderr.write ("%s from LilyPond %s\n" % (program_name, version))
 
 def help ():
-       print r"""
-Convert ETF to LilyPond.
+       print """Usage: etf2ly [OPTION]... ETF-FILE
 
-Usage: etf2ly [OPTION]... ETF-FILE
+Convert ETF to LilyPond.
 
 Options:
-  -h, --help          this help
-  -o, --output=FILE   set output filename to FILE
-  -v, --version       version information
+  -h,--help          this help
+  -o,--output=FILE   set output filename to FILE
+  -v,--version       version information
 
 Enigma Transport Format is a format used by Coda Music Technology's
 Finale product. This program will convert a subset of ETF to a
 ready-to-use lilypond file.
 
+Report bugs to bug-gnu-music@gnu.org
 
+Written by  Han-Wen Nienhuys <hanwen@cs.uu.nl>
 """
 
 def print_version ():
-       print r"""etf2ly (GNU lilypond) %s""" % version
+       print r"""etf2ly (GNU lilypond) %s
+
+This is free software.  It is covered by the GNU General Public License,
+and you are welcome to change it and/or distribute copies of it under
+certain conditions.  Invoke as `midi2ly --warranty' for more information.
+
+Copyright (c) 2000 by Han-Wen Nienhuys <hanwen@cs.uu.nl>
+""" % version
 
 
 
@@ -819,7 +1046,7 @@ for opt in options:
 
 identify()
 
-# header['tagline'] = 'Lily was here %s -- automatically converted from ABC' % version
+e = None
 for f in files:
        if f == '-':
                f = ''
@@ -827,7 +1054,7 @@ for f in files:
        sys.stderr.write ('Processing `%s\'\n' % f)
        e = Etf_file(f)
        if not out_filename:
-               out_filename = os.path.basename (re.sub ('.etf$', '.ly', f))
+               out_filename = os.path.basename (re.sub ('(?i).etf$', '.ly', f))
                
        if out_filename == f:
                out_filename = os.path.basename (f + '.ly')