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