]> git.donarmstrong.com Git - lilypond.git/blobdiff - python/musicxml.py
MusicXML: A grace note at the beginning shall not set staff for the voice
[lilypond.git] / python / musicxml.py
index 519a0c3f6c531e80512987a1cbbbf00f0187da42..9788d9745dd9fda26d898660bb13af0634bd168e 100644 (file)
@@ -1,5 +1,16 @@
 import new
+import string
 from rational import *
+import re
+import sys
+
+def escape_ly_output_string (input_string):
+    return_string = input_string
+    needs_quotes = not re.match (u"^[a-zA-ZäöüÜÄÖßñ]*$", return_string);
+    if needs_quotes:
+        return_string = "\"" + string.replace (return_string, "\"", "\\\"") + "\""
+    return return_string
+
 
 class Xml_node:
     def __init__ (self):
@@ -31,18 +42,21 @@ class Xml_node:
        return ''.join ([c.get_text () for c in self._children])
 
     def message (self, msg):
-        print msg
+        sys.stderr.write (msg+'\n')
 
         p = self
         while p:
-            print '  In: <%s %s>' % (p._name, ' '.join (['%s=%s' % item for item in p._attribute_dict.items()]))
+            sys.stderr.write ('  In: <%s %s>\n' % (p._name, ' '.join (['%s=%s' % item for item in p._attribute_dict.items()])))
             p = p.get_parent ()
         
     def get_typed_children (self, klass):
-       return [c for c in self._children if isinstance(c, klass)]
+        if not klass:
+            return []
+        else:
+            return [c for c in self._children if isinstance(c, klass)]
 
     def get_named_children (self, nm):
-       return self.get_typed_children (class_dict[nm])
+       return self.get_typed_children (get_class (nm))
 
     def get_named_child (self, nm):
        return self.get_maybe_exist_named_child (nm)
@@ -54,7 +68,7 @@ class Xml_node:
        return self._children
 
     def get_maybe_exist_named_child (self, name):
-       return self.get_maybe_exist_typed_child (class_dict[name])
+       return self.get_maybe_exist_typed_child (get_class (name))
 
     def get_maybe_exist_typed_child (self, klass):
        cn = self.get_typed_children (klass)
@@ -68,7 +82,7 @@ class Xml_node:
     def get_unique_typed_child (self, klass):
        cn = self.get_typed_children(klass)
        if len (cn) <> 1:
-           print self.__dict__ 
+           sys.stderr.write (self.__dict__ + '\n')
            raise 'Child is not unique for', (klass, 'found', cn)
 
        return cn[0]
@@ -79,6 +93,76 @@ class Music_xml_node (Xml_node):
        self.duration = Rational (0)
        self.start = Rational (0)
 
+class Work (Xml_node):
+    def get_work_information (self, tag):
+        wt = self.get_maybe_exist_named_child (tag)
+        if wt:
+            return wt.get_text ()
+        else:
+            return ''
+      
+    def get_work_title (self):
+        return self.get_work_information ('work-title')
+    def get_work_number (self):
+        return self.get_work_information ('work-number')
+    def get_opus (self):
+        return self.get_work_information ('opus')
+
+class Identification (Xml_node):
+    def get_rights (self):
+        rights = self.get_maybe_exist_named_child ('rights')
+        if rights:
+            return rights.get_text ()
+        else:
+            return ''
+
+    def get_creator (self, type):
+        creators = self.get_named_children ('creator')
+        # return the first creator tag that has the particular type
+        for i in creators:
+            if hasattr (i, 'type') and i.type == type:
+                return i.get_text ()
+        return None
+
+    def get_composer (self):
+        c = self.get_creator ('composer')
+        if c:
+            return c
+        creators = self.get_named_children ('creator')
+        # return the first creator tag that has no type at all
+        for i in creators:
+            if not hasattr (i, 'type'):
+                return i.get_text ()
+        return None
+    def get_arranger (self):
+        return self.get_creator ('arranger')
+    def get_editor (self):
+        return self.get_creator ('editor')
+    def get_poet (self):
+        v = self.get_creator ('lyricist')
+        if v:
+            return v
+        v = self.get_creator ('poet')
+        return v
+    
+    def get_encoding_information (self, type):
+        enc = self.get_named_children ('encoding')
+        if enc:
+            children = enc[0].get_named_children (type)
+            if children:
+                return children[0].get_text ()
+        else:
+            return None
+      
+    def get_encoding_software (self):
+        return self.get_encoding_information ('software')
+    def get_encoding_date (self):
+        return self.get_encoding_information ('encoding-date')
+    def get_encoding_person (self):
+        return self.get_encoding_information ('encoder')
+    def get_encoding_description (self):
+        return self.get_encoding_information ('encoding-description')
+
 
 class Duration (Music_xml_node):
     def get_length (self):
