]> git.donarmstrong.com Git - lilypond.git/blobdiff - python/musicxml.py
MusicXML: Add function to dump a whole XML node tree recursively
[lilypond.git] / python / musicxml.py
index a251afa5f313427575a4a80defcac1b87b9fb7ed..498661e6211b1a8e1bff7934d38e8d1c585c9627 100644 (file)
@@ -5,6 +5,13 @@ from rational import *
 import re
 import sys
 import copy
+import lilylib as ly
+
+_ = ly._
+
+def error (str):
+    ly.stderr_write ((_ ("error: %s") % str) + "\n")
+
 
 def escape_ly_output_string (input_string):
     return_string = input_string
@@ -14,6 +21,22 @@ def escape_ly_output_string (input_string):
     return return_string
 
 
+def musicxml_duration_to_log (dur):
+    return  {'256th': 8,
+             '128th': 7,
+             '64th': 6,
+             '32nd': 5,
+             '16th': 4,
+             'eighth': 3,
+             'quarter': 2,
+             'half': 1,
+             'whole': 0,
+             'breve': -1,
+             'longa': -2,
+             'long': -2}.get (dur, 0)
+
+
+
 class Xml_node:
     def __init__ (self):
        self._children = []
@@ -44,13 +67,25 @@ class Xml_node:
        return ''.join ([c.get_text () for c in self._children])
 
     def message (self, msg):
-        sys.stderr.write (msg+'\n')
+        ly.stderr_write (msg+'\n')
 
         p = self
         while p:
