]> git.donarmstrong.com Git - lilypond.git/blobdiff - python/musicxml.py
Merge branch 'lilypond/translation' of ssh://jomand@git.sv.gnu.org/srv/git/lilypond...
[lilypond.git] / python / musicxml.py
index 0d4813de1db5607dbd05963504a2bce0a673f831..7c398e1e5d7a5f73a97b3a25d22bcbf37122f289 100644 (file)
@@ -1,6 +1,25 @@
+# -*- coding: utf-8 -*-
 import new
 import string
 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
+    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):
@@ -32,11 +51,11 @@ class Xml_node:
        return ''.join ([c.get_text () for c in self._children])
 
     def message (self, msg):
-        print msg
+        ly.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):
@@ -72,11 +91,19 @@ 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]
 
+    def get_named_child_value_number (self, name, default):
+        n = self.get_maybe_exist_named_child (name)
+        if n:
+            return string.atoi (n.get_text())
+        else:
+            return default
+
+
 class Music_xml_node (Xml_node):
     def __init__ (self):
        Xml_node.__init__ (self)
@@ -108,12 +135,11 @@ class Identification (Xml_node):
 
     def get_creator (self, type):
         creators = self.get_named_children ('creator')
-        # return the first creator tag that has type 'editor'
+        # 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 ()
-            else:
-                return ''
+        return None
 
     def get_composer (self):
         c = self.get_creator ('composer')
@@ -124,13 +150,17 @@ class Identification (Xml_node):
         for i in creators:
             if not hasattr (i, 'type'):
                 return i.get_text ()
-        return c
+        return None
     def get_arranger (self):
         return self.get_creator ('arranger')
     def get_editor (self):
         return self.get_creator ('editor')
     def get_poet (self):
-        return self.get_creator ('poet')
+        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')
@@ -139,7 +169,7 @@ class Identification (Xml_node):
             if children:
                 return children[0].get_text ()
         else:
-            return ''
+            return None
       
     def get_encoding_software (self):
         return self.get_encoding_information ('software')
@@ -149,6 +179,16 @@ 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
+
 
 
 class Duration (Music_xml_node):
@@ -158,6 +198,8 @@ 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):
@@ -177,6 +219,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')
@@ -214,23 +271,33 @@ 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'
+            error (_ ("requested time signature, but time sig is unknown"))
             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"
@@ -243,7 +310,15 @@ class Attributes (Measure_element):
 
         fifths = int (key.get_maybe_exist_named_child ('fifths').get_text ())
         return (fifths, mode)
-                
+
+class Barline (Measure_element):
+    pass
+class BarStyle (Music_xml_node):
+    pass
+class Partial (Measure_element):
+    def __init__ (self, partial):
+        Measure_element.__init__ (self)
+        self.partial = partial
 
 class Note (Measure_element):
     def __init__ (self):
@@ -251,26 +326,33 @@ class Note (Measure_element):
         self.instrument_name = ''
         
     def get_duration_log (self):
-       ch = self.get_maybe_exist_typed_child (get_class (u'type'))
+        ch = self.get_maybe_exist_named_child (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}.get (log)
-       else:
-           return 0
+                    'whole': 0,
+                    'breve': -1,
+                    'longa': -2}.get (log, 0)
+       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 with %s duration (no <type> element):") % (self.start, self.duration) )
+            return 0
 
     def get_factor (self):
-       return 1
+        return 1
 
     def get_pitches (self):
-       return self.get_typed_children (get_class (u'pitch'))
+        return self.get_typed_children (get_class (u'pitch'))
 
 class Part_list (Music_xml_node):
     def __init__ (self):
@@ -297,10 +379,20 @@ class Part_list (Music_xml_node):
         if instrument_name:
             return instrument_name
         else:
-            print "Opps, couldn't find instrument for ID=", id
+            ly.stderr_write (_ ("Unable to find 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):
+    def __init__ (self):
+        Music_xml_node.__init__ (self)
+        self.partial = 0
+    def is_implicit (self):
+        return hasattr (self, 'implicit') and self.implicit == 'yes'
     def get_notes (self):
        return self.get_typed_children (get_class (u'note'))
 
@@ -336,11 +428,11 @@ class Lyric (Music_xml_node):
         elif text == "_" and continued:
             return "__"
         elif continued and text:
-            return text + " --"
+            return escape_ly_output_string (text) + " --"
         elif continued:
             return "--"
         elif text:
-            return text
+            return escape_ly_output_string (text)
         else:
             return ""
 
@@ -358,7 +450,7 @@ 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
 
@@ -382,11 +474,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
@@ -397,6 +489,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 ()
         
@@ -407,10 +500,37 @@ class Part (Music_xml_node):
        measures = self.get_typed_children (Measure)
         last_moment = Rational (-1)
         last_measure_position = Rational (-1)
+        measure_position = Rational (0)
+        measure_start_moment = now
+        is_first_measure = True
+        previous_measure = None
        for m in measures:
-            measure_start_moment = now
-            measure_position = Rational (0)
-           for n in m.get_all_children ():
+            # implicit measures are used for artificial measures, e.g. when
+            # a repeat bar line splits a bar into two halves. In this case,
+            # don't reset the measure position to 0. They are also used for
+            # upbeats (initial value of 0 fits these, too).
+            # Also, don't reset the measure position at the end of the loop,
+            # but rather when starting the next measure (since only then do we
+            # 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 and previous_measure and previous_measure.partial == 0:
+                    length = attributes_object.get_measure_length ()
+                    new_now = measure_start_moment + length
+                    if now <> new_now:
+                        problem = 'incomplete'
+                        if now > new_now:
+                            problem = 'overfull'
+                        ## only for verbose operation.
+                        if problem <> 'incomplete' and previous_measure:
+                            previous_measure.message ('%s measure? Expected: %s, Difference: %s' % (problem, now, new_now - now))
+                    now = new_now
+                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:
@@ -443,74 +563,168 @@ 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:
                         n.instrument_name = part_list.get_instrument (instrument.id)
 
-            if attributes_object:
-                length = attributes_object.get_measure_length ()
-                new_now = measure_start_moment + length
-                
-                if now <> new_now:
-                    problem = 'incomplete'
-                    if now > new_now:
-                        problem = 'overfull'
-
-                    ## only for verbose operation.
-                    if problem <> 'incomplete':
-                        m.message ('%s measure? Expected: %s, Difference: %s' % (problem, now, new_now - now))
-
-                now = new_now
+            # Incomplete first measures are not padded, but registered as partial
+            if is_first_measure:
+                is_first_measure = False
+                # upbeats are marked as implicit measures
+                if attributes_object and m.is_implicit ():
+                    length = attributes_object.get_measure_length ()
+                    measure_end = measure_start_moment + length
+                    if measure_end <> now:
+                        m.partial = now
+            previous_measure = m
+
+    # 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._dict = attr._dict.copy ()
+        for c in attributes._children:
+            if hasattr (c, 'number') and c.number != staff:
+                attributes._children.remove (c)
+        return attributes
 
     def extract_voices (part):
        voices = {}
        measures = part.get_typed_children (Measure)
        elements = []
        for m in measures:
+            if m.partial > 0:
+                elements.append (Partial (m.partial))
            elements.extend (m.get_all_children ())
+        # make sure we know all voices already so that dynamics, clefs, etc.
+        # can be assigned to the correct voices
+        voice_to_staff_dict = {}
+        for n in elements:
+            voice_id = n.get_maybe_exist_named_child (u'voice')
+            vid = None
+            if voice_id:
+                vid = voice_id.get_text ()
+
+            staff_id = n.get_maybe_exist_named_child (u'staff')
+            sid = None
+            if staff_id:
+                sid = staff_id.get_text ()
+            else:
+                sid = "None"
+            if vid and not voices.has_key (vid):
+                voices[vid] = Musicxml_voice()
+            if vid and sid and not n.get_maybe_exist_typed_child (Grace):
+                if not voice_to_staff_dict.has_key (vid):
+                    voice_to_staff_dict[vid] = sid
+        # invert the voice_to_staff_dict into a staff_to_voice_dict (since we
+        # need to assign staff-assigned objects like clefs, times, etc. to
+        # all the correct voices. This will never work entirely correct due
+        # to staff-switches, but that's the best we can do!
+        staff_to_voice_dict = {}
+        for (v,s) in voice_to_staff_dict.items ():
+            if not staff_to_voice_dict.has_key (s):
+                staff_to_voice_dict[s] = [v]
+            else:
+                staff_to_voice_dict[s].append (v)
+
 
        start_attr = None
+        assign_to_next_note = []
+        id = None
        for n in elements:
            voice_id = n.get_maybe_exist_typed_child (get_class ('voice'))
 
-            # 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) ):
+           if not (voice_id or isinstance (n, Attributes) or
+                    isinstance (n, Direction) or isinstance (n, Partial) or
+                    isinstance (n, Barline) or isinstance (n, Harmony) ):
                continue
 
            if isinstance (n, Attributes) and not start_attr:
                start_attr = n
                continue
 
-           if isinstance (n, Attributes) or isinstance (n, Direction):
-               for v in voices.values ():
-                   v.add_element (n)
-               continue
+            if isinstance (n, Attributes):
+                # 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)
+                continue
+
+            if isinstance (n, Partial) or isinstance (n, Barline):
+                for v in voices.keys ():
+                    voices[v].add_element (n)
+                continue
+
+            if isinstance (n, Direction):
+                staff_id = n.get_maybe_exist_named_child (u'staff')
+                if staff_id:
+                    staff_id = staff_id.get_text ()
+                if staff_id:
+                    dir_voices = staff_to_voice_dict.get (staff_id, voices.keys ())
+                else:
+                    dir_voices = voices.keys ()
+                for v in dir_voices:
+                    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.
+                assign_to_next_note.append (n)
+                continue
 
            id = voice_id.get_text ()
-           if not voices.has_key (id):
-               voices[id] = Musicxml_voice()
+            if hasattr (n, 'print-object') and getattr (n, 'print-object') == "no":
+                #Skip this note. 
+                pass
+            else:
+                for i in assign_to_next_note:
+                    voices[id].add_element (i)
+                assign_to_next_note = []
+                voices[id].add_element (n)
 
-           voices[id].add_element (n)
+        # Assign all remaining elements from assign_to_next_note to the voice
+        # of the previous note:
+        for i in assign_to_next_note:
+            voices[id].add_element (i)
+        assign_to_next_note = []
 
        if start_attr:
-           for (k,v) in voices.items ():
-               v.insert (0, start_attr)
+            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()
 
-       part._voices = voices
+        part._voices = voices
 
     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):
@@ -521,8 +735,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):
@@ -536,32 +750,88 @@ 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 Bracket (Music_xml_spanner):
+    pass
+
+class Dashes (Music_xml_spanner):
+    pass
+
+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 Pedal (Music_xml_spanner):
+    pass
+
+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):
+        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
+    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
@@ -575,35 +845,91 @@ class Direction (Music_xml_node):
 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