@@ -87,20 +171,22 @@ class Duration (Music_xml_node):
 
 class Hash_comment (Music_xml_node):
     pass
+class Hash_text (Music_xml_node):
+    pass
 
 class Pitch (Music_xml_node):
     def get_step (self):
-       ch = self.get_unique_typed_child (class_dict[u'step'])
+       ch = self.get_unique_typed_child (get_class (u'step'))
        step = ch.get_text ().strip ()
        return step
     def get_octave (self):
-       ch = self.get_unique_typed_child (class_dict[u'octave'])
+       ch = self.get_unique_typed_child (get_class (u'octave'))
 
        step = ch.get_text ().strip ()
        return int (step)
 
     def get_alteration (self):
-       ch = self.get_maybe_exist_typed_child (class_dict[u'alter'])
+       ch = self.get_maybe_exist_typed_child (get_class (u'alter'))
        alter = 0
        if ch:
            alter = int (ch.get_text ().strip ())
@@ -132,7 +218,7 @@ class Attributes (Measure_element):
            self._dict[c.get_name()] = c
 
     def get_named_attribute (self, name):
-       return self._dict[name]
+       return self._dict.get (name)
 
     def get_measure_length (self):
         (n,d) = self.get_time_signature ()
@@ -143,29 +229,39 @@ class Attributes (Measure_element):
 
         try:
             mxl = self.get_named_attribute ('time')
-            
-            beats = mxl.get_maybe_exist_named_child ('beats')
-            type = mxl.get_maybe_exist_named_child ('beat-type')
-            return (int (beats.get_text ()),
-                    int (type.get_text ()))
+            if mxl:
+                beats = mxl.get_maybe_exist_named_child ('beats')
+                type = mxl.get_maybe_exist_named_child ('beat-type')
+                return (int (beats.get_text ()),
+                        int (type.get_text ()))
+            else:
+                return (4, 4)
         except KeyError:
-            print 'error: requested time signature, but time sig unknown'
+            sys.stderr.write ('error: requested time signature, but time sig unknown\n')
             return (4, 4)
 
-    def get_clef_sign (self):
+    # returns clef information in the form ("cleftype", position, octave-shift)
+    def get_clef_information (self):
+        clefinfo = ['G', 2, 0]
         mxl = self.get_named_attribute ('clef')
+        if not mxl:
+            return clefinfo
         sign = mxl.get_maybe_exist_named_child ('sign')
         if sign:
-            return sign.get_text ()
-        else:
-            print 'clef requested, but unknow'
-            return 'G'
+            clefinfo[0] = sign.get_text()
+        line = mxl.get_maybe_exist_named_child ('line')
+        if line:
+            clefinfo[1] = string.atoi (line.get_text ())
+        octave = mxl.get_maybe_exist_named_child ('clef-octave-change')
+        if octave:
+            clefinfo[2] = string.atoi (octave.get_text ())
+        return clefinfo
 
     def get_key_signature (self):
         "return (fifths, mode) tuple"
 
         key = self.get_named_attribute ('key')
-        mode_node = self.get_maybe_exist_named_child ('mode')
+        mode_node = key.get_maybe_exist_named_child ('mode')
         mode = 'major'
         if mode_node:
             mode = mode_node.get_text ()
@@ -180,26 +276,30 @@ class Note (Measure_element):
         self.instrument_name = ''
         
     def get_duration_log (self):
