]> git.donarmstrong.com Git - lilypond.git/blob - python/musicxml.py
543e47786bc03b8456f572f9954f04ea31a57769
[lilypond.git] / python / musicxml.py
1 import new
2 import string
3 from rational import *
4 import re
5
6 def escape_ly_output_string (input_string):
7     return_string = input_string
8     needs_quotes = re.search ("[-0-9\" ,._()]", return_string);
9     return_string = string.replace (return_string, "\"", "\\\"")
10     if needs_quotes:
11         return_string = "\"" + return_string + "\""
12     return return_string
13
14
15 class Xml_node:
16     def __init__ (self):
17         self._children = []
18         self._data = None
19         self._original = None
20         self._name = 'xml_node'
21         self._parent = None
22         self._attribute_dict = {}
23         
24     def get_parent (self):
25         return self._parent
26     
27     def is_first (self):
28         return self._parent.get_typed_children (self.__class__)[0] == self
29
30     def original (self):
31         return self._original 
32     def get_name (self):
33         return self._name
34
35     def get_text (self):
36         if self._data:
37             return self._data
38
39         if not self._children:
40             return ''
41
42         return ''.join ([c.get_text () for c in self._children])
43
44     def message (self, msg):
45         print msg
46
47         p = self
48         while p:
49             print '  In: <%s %s>' % (p._name, ' '.join (['%s=%s' % item for item in p._attribute_dict.items()]))
50             p = p.get_parent ()
51         
52     def get_typed_children (self, klass):
53         if not klass:
54             return []
55         else:
56             return [c for c in self._children if isinstance(c, klass)]
57
58     def get_named_children (self, nm):
59         return self.get_typed_children (get_class (nm))
60
61     def get_named_child (self, nm):
62         return self.get_maybe_exist_named_child (nm)
63
64     def get_children (self, predicate):
65         return [c for c in self._children if predicate(c)]
66
67     def get_all_children (self):
68         return self._children
69
70     def get_maybe_exist_named_child (self, name):
71         return self.get_maybe_exist_typed_child (get_class (name))
72
73     def get_maybe_exist_typed_child (self, klass):
74         cn = self.get_typed_children (klass)
75         if len (cn)==0:
76             return None
77         elif len (cn) == 1:
78             return cn[0]
79         else:
80             raise "More than 1 child", klass
81
82     def get_unique_typed_child (self, klass):
83         cn = self.get_typed_children(klass)
84         if len (cn) <> 1:
85             print self.__dict__ 
86             raise 'Child is not unique for', (klass, 'found', cn)
87
88         return cn[0]
89
90 class Music_xml_node (Xml_node):
91     def __init__ (self):
92         Xml_node.__init__ (self)
93         self.duration = Rational (0)
94         self.start = Rational (0)
95
96 class Work (Xml_node):
97     def get_work_information (self, tag):
98         wt = self.get_maybe_exist_named_child (tag)
99         if wt:
100             return wt.get_text ()
101         else:
102             return ''
103       
104     def get_work_title (self):
105         return self.get_work_information ('work-title')
106     def get_work_number (self):
107         return self.get_work_information ('work-number')
108     def get_opus (self):
109         return self.get_work_information ('opus')
110
111 class Identification (Xml_node):
112     def get_rights (self):
113         rights = self.get_maybe_exist_named_child ('rights')
114         if rights:
115             return rights.get_text ()
116         else:
117             return ''
118
119     def get_creator (self, type):
120         creators = self.get_named_children ('creator')
121         # return the first creator tag that has the particular type
122         for i in creators:
123             if hasattr (i, 'type') and i.type == type:
124                 return i.get_text ()
125             else:
126                 return ''
127
128     def get_composer (self):
129         c = self.get_creator ('composer')
130         if c:
131             return c
132         creators = self.get_named_children ('creator')
133         # return the first creator tag that has no type at all
134         for i in creators:
135             if not hasattr (i, 'type'):
136                 return i.get_text ()
137         return c
138     def get_arranger (self):
139         return self.get_creator ('arranger')
140     def get_editor (self):
141         return self.get_creator ('editor')
142     def get_poet (self):
143         return self.get_creator ('poet')
144     
145     def get_encoding_information (self, type):
146         enc = self.get_named_children ('encoding')
147         if enc:
148             children = enc[0].get_named_children (type)
149             if children:
150                 return children[0].get_text ()
151         else:
152             return ''
153       
154     def get_encoding_software (self):
155         return self.get_encoding_information ('software')
156     def get_encoding_date (self):
157         return self.get_encoding_information ('encoding-date')
158     def get_encoding_person (self):
159         return self.get_encoding_information ('encoder')
160     def get_encoding_description (self):
161         return self.get_encoding_information ('encoding-description')
162
163
164 class Duration (Music_xml_node):
165     def get_length (self):
166         dur = int (self.get_text ()) * Rational (1,4)
167         return dur
168
169 class Hash_comment (Music_xml_node):
170     pass
171 class Hash_text (Music_xml_node):
172     pass
173
174 class Pitch (Music_xml_node):
175     def get_step (self):
176         ch = self.get_unique_typed_child (get_class (u'step'))
177         step = ch.get_text ().strip ()
178         return step
179     def get_octave (self):
180         ch = self.get_unique_typed_child (get_class (u'octave'))
181
182         step = ch.get_text ().strip ()
183         return int (step)
184
185     def get_alteration (self):
186         ch = self.get_maybe_exist_typed_child (get_class (u'alter'))
187         alter = 0
188         if ch:
189             alter = int (ch.get_text ().strip ())
190         return alter
191
192 class Measure_element (Music_xml_node):
193     def get_voice_id (self):
194         voice_id = self.get_maybe_exist_named_child ('voice')
195         if voice_id:
196             return voice_id.get_text ()
197         else:
198             return None
199
200     def is_first (self):
201         cn = self._parent.get_typed_children (self.__class__)
202         cn = [c for c in cn if c.get_voice_id () == self.get_voice_id ()]
203         return cn[0] == self
204
205 class Attributes (Measure_element):
206     def __init__ (self):
207         Measure_element.__init__ (self)
208         self._dict = {}
209
210     def set_attributes_from_previous (self, dict):
211         self._dict.update (dict)
212         
213     def read_self (self):
214         for c in self.get_all_children ():
215             self._dict[c.get_name()] = c
216
217     def get_named_attribute (self, name):
218         return self._dict.get (name)
219
220     def get_measure_length (self):
221         (n,d) = self.get_time_signature ()
222         return Rational (n,d)
223         
224     def get_time_signature (self):
225         "return time sig as a (beat, beat-type) tuple"
226
227         try:
228             mxl = self.get_named_attribute ('time')
229             
230             beats = mxl.get_maybe_exist_named_child ('beats')
231             type = mxl.get_maybe_exist_named_child ('beat-type')
232             return (int (beats.get_text ()),
233                     int (type.get_text ()))
234         except KeyError:
235             print 'error: requested time signature, but time sig unknown'
236             return (4, 4)
237
238     # returns clef information in the form ("cleftype", position, octave-shift)
239     def get_clef_information (self):
240         clefinfo = ['G', 2, 0]
241         mxl = self.get_named_attribute ('clef')
242         if not mxl:
243             return clefinfo
244         sign = mxl.get_maybe_exist_named_child ('sign')
245         if sign:
246             clefinfo[0] = sign.get_text()
247         line = mxl.get_maybe_exist_named_child ('line')
248         if line:
249             clefinfo[1] = string.atoi (line.get_text ())
250         octave = mxl.get_maybe_exist_named_child ('clef-octave-change')
251         if octave:
252             clefinfo[2] = string.atoi (octave.get_text ())
253         return clefinfo
254
255     def get_key_signature (self):
256         "return (fifths, mode) tuple"
257
258         key = self.get_named_attribute ('key')
259         mode_node = key.get_maybe_exist_named_child ('mode')
260         mode = 'major'
261         if mode_node:
262             mode = mode_node.get_text ()
263
264         fifths = int (key.get_maybe_exist_named_child ('fifths').get_text ())
265         return (fifths, mode)
266                 
267
268 class Note (Measure_element):
269     def __init__ (self):
270         Measure_element.__init__ (self)
271         self.instrument_name = ''
272         
273     def get_duration_log (self):
274         ch = self.get_maybe_exist_typed_child (get_class (u'type'))
275
276         if ch:
277             log = ch.get_text ().strip()
278             return {'eighth': 3,
279                     'quarter': 2,
280                     'half': 1,
281                     '16th': 4,
282                     '32nd': 5,
283                      'breve': -1,
284                     'long': -2,
285                     'whole': 0}.get (log)
286         else:
287             return 0
288
289     def get_factor (self):
290         return 1
291
292     def get_pitches (self):
293         return self.get_typed_children (get_class (u'pitch'))
294
295 class Part_list (Music_xml_node):
296     def __init__ (self):
297         Music_xml_node.__init__ (self)
298         self._id_instrument_name_dict = {}
299         
300     def generate_id_instrument_dict (self):
301
302         ## not empty to make sure this happens only once.
303         mapping = {1: 1}
304         for score_part in self.get_named_children ('score-part'):
305             for instr in score_part.get_named_children ('score-instrument'):
306                 id = instr.id
307                 name = instr.get_named_child ("instrument-name")
308                 mapping[id] = name.get_text ()
309
310         self._id_instrument_name_dict = mapping
311
312     def get_instrument (self, id):
313         if not self._id_instrument_name_dict:
314             self.generate_id_instrument_dict()
315
316         instrument_name = self._id_instrument_name_dict.get (id)
317         if instrument_name:
318             return instrument_name
319         else:
320             print "Opps, couldn't find instrument for ID=", id
321             return "Grand Piano"
322         
323 class Measure (Music_xml_node):
324     def get_notes (self):
325         return self.get_typed_children (get_class (u'note'))
326
327 class Syllabic (Music_xml_node):
328     def continued (self):
329         text = self.get_text()
330         return (text == "begin") or (text == "middle")
331 class Text (Music_xml_node):
332     pass
333
334 class Lyric (Music_xml_node):
335     def get_number (self):
336         if hasattr (self, 'number'):
337             return self.number
338         else:
339             return -1
340
341     def lyric_to_text (self):
342         continued = False
343         syllabic = self.get_maybe_exist_typed_child (Syllabic)
344         if syllabic:
345             continued = syllabic.continued ()
346         text = self.get_maybe_exist_typed_child (Text)
347         
348         if text:
349             text = text.get_text()
350             # We need to convert soft hyphens to -, otherwise the ascii codec as well
351             # as lilypond will barf on that character
352             text = string.replace( text, u'\xad', '-' )
353         
354         if text == "-" and continued:
355             return "--"
356         elif text == "_" and continued:
357             return "__"
358         elif continued and text:
359             return escape_ly_output_string (text) + " --"
360         elif continued:
361             return "--"
362         elif text:
363             return escape_ly_output_string (text)
364         else:
365             return ""
366
367 class Musicxml_voice:
368     def __init__ (self):
369         self._elements = []
370         self._staves = {}
371         self._start_staff = None
372         self._lyrics = []
373         self._has_lyrics = False
374
375     def add_element (self, e):
376         self._elements.append (e)
377         if (isinstance (e, Note)
378             and e.get_maybe_exist_typed_child (Staff)):
379             name = e.get_maybe_exist_typed_child (Staff).get_text ()
380
381             if not self._start_staff:
382                 self._start_staff = name
383             self._staves[name] = True
384
385         lyrics = e.get_typed_children (Lyric)
386         if not self._has_lyrics:
387           self.has_lyrics = len (lyrics) > 0
388
389         for l in lyrics:
390             nr = l.get_number()
391             if (nr > 0) and not (nr in self._lyrics):
392                 self._lyrics.append (nr)
393
394     def insert (self, idx, e):
395         self._elements.insert (idx, e)
396
397     def get_lyrics_numbers (self):
398         if (len (self._lyrics) == 0) and self._has_lyrics:
399             #only happens if none of the <lyric> tags has a number attribute
400             return [1]
401         else:
402             return self._lyrics
403
404
405
406 class Part (Music_xml_node):
407     def __init__ (self):
408         Music_xml_node.__init__ (self)
409         self._voices = []
410
411     def get_part_list (self):
412         n = self
413         while n and n.get_name() != 'score-partwise':
414             n = n._parent
415
416         return n.get_named_child ('part-list')
417         
418     def interpret (self):
419         """Set durations and starting points."""
420         
421         part_list = self.get_part_list ()
422         
423         now = Rational (0)
424         factor = Rational (1)
425         attributes_dict = {}
426         attributes_object = None
427         measures = self.get_typed_children (Measure)
428         last_moment = Rational (-1)
429         last_measure_position = Rational (-1)
430         for m in measures:
431             measure_start_moment = now
432             measure_position = Rational (0)
433             for n in m.get_all_children ():
434                 if isinstance (n, Hash_text):
435                     continue
436                 dur = Rational (0)
437
438                 if n.__class__ == Attributes:
439                     n.set_attributes_from_previous (attributes_dict)
440                     n.read_self ()
441                     attributes_dict = n._dict.copy ()
442                     attributes_object = n
443                     
444                     factor = Rational (1,
445                                        int (attributes_dict.get ('divisions').get_text ()))
446
447                 
448                 if (n.get_maybe_exist_typed_child (Duration)):
449                     mxl_dur = n.get_maybe_exist_typed_child (Duration)
450                     dur = mxl_dur.get_length () * factor
451                     
452                     if n.get_name() == 'backup':
453                         dur = - dur
454                     if n.get_maybe_exist_typed_child (Grace):
455                         dur = Rational (0)
456
457                     rest = n.get_maybe_exist_typed_child (Rest)
458                     if (rest
459                         and attributes_object
460                         and attributes_object.get_measure_length () == dur):
461
462                         rest._is_whole_measure = True
463
464                 if (dur > Rational (0) 
465                     and n.get_maybe_exist_typed_child (Chord)):
466                     now = last_moment
467                     measure_position = last_measure_position
468
469                 n._when = now
470                 n._measure_position = measure_position
471                 n._duration = dur
472                 if dur > Rational (0):
473                     last_moment = now
474                     last_measure_position = measure_position
475                     now += dur
476                     measure_position += dur
477                 if n._name == 'note':
478                     instrument = n.get_maybe_exist_named_child ('instrument')
479                     if instrument:
480                         n.instrument_name = part_list.get_instrument (instrument.id)
481
482             if attributes_object:
483                 length = attributes_object.get_measure_length ()
484                 new_now = measure_start_moment + length
485                 
486                 if now <> new_now:
487                     problem = 'incomplete'
488                     if now > new_now:
489                         problem = 'overfull'
490
491                     ## only for verbose operation.
492                     if problem <> 'incomplete':
493                         m.message ('%s measure? Expected: %s, Difference: %s' % (problem, now, new_now - now))
494
495                 now = new_now
496
497     def extract_voices (part):
498         voices = {}
499         measures = part.get_typed_children (Measure)
500         elements = []
501         for m in measures:
502             elements.extend (m.get_all_children ())
503
504         start_attr = None
505         for n in elements:
506             voice_id = n.get_maybe_exist_typed_child (get_class ('voice'))
507
508             # TODO: If the first element of a voice is a dynamics entry,
509             #       then voice_id is not yet set! Thus it will currently be ignored
510             if not (voice_id or isinstance (n, Attributes) or isinstance (n, Direction) ):
511                 continue
512
513             if isinstance (n, Attributes) and not start_attr:
514                 start_attr = n
515                 continue
516
517             if isinstance (n, Attributes) or isinstance (n, Direction):
518                 for v in voices.values ():
519                     v.add_element (n)
520                 continue
521
522             id = voice_id.get_text ()
523             if not voices.has_key (id):
524                 voices[id] = Musicxml_voice()
525
526             voices[id].add_element (n)
527
528         if start_attr:
529             for (k,v) in voices.items ():
530                 v.insert (0, start_attr)
531
532         part._voices = voices
533
534     def get_voices (self):
535         return self._voices
536
537 class Notations (Music_xml_node):
538     def get_tie (self):
539         ts = self.get_named_children ('tied')
540         starts = [t for t in ts if t.type == 'start']
541         if starts:
542             return starts[0]
543         else:
544             return None
545
546     def get_tuplet (self):
547         return self.get_maybe_exist_typed_child (Tuplet)
548
549 class Time_modification(Music_xml_node):
550     def get_fraction (self):
551         b = self.get_maybe_exist_named_child ('actual-notes')
552         a = self.get_maybe_exist_named_child ('normal-notes')
553         return (int(a.get_text ()), int (b.get_text ()))
554
555 class Accidental (Music_xml_node):
556     def __init__ (self):
557         Music_xml_node.__init__ (self)
558         self.editorial = False
559         self.cautionary = False
560
561 class Music_xml_spanner (Music_xml_node):
562     def get_type (self):
563         if hasattr (self, 'type'):
564             return self.type
565         else:
566             return 0
567     def get_size (self):
568         if hasattr (self, 'size'):
569             return string.atoi (self.size)
570         else:
571             return 0
572
573 class Tuplet(Music_xml_spanner):
574     pass
575
576 class Slur (Music_xml_spanner):
577     def get_type (self):
578         return self.type
579
580 class Beam (Music_xml_spanner):
581     def get_type (self):
582         return self.get_text ()
583     def is_primary (self):
584         return self.number == "1"
585
586 class Wavy_line (Music_xml_spanner):
587     pass
588     
589 class Pedal (Music_xml_spanner):
590     pass
591
592 class Glissando (Music_xml_spanner):
593     pass
594
595 class Octave_shift (Music_xml_spanner):
596     # default is 8 for the octave-shift!
597     def get_size (self):
598         if hasattr (self, 'size'):
599             return string.atoi (self.size)
600         else:
601             return 8
602
603 class Chord (Music_xml_node):
604     pass
605
606 class Dot (Music_xml_node):
607     pass
608
609 class Rest (Music_xml_node):
610     def __init__ (self):
611         Music_xml_node.__init__ (self)
612         self._is_whole_measure = False
613     def is_whole_measure (self):
614         return self._is_whole_measure
615
616 class Type (Music_xml_node):
617     pass
618 class Grace (Music_xml_node):
619     pass
620 class Staff (Music_xml_node):
621     pass
622
623 class Direction (Music_xml_node):
624     pass
625 class DirType (Music_xml_node):
626     pass
627
628 class Bend (Music_xml_node):
629     def bend_alter (self):
630         alter = self.get_maybe_exist_named_child ('bend-alter')
631         if alter:
632             return alter.get_text()
633         else:
634             return 0
635
636
637
638 ## need this, not all classes are instantiated
639 ## for every input file. Only add those classes, that are either directly
640 ## used by class name or extend Music_xml_node in some way!
641 class_dict = {
642         '#comment': Hash_comment,
643         '#text': Hash_text,
644         'accidental': Accidental,
645         'attributes': Attributes,
646         'beam' : Beam,
647         'bend' : Bend,
648         'chord': Chord,
649         'dot': Dot,
650         'direction': Direction,
651         'direction-type': DirType,
652         'duration': Duration,
653         'glissando': Glissando,
654         'grace': Grace,
655         'identification': Identification,
656         'lyric': Lyric,
657         'measure': Measure,
658         'notations': Notations,
659         'note': Note,
660         'octave-shift': Octave_shift,
661         'part': Part,
662         'part-list': Part_list,
663         'pedal': Pedal,
664         'pitch': Pitch,
665         'rest': Rest,
666         'slur': Slur,
667         'staff': Staff,
668         'syllabic': Syllabic,
669         'text': Text,
670         'time-modification': Time_modification,
671         'tuplet': Tuplet,
672         'type': Type,
673         'wavy-line': Wavy_line,
674         'work': Work,
675 }
676
677 def name2class_name (name):
678     name = name.replace ('-', '_')
679     name = name.replace ('#', 'hash_')
680     name = name[0].upper() + name[1:].lower()
681
682     return str (name)
683
684 def get_class (name):
685     classname = class_dict.get (name)
686     if classname:
687         return classname
688     else:
689         class_name = name2class_name (name)
690         klass = new.classobj (class_name, (Music_xml_node,) , {})
691         class_dict[name] = klass
692         return klass
693         
694 def lxml_demarshal_node (node):
695     name = node.tag
696
697     if name is None:
698         return None
699     klass = get_class (name)
700     py_node = klass()
701     
702     py_node._original = node
703     py_node._name = name
704     py_node._data = node.text
705     py_node._children = [lxml_demarshal_node (cn) for cn in node.getchildren()]
706     py_node._children = filter (lambda x: x, py_node._children)
707     
708     for c in py_node._children:
709         c._parent = py_node
710
711     for (k,v) in node.items ():
712         py_node.__dict__[k] = v
713         py_node._attribute_dict[k] = v
714
715     return py_node
716
717 def minidom_demarshal_node (node):
718     name = node.nodeName
719
720     klass = get_class (name)
721     py_node = klass()
722     py_node._name = name
723     py_node._children = [minidom_demarshal_node (cn) for cn in node.childNodes]
724     for c in py_node._children:
725         c._parent = py_node
726
727     if node.attributes:
728         for (nm, value) in node.attributes.items():
729             py_node.__dict__[nm] = value
730             py_node._attribute_dict[nm] = value
731             
732     py_node._data = None
733     if node.nodeType == node.TEXT_NODE and node.data:
734         py_node._data = node.data 
735
736     py_node._original = node
737     return py_node
738
739
740 if __name__  == '__main__':
741         import lxml.etree
742         
743         tree = lxml.etree.parse ('beethoven.xml')
744         mxl_tree = lxml_demarshal_node (tree.getroot ())
745         ks = class_dict.keys()
746         ks.sort()
747         print '\n'.join (ks)