+
+class Words (Music_xml_node):
+    pass
+
+class Harmony (Music_xml_node):
+    pass
+
+class Frame (Music_xml_node):
+    def get_frets (self):
+        return self.get_named_child_value_number ('frame-frets', 4)
+    def get_strings (self):
+        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)
+    def get_fret (self):
+        return self.get_named_child_value_number ('fret', 0)
+    def get_fingering (self):
+        return self.get_named_child_value_number ('fingering', -1)
+    def get_barre (self):
+        n = self.get_maybe_exist_named_child ('barre')
+        if n:
+            return getattr (n, 'type', '')
+        else:
+            return ''
+
 
 ## need this, not all classes are instantiated
 ## 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,
        'attributes': Attributes,
+        'barline': Barline,
+        'bar-style': BarStyle,
        'beam' : Beam,
+        'bend' : Bend,
+        'bracket' : Bracket,
        'chord': Chord,
+        'dashes' : Dashes,
        'dot': Dot,
        'direction': Direction,
         'direction-type': DirType,
        'duration': Duration,
+        'frame': Frame,
+        'frame-note': Frame_Note,
+        'glissando': Glissando,
        'grace': Grace,
+        'harmony': Harmony,
         '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,
+    'score-part': Score_part,
+        'slide': Slide,
        'slur': Slur,
+       'staff': Staff,
         'syllabic': Syllabic,
         'text': Text,
        'time-modification': Time_modification,
+        'tuplet': Tuplet,
        'type': Type,
+       'unpitched': Unpitched,
+        'wavy-line': Wavy_line,
+        'wedge': Wedge,
+        'words': Words,
         'work': Work,
 }
 
@@ -627,7 +953,9 @@ def get_class (name):
 def lxml_demarshal_node (node):
     name = node.tag
 
-    if name is None:
+    # TODO: This is a nasty hack, but I couldn't find any other way to check
+    #       if the given node is a comment node (class _Comment):
+    if name is None or re.match (u"^<!--.*-->$", node.__repr__()):
         return None
     klass = get_class (name)
     py_node = klass()