-       ch = self.get_maybe_exist_typed_child (class_dict[u'type'])
+        ch = self.get_maybe_exist_typed_child (get_class (u'type'))
 
-       if ch:
-           log = ch.get_text ().strip()
-           return {'eighth': 3,
+        if ch:
+            log = ch.get_text ().strip()
+            return {'256th': 8,
+                    '128th': 7,
+                    '64th': 6,
+                    '32nd': 5,
+                    '16th': 4,
+                    'eighth': 3,
                     'quarter': 2,
                     'half': 1,
-                    '16th': 4,
-                    '32nd': 5,
-                    'breve': -1,
-                    'long': -2,
-                    'whole': 0} [log]
-       else:
-           return 0
+                    'whole': 0,
+                    'breve': -1,
+                    'long': -2}.get (log, 0)
+        else:
+            sys.stderr.write ("Encountered note without duration (no <type> element): %s\n" % self)
+            return 0
 
     def get_factor (self):
-       return 1
+        return 1
 
     def get_pitches (self):
-       return self.get_typed_children (class_dict[u'pitch'])
+        return self.get_typed_children (get_class (u'pitch'))
 
 class Part_list (Music_xml_node):
     def __init__ (self):
@@ -222,22 +322,69 @@ class Part_list (Music_xml_node):
         if not self._id_instrument_name_dict:
             self.generate_id_instrument_dict()
 
-        try:
-            return self._id_instrument_name_dict[id]
-        except KeyError:
-            print "Opps, couldn't find instrument for ID=", id
+        instrument_name = self._id_instrument_name_dict.get (id)
+        if instrument_name:
+            return instrument_name
+        else:
+            sys.stderr.write ("Opps, couldn't find instrument for ID=%s\n" % id)
             return "Grand Piano"
+
+class Part_group (Music_xml_node):
+    pass
+class Score_part (Music_xml_node):
+    pass
         
-class Measure(Music_xml_node):
+class Measure (Music_xml_node):
     def get_notes (self):
-       return self.get_typed_children (class_dict[u'note'])
+       return self.get_typed_children (get_class (u'note'))
+
+class Syllabic (Music_xml_node):
+    def continued (self):
+        text = self.get_text()
+        return (text == "begin") or (text == "middle")
+class Text (Music_xml_node):
+    pass
+
+class Lyric (Music_xml_node):
+    def get_number (self):
+        if hasattr (self, 'number'):
+            return self.number
+        else:
+            return -1
+
+    def lyric_to_text (self):
+        continued = False
+        syllabic = self.get_maybe_exist_typed_child (Syllabic)
+        if syllabic:
+            continued = syllabic.continued ()
+        text = self.get_maybe_exist_typed_child (Text)
+        
+        if text:
+            text = text.get_text()
+            # We need to convert soft hyphens to -, otherwise the ascii codec as well
+            # as lilypond will barf on that character
+            text = string.replace( text, u'\xad', '-' )
+        
+        if text == "-" and continued:
+            return "--"
+        elif text == "_" and continued:
+            return "__"
+        elif continued and text:
+            return escape_ly_output_string (text) + " --"
+        elif continued:
+            return "--"
+        elif text:
+            return escape_ly_output_string (text)
+        else:
+            return ""
 
-    
 class Musicxml_voice:
     def __init__ (self):
        self._elements = []
        self._staves = {}
        self._start_staff = None
+        self._lyrics = []
+        self._has_lyrics = False
 
     def add_element (self, e):
        self._elements.append (e)
@@ -245,13 +392,29 @@ class Musicxml_voice:
            and e.get_maybe_exist_typed_child (Staff)):
            name = e.get_maybe_exist_typed_child (Staff).get_text ()
 
-           if not self._start_staff:
+           if not self._start_staff and not e.get_maybe_exist_typed_child (Grace):
                self._start_staff = name
            self._staves[name] = True
 
+        lyrics = e.get_typed_children (Lyric)
+        if not self._has_lyrics:
+          self.has_lyrics = len (lyrics) > 0
+
+        for l in lyrics:
+            nr = l.get_number()
+            if (nr > 0) and not (nr in self._lyrics):
+                self._lyrics.append (nr)
+
     def insert (self, idx, e):
        self._elements.insert (idx, e)
 
