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