-            sys.stderr.write ('  In: <%s %s>\n' % (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 dump (self, indent = ''):
+        sys.stderr.write ('%s<%s%s>' % (indent, self._name, ''.join ([' %s=%s' % item for item in self._attribute_dict.items ()])))
+        non_text_children = [c for c in self._children if not isinstance (c, Hash_text)]
+        if non_text_children:
+            sys.stderr.write ('\n')
+        for c in self._children:
+            c.dump (indent + "    ")
+        if non_text_children:
+            sys.stderr.write (indent)
+        sys.stderr.write ('</%s>\n' % self._name)
+
+        
     def get_typed_children (self, klass):
         if not klass:
             return []
@@ -120,11 +155,11 @@ class Work (Xml_node):
 
 class Identification (Xml_node):
     def get_rights (self):
-        rights = self.get_maybe_exist_named_child ('rights')
-        if rights:
-            return rights.get_text ()
-        else:
-            return ''
+        rights = self.get_named_children ('rights')
+        ret = []
+        for r in rights:
+          ret.append (r.get_text ())
+        return string.join (ret, "\n")
 
     def get_creator (self, type):
         creators = self.get_named_children ('creator')
@@ -172,6 +207,25 @@ class Identification (Xml_node):
         return self.get_encoding_information ('encoder')
     def get_encoding_description (self):
         return self.get_encoding_information ('encoding-description')
+    
+    def get_encoding_software_list (self):
+        enc = self.get_named_children ('encoding')
+        software = []
+        for e in enc:
+            softwares = e.get_named_children ('software')
+            for s in softwares:
+                software.append (s.get_text ())
+        return software
+
+    def get_file_description (self):
+        misc = self.get_named_children ('miscellaneous')
+        for m in misc:
+            misc_fields = m.get_named_children ('miscellaneous-field')
+            for mf in misc_fields:
+                if hasattr (mf, 'name') and mf.name == 'description':
+                    return mf.get_text () 
+        return None
+
 
 
 class Duration (Music_xml_node):
@@ -182,7 +236,8 @@ class Duration (Music_xml_node):
 class Hash_comment (Music_xml_node):
     pass
 class Hash_text (Music_xml_node):
-    pass
+    def dump (self, indent = ''):
+        sys.stderr.write ('%s' % string.strip (self._data))
 
 class Pitch (Music_xml_node):
     def get_step (self):
@@ -202,6 +257,21 @@ class Pitch (Music_xml_node):
            alter = int (ch.get_text ().strip ())
        return alter
 
+class Unpitched (Music_xml_node):
+    def get_step (self):
+       ch = self.get_unique_typed_child (get_class (u'display-step'))
+       step = ch.get_text ().strip ()
+       return step
+
+    def get_octave (self):
+       ch = self.get_unique_typed_child (get_class (u'display-octave'))
+
+       if ch:
+           octave = ch.get_text ().strip ()
+           return int (octave)
+       else:
+           return None
+
 class Measure_element (Music_xml_node):
     def get_voice_id (self):
        voice_id = self.get_maybe_exist_named_child ('voice')
@@ -211,15 +281,27 @@ class Measure_element (Music_xml_node):
            return None
 
     def is_first (self):
-       cn = self._parent.get_typed_children (self.__class__)
-       cn = [c for c in cn if c.get_voice_id () == self.get_voice_id ()]
+        # Look at all measure elements (previously we had self.__class__, which
+        # only looked at objects of the same type!
+       cn = self._parent.get_typed_children (Measure_element)
+        # But only look at the correct voice; But include Attributes, too, which
+        # are not tied to any particular voice
+       cn = [c for c in cn if (c.get_voice_id () == self.get_voice_id ()) or isinstance (c, Attributes)]
        return cn[0] == self
 
 class Attributes (Measure_element):
     def __init__ (self):
        Measure_element.__init__ (self)
        self._dict = {}
+        self._original_tag = None
 
+    def is_first (self):
+       cn = self._parent.get_typed_children (self.__class__)
+        if self._original_tag:
+            return cn[0] == self._original_tag
+        else:
+            return cn[0] == self
+    
     def set_attributes_from_previous (self, dict):
        self._dict.update (dict)
         
@@ -247,7 +329,7 @@ class Attributes (Measure_element):
             else:
                 return (4, 4)
         except KeyError:
-            sys.stderr.write ('error: requested time signature, but time sig unknown\n')
+            error (_ ("requested time signature, but time sig is unknown"))
             return (4, 4)
 
     # returns clef information in the form ("cleftype", position, octave-shift)
@@ -272,9 +354,11 @@ class Attributes (Measure_element):
 
         key = self.get_named_attribute ('key')
         mode_node = key.get_maybe_exist_named_child ('mode')
-        mode = 'major'
+        mode = None
         if mode_node:
             mode = mode_node.get_text ()
+        if not mode or mode == '':
+            mode = 'major'
 
         fifths = int (key.get_maybe_exist_named_child ('fifths').get_text ())
         return (fifths, mode)
@@ -292,26 +376,26 @@ class Note (Measure_element):
     def __init__ (self):
         Measure_element.__init__ (self)
         self.instrument_name = ''
-        
+        self._after_grace = False
+    def is_grace (self):
+        return self.get_maybe_exist_named_child (u'grace')
+    def is_after_grace (self):
+        if not self.is_grace():
+            return False;
+        gr = self.get_maybe_exist_typed_child (Grace)
+        return self._after_grace or hasattr (gr, 'steal-time-previous');
+
     def get_duration_log (self):
         ch = self.get_maybe_exist_named_child (u'type')
 
         if ch:
             log = ch.get_text ().strip()
-            return {'256th': 8,
-                    '128th': 7,
-                    '64th': 6,
-                    '32nd': 5,
-                    '16th': 4,
-                    'eighth': 3,
-                    'quarter': 2,
-                    'half': 1,
-                    'whole': 0,
-                    'breve': -1,
-                    'long': -2}.get (log, 0)
+            return musicxml_duration_to_log (log)
+       elif self.get_maybe_exist_named_child (u'grace'):
+           # FIXME: is it ok to default to eight note for grace notes?
+           return 3
         else:
-            self.message ("Encountered note at %s without %s duration (no <type> element):" % (self.start, self.duration) )
-            return 0
+            return None
 
     def get_factor (self):
         return 1
@@ -344,7 +428,7 @@ class Part_list (Music_xml_node):
         if instrument_name:
             return instrument_name
         else:
-            sys.stderr.write ("Opps, couldn't find instrument for ID=%s\n" % id)
+            ly.stderr_write (_ ("Unable to find instrument for ID=%s\n") % id)
             return "Grand Piano"
 
 class Part_group (Music_xml_node):
@@ -439,11 +523,11 @@ class Musicxml_voice:
             return self._lyrics
 
 
-
 class Part (Music_xml_node):
     def __init__ (self):
         Music_xml_node.__init__ (self)
-       self._voices = []
+       self._voices = {}
+        self._staff_attributes_dict = {}
 
     def get_part_list (self):
         n = self
@@ -454,6 +538,7 @@ class Part (Music_xml_node):
         
     def interpret (self):
        """Set durations and starting points."""
+        """The starting point of the very first note is 0!"""
         
         part_list = self.get_part_list ()
         
@@ -467,7 +552,10 @@ class Part (Music_xml_node):
         measure_position = Rational (0)
         measure_start_moment = now
         is_first_measure = True
-        prvious_measure = None
+        previous_measure = None
+        # Graces at the end of a measure need to have their position set to the
+        # previous number!
+        pending_graces = []
        for m in measures:
             # implicit measures are used for artificial measures, e.g. when
             # a repeat bar line splits a bar into two halves. In this case,
@@ -478,7 +566,7 @@ class Part (Music_xml_node):
             # know if the next measure is implicit and continues that measure)
             if not m.is_implicit ():
                 # Warn about possibly overfull measures and reset the position
-                if attributes_object:
+                if attributes_object and previous_measure and previous_measure.partial == 0:
                     length = attributes_object.get_measure_length ()
                     new_now = measure_start_moment + length
                     if now <> new_now:
@@ -493,6 +581,14 @@ class Part (Music_xml_node):
                 measure_position = Rational (0)
 
             for n in m.get_all_children ():
+                # figured bass has a duration, but applies to the next note
+                # and should not change the current measure position!
+                if isinstance (n, FiguredBass):
+                    n._divisions = factor.denominator ()
+                    n._when = now
+                    n._measure_position = measure_position
+                    continue
+
                 if isinstance (n, Hash_text):
                     continue
                dur = Rational (0)
@@ -513,6 +609,12 @@ class Part (Music_xml_node):
                     
                    if n.get_name() == 'backup':
                        dur = - dur
+                        # reset all graces before the backup to after-graces:
+                        for n in pending_graces:
+                            n._when = n._prev_when
+                            n._measure_position = n._prev_measure_position
+                            n._after_grace = True
+                        pending_graces = []
                    if n.get_maybe_exist_typed_child (Grace):
                        dur = Rational (0)
 
@@ -530,6 +632,23 @@ class Part (Music_xml_node):
 
                 n._when = now
                 n._measure_position = measure_position
+
+                # For all grace notes, store the previous note,  in case need
+                # to turn the grace note into an after-grace later on!
+                if isinstance(n, Note) and n.is_grace ():
+                    n._prev_when = last_moment
+                    n._prev_measure_position = last_measure_position
+                # After-graces are placed at the same position as the previous note
+                if isinstance(n, Note) and  n.is_after_grace ():
+                    # TODO: We should do the same for grace notes at the end of 
+                    # a measure with no following note!!!
+                    n._when = last_moment
+                    n._measure_position = last_measure_position
+                elif isinstance(n, Note) and n.is_grace ():
+                    pending_graces.append (n)
+                elif (dur > Rational (0)):
+                    pending_graces = [];
+
                 n._duration = dur
                 if dur > Rational (0):
                     last_moment = now
@@ -551,6 +670,12 @@ class Part (Music_xml_node):
                     if instrument:
                         n.instrument_name = part_list.get_instrument (instrument.id)
 
+            # reset all graces at the end of the measure to after-graces:
+            for n in pending_graces:
+                n._when = n._prev_when
+                n._measure_position = n._prev_measure_position
+                n._after_grace = True
+            pending_graces = []
             # Incomplete first measures are not padded, but registered as partial
             if is_first_measure:
                 is_first_measure = False
@@ -565,12 +690,18 @@ class Part (Music_xml_node):
     # modify attributes so that only those applying to the given staff remain
     def extract_attributes_for_staff (part, attr, staff):
         attributes = copy.copy (attr)
-        attributes._children = copy.copy (attr._children)
+        attributes._children = [];
         attributes._dict = attr._dict.copy ()
-        for c in attributes._children:
-            if hasattr (c, 'number') and c.number != staff:
-                attributes._children.remove (c)
-        return attributes
+        attributes._original_tag = attr
+        # copy only the relevant children over for the given staff
+        for c in attr._children:
+            if (not (hasattr (c, 'number') and (c.number != staff)) and
+                not (isinstance (c, Hash_text))):
+                attributes._children.append (c)
+        if not attributes._children:
+            return None
+        else:
+            return attributes
 
     def extract_voices (part):
        voices = {}
@@ -588,6 +719,8 @@ class Part (Music_xml_node):
             vid = None
             if voice_id:
                 vid = voice_id.get_text ()
+            elif isinstance (n, Note):
+                vid = "None"
 
             staff_id = n.get_maybe_exist_named_child (u'staff')
             sid = None
@@ -617,10 +750,18 @@ class Part (Music_xml_node):
         id = None
        for n in elements:
            voice_id = n.get_maybe_exist_typed_child (get_class ('voice'))
+            if voice_id:
+                id = voice_id.get_text ()
+            else:
+                id = "None"
 
-           if not (voice_id or isinstance (n, Attributes) or
+            # We don't need backup/forward any more, since we have already 
+            # assigned the correct onset times. 
+            # TODO: Let Grouping through. Also: link, print, bokmark sound
+           if not (isinstance (n, Note) or isinstance (n, Attributes) or
                     isinstance (n, Direction) or isinstance (n, Partial) or
-                    isinstance (n, Barline) or isinstance (n, Harmony) ):
+                    isinstance (n, Barline) or isinstance (n, Harmony) or
+                    isinstance (n, FiguredBass) ):
                continue
 
            if isinstance (n, Attributes) and not start_attr:
@@ -631,8 +772,9 @@ class Part (Music_xml_node):
                 # assign these only to the voices they really belongs to!
                 for (s, vids) in staff_to_voice_dict.items ():
                     staff_attributes = part.extract_attributes_for_staff (n, s)
-                    for v in vids:
-                        voices[v].add_element (staff_attributes)
+                    if staff_attributes:
+                        for v in vids:
+                            voices[v].add_element (staff_attributes)
                 continue
 
             if isinstance (n, Partial) or isinstance (n, Barline):
@@ -652,13 +794,12 @@ class Part (Music_xml_node):
                     voices[v].add_element (n)
                 continue
 
-            if isinstance (n, Harmony):
-                # store the harmony element until we encounter the next note
-                # and assign it only to that one voice.
+            if isinstance (n, Harmony) or isinstance (n, FiguredBass):
+                # store the harmony or figured bass element until we encounter 
+                # the next note and assign it only to that one voice.
                 assign_to_next_note.append (n)
                 continue
 
-           id = voice_id.get_text ()
             if hasattr (n, 'print-object') and getattr (n, 'print-object') == "no":
                 #Skip this note. 
                 pass
@@ -678,6 +819,7 @@ class Part (Music_xml_node):
             for (s, vids) in staff_to_voice_dict.items ():
                 staff_attributes = part.extract_attributes_for_staff (start_attr, s)
                 staff_attributes.read_self ()
+                part._staff_attributes_dict[s] = staff_attributes
                 for v in vids:
                     voices[v].insert (0, staff_attributes)
                     voices[v]._elements[0].read_self()
@@ -686,6 +828,8 @@ class Part (Music_xml_node):
 
     def get_voices (self):
        return self._voices
+    def get_staff_attributes (self):
+        return self._staff_attributes_dict
 
 class Notations (Music_xml_node):
     def get_tie (self):
@@ -696,8 +840,8 @@ class Notations (Music_xml_node):
        else:
            return None
 
-    def get_tuplet (self):
-       return self.get_maybe_exist_typed_child (Tuplet)
+    def get_tuplets (self):
+       return self.get_typed_children (Tuplet)
 
 class Time_modification(Music_xml_node):
     def get_fraction (self):
@@ -729,6 +873,12 @@ class Wedge (Music_xml_spanner):
 class Tuplet (Music_xml_spanner):
     pass
 
+class Bracket (Music_xml_spanner):
+    pass
+
+class Dashes (Music_xml_spanner):
+    pass
+
 class Slur (Music_xml_spanner):
     def get_type (self):
        return self.type
@@ -748,6 +898,9 @@ class Pedal (Music_xml_spanner):
 class Glissando (Music_xml_spanner):
     pass
 
+class Slide (Music_xml_spanner):
+    pass
+
 class Octave_shift (Music_xml_spanner):
     # default is 8 for the octave-shift!
     def get_size (self):
@@ -811,6 +964,48 @@ class Words (Music_xml_node):
 class Harmony (Music_xml_node):
     pass
 
+class ChordPitch (Music_xml_node):
+    def step_class_name (self):
+        return u'root-step'
+    def alter_class_name (self):
+        return u'root-alter'
+    def get_step (self):
+        ch = self.get_unique_typed_child (get_class (self.step_class_name ()))
+        return ch.get_text ().strip ()
+    def get_alteration (self):
+        ch = self.get_maybe_exist_typed_child (get_class (self.alter_class_name ()))
+        alter = 0
+        if ch:
+            alter = int (ch.get_text ().strip ())
+        return alter
+
+class Root (ChordPitch):
+    pass
+
+class Bass (ChordPitch):
+    def step_class_name (self):
+        return u'bass-step'
+    def alter_class_name (self):
+        return u'bass-alter'
+
+class ChordModification (Music_xml_node):
+    def get_type (self):
+        ch = self.get_maybe_exist_typed_child (get_class (u'degree-type'))
+        return {'add': 1, 'alter': 1, 'subtract': -1}.get (ch.get_text ().strip (), 0)
+    def get_value (self):
+        ch = self.get_maybe_exist_typed_child (get_class (u'degree-value'))
+        value = 0
+        if ch:
+            value = int (ch.get_text ().strip ())
+        return value
+    def get_alter (self):
+        ch = self.get_maybe_exist_typed_child (get_class (u'degree-alter'))
+        value = 0
+        if ch:
+            value = int (ch.get_text ().strip ())
+        return value
+
+
 class Frame (Music_xml_node):
     def get_frets (self):
         return self.get_named_child_value_number ('frame-frets', 4)
@@ -818,6 +1013,7 @@ class Frame (Music_xml_node):
         return self.get_named_child_value_number ('frame-strings', 6)
     def get_first_fret (self):
         return self.get_named_child_value_number ('first-fret', 1)
+
 class Frame_Note (Music_xml_node):
     def get_string (self):
         return self.get_named_child_value_number ('string', 1)
@@ -832,6 +1028,19 @@ class Frame_Note (Music_xml_node):
         else:
             return ''
 
+class FiguredBass (Music_xml_node):
+    pass
+
+class BeatUnit (Music_xml_node):
+    pass
+
+class BeatUnitDot (Music_xml_node):
+    pass
+
+class PerMinute (Music_xml_node):
+    pass
+
+
 
 ## need this, not all classes are instantiated
 ## for every input file. Only add those classes, that are either directly
@@ -843,15 +1052,22 @@ class_dict = {
        'attributes': Attributes,
         'barline': Barline,
         'bar-style': BarStyle,
+        'bass': Bass,
        'beam' : Beam,
+        'beat-unit': BeatUnit,
+        'beat-unit-dot': BeatUnitDot,
         'bend' : Bend,
+        'bracket' : Bracket,
        'chord': Chord,
+        'dashes' : Dashes,
+        'degree' : ChordModification,
        'dot': Dot,
        'direction': Direction,
         'direction-type': DirType,
        'duration': Duration,
         'frame': Frame,
         'frame-note': Frame_Note,
+        'figured-bass': FiguredBass,
         'glissando': Glissando,
        'grace': Grace,
         'harmony': Harmony,
@@ -865,9 +1081,12 @@ class_dict = {
     'part-group': Part_group,
        'part-list': Part_list,
         'pedal': Pedal,
+        'per-minute': PerMinute,
        'pitch': Pitch,
        'rest': Rest,
-    'score-part': Score_part,
+        'root': Root,
+        'score-part': Score_part,
+        'slide': Slide,
        'slur': Slur,
        'staff': Staff,
         'syllabic': Syllabic,
@@ -875,6 +1094,7 @@ class_dict = {
        'time-modification': Time_modification,
         'tuplet': Tuplet,
        'type': Type,
+       'unpitched': Unpitched,
         'wavy-line': Wavy_line,
         'wedge': Wedge,
         'words': Words,
@@ -901,7 +1121,8 @@ def get_class (name):
 def lxml_demarshal_node (node):
     name = node.tag
 
-    if name is None:
+    # Ignore comment nodes, which are also returned by the etree parser!
+    if name is None or node.__class__.__name__ == "_Comment":
         return None
     klass = get_class (name)
     py_node = klass()
@@ -915,7 +1136,7 @@ def lxml_demarshal_node (node):
     for c in py_node._children:
        c._parent = py_node
 
-    for (k,v) in node.items ():
+    for (k, v) in node.items ():
         py_node.__dict__[k] = v
         py_node._attribute_dict[k] = v
 
@@ -925,14 +1146,14 @@ def minidom_demarshal_node (node):
     name = node.nodeName
 
     klass = get_class (name)
-    py_node = klass()
+    py_node = klass ()
     py_node._name = name
     py_node._children = [minidom_demarshal_node (cn) for cn in node.childNodes]
     for c in py_node._children:
        c._parent = py_node
 
     if node.attributes:
-       for (nm, value) in node.attributes.items():
+       for (nm, value) in node.attributes.items ():
            py_node.__dict__[nm] = value
             py_node._attribute_dict[nm] = value
             
@@ -945,10 +1166,10 @@ def minidom_demarshal_node (node):
 
 
 if __name__  == '__main__':
-        import lxml.etree
+    import lxml.etree
         
-        tree = lxml.etree.parse ('beethoven.xml')
-        mxl_tree = lxml_demarshal_node (tree.getroot ())
-        ks = class_dict.keys()
-        ks.sort()
-        print '\n'.join (ks)
+    tree = lxml.etree.parse ('beethoven.xml')
+    mxl_tree = lxml_demarshal_node (tree.getroot ())
+    ks = class_dict.keys ()
+    ks.sort ()
+    print '\n'.join (ks)