+    def get_lyrics_numbers (self):
+        if (len (self._lyrics) == 0) and self._has_lyrics:
+            #only happens if none of the <lyric> tags has a number attribute
+            return [1]
+        else:
+            return self._lyrics
+
 
 
 class Part (Music_xml_node):
@@ -282,6 +445,8 @@ class Part (Music_xml_node):
             measure_start_moment = now
             measure_position = Rational (0)
            for n in m.get_all_children ():
+                if isinstance (n, Hash_text):
+                    continue
                dur = Rational (0)
 
                 if n.__class__ == Attributes:
@@ -291,7 +456,7 @@ class Part (Music_xml_node):
                     attributes_object = n
                     
                    factor = Rational (1,
-                                      int (attributes_dict['divisions'].get_text ()))
+                                      int (attributes_dict.get ('divisions').get_text ()))
 
                 
                if (n.get_maybe_exist_typed_child (Duration)):
@@ -314,15 +479,25 @@ class Part (Music_xml_node):
                     and n.get_maybe_exist_typed_child (Chord)):
                     now = last_moment
                     measure_position = last_measure_position
-                    
-                last_moment = now
-                last_measure_position = measure_position
 
-               n._when = now
+                n._when = now
                 n._measure_position = measure_position
-               n._duration = dur
-               now += dur
-                measure_position += dur
+                n._duration = dur
+                if dur > Rational (0):
+                    last_moment = now
+                    last_measure_position = measure_position
+                    now += dur
+                    measure_position += dur
+                elif dur < Rational (0):
+                    # backup element, reset measure position
+                    now += dur
+                    measure_position += dur
+                    if measure_position < 0:
+                        # backup went beyond the measure start => reset to 0
+                        now -= measure_position
+                        measure_position = 0
+                    last_moment = now
+                    last_measure_position = measure_position
                 if n._name == 'note':
                     instrument = n.get_maybe_exist_named_child ('instrument')
                     if instrument:
@@ -352,16 +527,18 @@ class Part (Music_xml_node):
 
        start_attr = None
        for n in elements:
-           voice_id = n.get_maybe_exist_typed_child (class_dict['voice'])
+           voice_id = n.get_maybe_exist_typed_child (get_class ('voice'))
 
-           if not (voice_id or isinstance (n, Attributes)):
+            # TODO: If the first element of a voice is a dynamics entry,
+            #       then voice_id is not yet set! Thus it will currently be ignored
+           if not (voice_id or isinstance (n, Attributes) or isinstance (n, Direction) ):
                continue
 
            if isinstance (n, Attributes) and not start_attr:
                start_attr = n
                continue
 
-           if isinstance (n, Attributes):
+           if isinstance (n, Attributes) or isinstance (n, Direction):
                for v in voices.values ():
                    v.add_element (n)
                continue
@@ -395,8 +572,8 @@ class Notations (Music_xml_node):
 
 class Time_modification(Music_xml_node):
     def get_fraction (self):
-       b = self.get_maybe_exist_typed_child (class_dict['actual-notes'])
-       a = self.get_maybe_exist_typed_child (class_dict['normal-notes'])
+       b = self.get_maybe_exist_named_child ('actual-notes')
+       a = self.get_maybe_exist_named_child ('normal-notes')
        return (int(a.get_text ()), int (b.get_text ()))
 
 class Accidental (Music_xml_node):
@@ -405,40 +582,79 @@ class Accidental (Music_xml_node):
        self.editorial = False
        self.cautionary = False
 
+class Music_xml_spanner (Music_xml_node):
+    def get_type (self):
+        if hasattr (self, 'type'):
+            return self.type
+        else:
+            return 0
+    def get_size (self):
+        if hasattr (self, 'size'):
+            return string.atoi (self.size)
+        else:
+            return 0
+
+class Wedge (Music_xml_spanner):
+    pass
 
-class Tuplet(Music_xml_node):
+class Tuplet (Music_xml_spanner):
     pass
 
-class Slur (Music_xml_node):
+class Slur (Music_xml_spanner):
     def get_type (self):
        return self.type
 
