X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=python%2Fmusicxml.py;h=a251afa5f313427575a4a80defcac1b87b9fb7ed;hb=794a037c19a838dd56ceb59e2280059fb80d944a;hp=519a0c3f6c531e80512987a1cbbbf00f0187da42;hpb=9f3572d98bb948c9689cd1f75401a029451fa001;p=lilypond.git diff --git a/python/musicxml.py b/python/musicxml.py index 519a0c3f6c..a251afa5f3 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -1,5 +1,18 @@ +# -*- coding: utf-8 -*- import new +import string from rational import * +import re +import sys +import copy + +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 +44,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 +70,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,17 +84,95 @@ 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) 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 +181,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 +228,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,36 +239,54 @@ 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 () 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): @@ -180,26 +294,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_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} [log] - else: - return 0 + 'whole': 0, + 'breve': -1, + 'long': -2}.get (log, 0) + else: + self.message ("Encountered note at %s without %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 (class_dict[u'pitch']) + return self.get_typed_children (get_class (u'pitch')) class Part_list (Music_xml_node): def __init__ (self): @@ -222,22 +340,74 @@ 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 __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 (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 +415,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 tags has a number attribute + return [1] + else: + return self._lyrics + class Part (Music_xml_node): @@ -278,10 +464,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 + prvious_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: + 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: @@ -291,7 +504,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,69 +527,162 @@ 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 (class_dict['voice']) + voice_id = n.get_maybe_exist_typed_child (get_class ('voice')) - if not (voice_id or isinstance (n, Attributes)): + 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): - 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() - - voices[id].add_element (n) + 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) + + # 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 () + 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 @@ -395,8 +701,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 +711,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 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 Alter (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 - -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 +792,93 @@ 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 + +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. +## 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, + 'barline': Barline, + 'bar-style': BarStyle, 'beam' : Beam, + 'bend' : Bend, 'chord': Chord, 'dot': Dot, + 'direction': Direction, + 'direction-type': DirType, 'duration': Duration, + 'frame': Frame, + 'frame-note': Frame_Note, + 'glissando': Glissando, 'grace': Grace, - 'instrument': Instrument, - 'mode' : Mode, + '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, + '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, + 'words': Words, + 'work': Work, } def name2class_name (name): @@ -487,9 +889,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