X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=python%2Fmusicxml.py;h=e9d1ef8bedebd195450a78660f3ff716379dbc5e;hb=9a14a394dfa23b38ab78c3eb09a2d7b5f504fa35;hp=8390671425c7d505bde2f92eab8ec5721c1658a0;hpb=b50ab1c348bba9622c85ab9f53a1519d94a3f1b7;p=lilypond.git diff --git a/python/musicxml.py b/python/musicxml.py index 8390671425..e9d1ef8bed 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -1,14 +1,23 @@ +# -*- coding: utf-8 -*- import new import string from rational import * import re +import sys +import copy +import lilylib + +_ = lilylib._ + +def error (str): + ly.stderr_write ((_ ("error: %s") % str) + "\n") + def escape_ly_output_string (input_string): return_string = input_string - needs_quotes = re.search ("[0-9\" ,.]", return_string); - return_string = string.replace (return_string, "\"", "\\\"") + needs_quotes = not re.match (u"^[a-zA-ZäöüÜÄÖßñ]*$", return_string); if needs_quotes: - return_string = "\"" + return_string + "\"" + return_string = "\"" + string.replace (return_string, "\"", "\\\"") + "\"" return return_string @@ -42,11 +51,11 @@ class Xml_node: return ''.join ([c.get_text () for c in self._children]) def message (self, msg): - print msg + lilylib.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): @@ -82,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) @@ -122,8 +139,7 @@ class Identification (Xml_node): 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') @@ -134,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') @@ -149,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') @@ -159,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): @@ -189,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') @@ -226,13 +271,15 @@ 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) # returns clef information in the form ("cleftype", position, octave-shift) @@ -263,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): @@ -271,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, + 'long': -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 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): @@ -317,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 + lilylib.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')) @@ -378,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 @@ -402,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 @@ -417,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 () @@ -427,10 +500,35 @@ 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) @@ -465,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): @@ -543,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): @@ -570,7 +762,16 @@ class Music_xml_spanner (Music_xml_node): else: return 0 -class Tuplet(Music_xml_spanner): +class Wedge (Music_xml_spanner): + pass + +class Tuplet (Music_xml_spanner): + pass + +class Bracket (Music_xml_spanner): + pass + +class Dashes (Music_xml_spanner): pass class Slur (Music_xml_spanner): @@ -592,6 +793,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): @@ -606,12 +810,28 @@ class Chord (Music_xml_node): class Dot (Music_xml_node): pass +# Rests in MusicXML are blocks with a inside. This class is only +# for the inner 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 @@ -633,6 +853,32 @@ class Bend (Music_xml_node): 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 @@ -643,15 +889,22 @@ class_dict = { '#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, @@ -659,17 +912,24 @@ class_dict = { '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, } @@ -693,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()