-class Beam (Music_xml_node):
+class Beam (Music_xml_spanner):
     def get_type (self):
        return self.get_text ()
     def is_primary (self):
         return self.number == "1"
+
+class Wavy_line (Music_xml_spanner):
+    pass
     
-class Chord (Music_xml_node):
+class Pedal (Music_xml_spanner):
     pass
 
-class Dot (Music_xml_node):
+class Glissando (Music_xml_spanner):
     pass
 
-class Alter (Music_xml_node):
+class Octave_shift (Music_xml_spanner):
+    # default is 8 for the octave-shift!
+    def get_size (self):
+        if hasattr (self, 'size'):
+            return string.atoi (self.size)
+        else:
+            return 8
+
+class Chord (Music_xml_node):
     pass
 
+class Dot (Music_xml_node):
+    pass
+
+# Rests in MusicXML are <note> blocks with a <rest> inside. This class is only
+# for the inner <rest> element, not the whole rest block.
 class Rest (Music_xml_node):
     def __init__ (self):
         Music_xml_node.__init__ (self)
         self._is_whole_measure = False
     def is_whole_measure (self):
         return self._is_whole_measure
-
-class Mode (Music_xml_node):
-    pass
-class Tied (Music_xml_node):
-    pass
+    def get_step (self):
+        ch = self.get_maybe_exist_typed_child (get_class (u'display-step'))
+        if ch:
+            step = ch.get_text ().strip ()
+            return step
+        else:
+            return None
+    def get_octave (self):
+        ch = self.get_maybe_exist_typed_child (get_class (u'display-octave'))
+        if ch:
+            step = ch.get_text ().strip ()
+            return int (step)
+        else:
+            return None
 
 class Type (Music_xml_node):
     pass
@@ -447,36 +663,61 @@ class Grace (Music_xml_node):
 class Staff (Music_xml_node):
     pass
 
-class Instrument (Music_xml_node):
+class Direction (Music_xml_node):
+    pass
+class DirType (Music_xml_node):
     pass
 
+class Bend (Music_xml_node):
+    def bend_alter (self):
+        alter = self.get_maybe_exist_named_child ('bend-alter')
+        if alter:
+            return alter.get_text()
+        else:
+            return 0
+
+
+
 ## need this, not all classes are instantiated
-## for every input file.
+## for every input file. Only add those classes, that are either directly
+## used by class name or extend Music_xml_node in some way!
 class_dict = {
        '#comment': Hash_comment,
+        '#text': Hash_text,
        'accidental': Accidental,
-       'alter': Alter,
        'attributes': Attributes,
        'beam' : Beam,
+        'bend' : Bend,
        'chord': Chord,
        'dot': Dot,
+       'direction': Direction,
+        'direction-type': DirType,
        'duration': Duration,
+        'glissando': Glissando,
        'grace': Grace,
-        'instrument': Instrument, 
-       'mode' : Mode,
+        'identification': Identification,
+        'lyric': Lyric,
        'measure': Measure,
        'notations': Notations,
        'note': Note,
+        'octave-shift': Octave_shift,
        'part': Part,
+    'part-group': Part_group,
+       'part-list': Part_list,
+        'pedal': Pedal,
        'pitch': Pitch,
-       'rest':Rest,
+       'rest': Rest,
+    'score-part': Score_part,
        'slur': Slur,
-       'tied': Tied,
+       'staff': Staff,
+        'syllabic': Syllabic,
+        'text': Text,
        'time-modification': Time_modification,
-       'tuplet': Tuplet,
+        'tuplet': Tuplet,
        'type': Type,
-       'part-list': Part_list,
-       'staff': Staff,
+        'wavy-line': Wavy_line,
+        'wedge': Wedge,
+        'work': Work,
 }
 
 def name2class_name (name):
@@ -487,9 +728,10 @@ def name2class_name (name):
     return str (name)
 
 def get_class (name):
-    try:
-        return class_dict[name]
-    except KeyError:
+    classname = class_dict.get (name)
+    if classname:
+        return classname
+    else:
        class_name = name2class_name (name)
        klass = new.classobj (class_name, (Music_xml_node,) , {})
        class_dict[name] = klass