]> git.donarmstrong.com Git - lilypond.git/blob - scripts/musicxml2ly.py
Merge branch 'master' of ssh+git://gpercival@git.sv.gnu.org/srv/git/lilypond
[lilypond.git] / scripts / musicxml2ly.py
1 #!@TARGET_PYTHON@
2
3 import optparse
4 import sys
5 import re
6 import os
7 import string
8 from gettext import gettext as _
9
10
11
12 for d in ['@lilypond_datadir@',
13           '@lilypond_libdir@']:
14     sys.path.insert (0, os.path.join (d, 'python'))
15
16 # dynamic relocation, for GUB binaries.
17 bindir = os.path.abspath (os.path.split (sys.argv[0])[0])
18 for p in ['share', 'lib']:
19     datadir = os.path.abspath (bindir + '/../%s/lilypond/current/python/' % p)
20     sys.path.insert (0, datadir)
21
22
23
24
25 import lilylib as ly
26
27 import musicxml
28 import musicexp
29
30 from rational import Rational
31
32
33 def progress (str):
34     sys.stderr.write (str + '\n')
35     sys.stderr.flush ()
36     
37
38 def musicxml_duration_to_lily (mxl_note):
39     d = musicexp.Duration ()
40     if mxl_note.get_maybe_exist_typed_child (musicxml.Type):
41         d.duration_log = mxl_note.get_duration_log ()
42     else:
43         d.duration_log = 0
44
45     d.dots = len (mxl_note.get_typed_children (musicxml.Dot))
46     d.factor = mxl_note._duration / d.get_length ()
47
48     return d         
49
50 def group_tuplets (music_list, events):
51
52
53     """Collect Musics from
54     MUSIC_LIST demarcated by EVENTS_LIST in TimeScaledMusic objects.
55     """
56
57     
58     indices = []
59
60     j = 0
61     for (ev_chord, tuplet_elt, fraction) in events:
62         while (j < len (music_list)):
63             if music_list[j]== ev_chord:
64                 break
65             j += 1
66         if tuplet_elt.type == 'start':
67             indices.append ((j, None, fraction))
68         elif tuplet_elt.type == 'stop':
69             indices[-1] = (indices[-1][0], j, indices[-1][2])
70
71     new_list = []
72     last = 0
73     for (i1, i2, frac) in indices:
74         if i1 >= i2:
75             continue
76
77         new_list.extend (music_list[last:i1])
78         seq = musicexp.SequentialMusic ()
79         last = i2 + 1
80         seq.elements = music_list[i1:last]
81
82         tsm = musicexp.TimeScaledMusic ()
83         tsm.element = seq
84
85         tsm.numerator = frac[0]
86         tsm.denominator  = frac[1]
87
88         new_list.append (tsm)
89
90     new_list.extend (music_list[last:])
91     return new_list
92
93
94 def musicxml_clef_to_lily (attributes):
95     change = musicexp.ClefChange ()
96     change.type = attributes.get_clef_sign ()
97     return change
98     
99 def musicxml_time_to_lily (attributes):
100     (beats, type) = attributes.get_time_signature ()
101
102     change = musicexp.TimeSignatureChange()
103     change.fraction = (beats, type)
104     
105     return change
106
107 def musicxml_key_to_lily (attributes):
108     start_pitch  = musicexp.Pitch ()
109     (fifths, mode) = attributes.get_key_signature () 
110     try:
111         (n,a) = {
112             'major' : (0,0),
113             'minor' : (5,0),
114             }[mode]
115         start_pitch.step = n
116         start_pitch.alteration = a
117     except  KeyError:
118         print 'unknown mode', mode
119
120     fifth = musicexp.Pitch()
121     fifth.step = 4
122     if fifths < 0:
123         fifths *= -1
124         fifth.step *= -1
125         fifth.normalize ()
126     
127     for x in range (fifths):
128         start_pitch = start_pitch.transposed (fifth)
129
130     start_pitch.octave = 0
131
132     change = musicexp.KeySignatureChange()
133     change.mode = mode
134     change.tonic = start_pitch
135     return change
136     
137 def musicxml_attributes_to_lily (attrs):
138     elts = []
139     attr_dispatch =  {
140         'clef': musicxml_clef_to_lily,
141         'time': musicxml_time_to_lily,
142         'key': musicxml_key_to_lily
143     }
144     for (k, func) in attr_dispatch.items ():
145         childs = attrs.get_named_children (k)
146
147         ## ugh: you get clefs spread over staves for piano
148         if childs:
149             elts.append (func (attrs))
150     
151     return elts
152
153 spanner_event_dict = {
154     'slur' : musicexp.SlurEvent,
155     'beam' : musicexp.BeamEvent,
156 }        
157 spanner_type_dict = {
158     'start': -1,
159     'begin': -1,
160     'stop': 1,
161     'end' : 1
162 }
163
164 def musicxml_spanner_to_lily_event (mxl_event):
165     ev = None
166     
167     name = mxl_event.get_name()
168     try:
169         func = spanner_event_dict[name]
170         ev = func()
171     except KeyError:
172         print 'unknown span event ', mxl_event
173
174     try:
175         key = mxl_event.get_type ()
176         ev.span_direction = spanner_type_dict[key]
177     except KeyError:
178         print 'unknown span type', key, 'for', name
179
180     return ev
181
182 instrument_drumtype_dict = {
183     'Acoustic Snare Drum': 'acousticsnare',
184     'Side Stick': 'sidestick',
185     'Open Triangle': 'opentriangle',
186     'Mute Triangle': 'mutetriangle',
187     'Tambourine': 'tambourine',
188     
189 }
190
191 def musicxml_note_to_lily_main_event (n):
192     pitch  = None
193     duration = None
194         
195     mxl_pitch = n.get_maybe_exist_typed_child (musicxml.Pitch)
196     event = None
197     if mxl_pitch:
198         pitch = musicxml_pitch_to_lily (mxl_pitch)
199         event = musicexp.NoteEvent()
200         event.pitch = pitch
201
202         acc = n.get_maybe_exist_named_child ('accidental')
203         if acc:
204             # let's not force accs everywhere. 
205             event.cautionary = acc.editorial
206         
207     elif n.get_maybe_exist_typed_child (musicxml.Rest):
208         event = musicexp.RestEvent()
209     elif n.instrument_name:
210         event = musicexp.NoteEvent ()
211         event.drum_type = instrument_drumtype_dict[n.instrument_name]
212         
213     
214     if not event:
215         n.message ("cannot find suitable event")
216
217     event.duration = musicxml_duration_to_lily (n)
218     return event
219
220
221 ## todo
222 class NegativeSkip:
223     def __init__ (self, here, dest):
224         self.here = here
225         self.dest = dest
226
227 class LilyPondVoiceBuilder:
228     def __init__ (self):
229         self.elements = []
230         self.end_moment = Rational (0)
231         self.begin_moment = Rational (0)
232         self.pending_multibar = Rational (0)
233
234     def _insert_multibar (self):
235         r = musicexp.MultiMeasureRest ()
236         r.duration = musicexp.Duration()
237         r.duration.duration_log = 0
238         r.duration.factor = self.pending_multibar
239         self.elements.append (r)
240         self.begin_moment = self.end_moment
241         self.end_moment = self.begin_moment + self.pending_multibar
242         self.pending_multibar = Rational (0)
243         
244     def add_multibar_rest (self, duration):
245         self.pending_multibar += duration
246         
247         
248     def add_music (self, music, duration):
249         assert isinstance (music, musicexp.Music)
250         if self.pending_multibar > Rational (0):
251             self._insert_multibar ()
252
253         self.elements.append (music)
254         self.begin_moment = self.end_moment
255         self.end_moment = self.begin_moment + duration 
256
257     def add_bar_check (self, number):
258         b = musicexp.BarCheck ()
259         b.bar_number = number
260         self.add_music (b, Rational (0))
261
262     def jumpto (self, moment):
263         current_end = self.end_moment + self.pending_multibar
264         diff = moment - current_end
265         
266         if diff < Rational (0):
267             print 'Negative skip', diff
268             diff = Rational (0)
269
270         if diff > Rational (0):
271             skip = musicexp.SkipEvent()
272             skip.duration.duration_log = 0
273             skip.duration.factor = diff
274
275             evc = musicexp.EventChord ()
276             evc.elements.append (skip)
277             self.add_music (evc, diff)
278                 
279     def last_event_chord (self, starting_at):
280
281         value = None
282         if (self.elements
283             and isinstance (self.elements[-1], musicexp.EventChord)
284             and self.begin_moment == starting_at):
285             value = self.elements[-1]
286         else:
287             self.jumpto (starting_at)
288             value = None
289
290         return value
291         
292     def correct_negative_skip (self, goto):
293         self.end_moment = goto
294         self.begin_moment = goto
295         evc = musicexp.EventChord ()
296         self.elements.append (evc)
297         
298 def musicxml_voice_to_lily_voice (voice):
299     tuplet_events = []
300     modes_found = {}
301
302     voice_builder = LilyPondVoiceBuilder()
303
304     for n in voice._elements:
305         if n.get_name () == 'forward':
306             continue
307
308         if not n.get_maybe_exist_named_child ('chord'):
309             try:
310                 voice_builder.jumpto (n._when)
311             except NegativeSkip, neg:
312                 voice_builder.correct_negative_skip (n._when)
313                 n.message ("Negative skip? from %s to %s, diff %s" % (neg.here, neg.dest, neg.dest - neg.here))
314             
315         if isinstance (n, musicxml.Attributes):
316             if n.is_first () and n._measure_position == Rational (0):
317                 try:
318                     number = int (n.get_parent ().number)
319                 except ValueError:
320                     number = 0
321                 
322                 voice_builder.add_bar_check (number)
323             for a in musicxml_attributes_to_lily (n):
324                 voice_builder.add_music (a, Rational (0))
325             continue
326
327         if not n.__class__.__name__ == 'Note':
328             print 'not a Note or Attributes?', n
329             continue
330
331         rest = n.get_maybe_exist_typed_child (musicxml.Rest)
332         if (rest
333             and rest.is_whole_measure ()):
334
335             voice_builder.add_multibar_rest (n._duration)
336             continue
337
338         if n.is_first () and n._measure_position == Rational (0):
339             try: 
340                 num = int (n.get_parent ().number)
341             except ValueError:
342                 num = 0
343             voice_builder.add_bar_check (num)
344         
345         main_event = musicxml_note_to_lily_main_event (n)
346
347         try:
348             if main_event.drum_type:
349                 modes_found['drummode'] = True
350         except AttributeError:
351             pass
352
353
354         ev_chord = voice_builder.last_event_chord (n._when)
355         if not ev_chord: 
356             ev_chord = musicexp.EventChord()
357             voice_builder.add_music (ev_chord, n._duration)
358
359         ev_chord.append (main_event)
360         
361         notations = n.get_maybe_exist_typed_child (musicxml.Notations)
362         tuplet_event = None
363         span_events = []
364         if notations:
365             if notations.get_tuplet():
366                 tuplet_event = notations.get_tuplet()
367                 mod = n.get_maybe_exist_typed_child (musicxml.Time_modification)
368                 frac = (1,1)
369                 if mod:
370                     frac = mod.get_fraction ()
371                 
372                 tuplet_events.append ((ev_chord, tuplet_event, frac))
373
374             slurs = [s for s in notations.get_named_children ('slur')
375                 if s.get_type () in ('start','stop')]
376             if slurs:
377                 if len (slurs) > 1:
378                     print 'more than 1 slur?'
379
380                 lily_ev = musicxml_spanner_to_lily_event (slurs[0])
381                 ev_chord.append (lily_ev)
382
383             mxl_tie = notations.get_tie ()
384             if mxl_tie and mxl_tie.type == 'start':
385                 ev_chord.append (musicexp.TieEvent ())
386
387         mxl_beams = [b for b in n.get_named_children ('beam')
388                      if (b.get_type () in ('begin', 'end')
389                          and b.is_primary ())] 
390         if mxl_beams:
391             beam_ev = musicxml_spanner_to_lily_event (mxl_beams[0])
392             if beam_ev:
393                 ev_chord.append (beam_ev)
394             
395         if tuplet_event:
396             mod = n.get_maybe_exist_typed_child (musicxml.Time_modification)
397             frac = (1,1)
398             if mod:
399                 frac = mod.get_fraction ()
400                 
401             tuplet_events.append ((ev_chord, tuplet_event, frac))
402
403     ## force trailing mm rests to be written out.   
404     voice_builder.add_music (musicexp.EventChord (), Rational (0))
405     
406     ly_voice = group_tuplets (voice_builder.elements, tuplet_events)
407
408     seq_music = musicexp.SequentialMusic()
409
410     if 'drummode' in modes_found.keys ():
411         ## \key <pitch> barfs in drummode.
412         ly_voice = [e for e in ly_voice
413                     if not isinstance(e, musicexp.KeySignatureChange)]
414     
415     seq_music.elements = ly_voice
416
417     
418     
419     if len (modes_found) > 1:
420        print 'Too many modes found', modes_found.keys ()
421
422     return_value = seq_music
423     for mode in modes_found.keys ():
424         v = musicexp.ModeChangingMusicWrapper()
425         v.element = return_value
426         v.mode = mode
427         return_value = v
428     
429     return return_value
430
431
432 def musicxml_id_to_lily (id):
433     digits = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
434               'nine', 'ten']
435     
436     for dig in digits:
437         d = digits.index (dig) + 1
438         dig = dig[0].upper() + dig[1:]
439         id = re.sub ('%d' % d, dig, id)
440
441     id = re.sub  ('[^a-zA-Z]', 'X', id)
442     return id
443
444
445 def musicxml_pitch_to_lily (mxl_pitch):
446     p = musicexp.Pitch()
447     p.alteration = mxl_pitch.get_alteration ()
448     p.step = (ord (mxl_pitch.get_step ()) - ord ('A') + 7 - 2) % 7
449     p.octave = mxl_pitch.get_octave () - 4
450     return p
451
452 def voices_in_part (part):
453     """Return a Name -> Voice dictionary for PART"""
454     part.interpret ()
455     part.extract_voices ()
456     voice_dict = part.get_voices ()
457
458     return voice_dict
459
460 def voices_in_part_in_parts (parts):
461     """return a Part -> Name -> Voice dictionary"""
462     return dict([(p, voices_in_part (p)) for p in parts])
463
464
465 def get_all_voices (parts):
466     all_voices = voices_in_part_in_parts (parts)
467
468     all_ly_voices = {}
469     for p, name_voice in all_voices.items ():
470
471         part_ly_voices = {}
472         for n, v in name_voice.items ():
473             progress ("Converting to LilyPond expressions...")
474             part_ly_voices[n] = (musicxml_voice_to_lily_voice (v), v)
475
476         all_ly_voices[p] = part_ly_voices
477         
478     return all_ly_voices
479
480
481 def option_parser ():
482     p = ly.get_option_parser(usage=_ ("musicxml2ly FILE.xml"),
483                              version=('''%prog (LilyPond) @TOPLEVEL_VERSION@\n\n'''
484                                       +
485 _ ("""This program is free software.  It is covered by the GNU General Public
486 License and you are welcome to change it and/or distribute copies of it
487 under certain conditions.  Invoke as `%s --warranty' for more
488 information.""") % 'lilypond'
489 + """
490 Copyright (c) 2005--2006 by
491     Han-Wen Nienhuys <hanwen@xs4all.nl> and
492     Jan Nieuwenhuizen <janneke@gnu.org>
493 """),
494                              description=_ ("Convert %s to LilyPond input.") % 'MusicXML' + "\n")
495     p.add_option ('-v', '--verbose',
496                   action="store_true",
497                   dest='verbose',
498                   help=_ ("be verbose"))
499
500     p.add_option ('', '--lxml',
501                   action="store_true",
502                   default=False,
503                   dest="use_lxml",
504                   help=_ ("Use lxml.etree; uses less memory and cpu time."))
505     
506     p.add_option ('-o', '--output',
507                   metavar=_ ("FILE"),
508                   action="store",
509                   default=None,
510                   type='string',
511                   dest='output_name',
512                   help=_ ("set output filename to FILE"))
513     p.add_option_group ('bugs',
514                         description=(_ ("Report bugs via")
515                                      + ''' http://post.gmane.org/post.php'''
516                                      '''?group=gmane.comp.gnu.lilypond.bugs\n'''))
517     return p
518
519 def music_xml_voice_name_to_lily_name (part, name):
520     str = "Part%sVoice%s" % (part.id, name)
521     return musicxml_id_to_lily (str) 
522
523 def print_voice_definitions (printer, voices):
524     for (part, nv_dict) in voices.items():
525         
526         for (name, (voice, mxlvoice)) in nv_dict.items ():
527             k = music_xml_voice_name_to_lily_name (part, name)
528             printer.dump ('%s = ' % k)
529             voice.print_ly (printer)
530             printer.newline()
531
532             
533 def uniq_list (l):
534     return dict ([(elt,1) for elt in l]).keys ()
535     
536 def print_score_setup (printer, part_list, voices):
537     part_dict = dict ([(p.id, p) for p in voices.keys ()]) 
538
539     printer ('<<')
540     printer.newline ()
541     for part_definition in part_list:
542         part_name = part_definition.id
543         try:
544             part = part_dict[part_name]
545         except KeyError:
546             print 'unknown part in part-list:', part_name
547             continue
548
549         nv_dict = voices[part]
550         staves = reduce (lambda x,y: x+ y,
551                 [mxlvoice._staves.keys ()
552                  for (v, mxlvoice) in nv_dict.values ()],
553                 [])
554
555         if len (staves) > 1:
556             staves = uniq_list (staves)
557             staves.sort ()
558             printer ('\\context PianoStaff << ')
559             printer.newline ()
560             
561             for s in staves:
562                 staff_voices = [music_xml_voice_name_to_lily_name (part, voice_name)
563                         for (voice_name, (v, mxlvoice)) in nv_dict.items ()
564                         if mxlvoice._start_staff == s]
565                 
566                 printer ('\\context Staff = "%s" << ' % s)
567                 printer.newline ()
568                 for v in staff_voices:
569                     printer ('\\context Voice = "%s"  \\%s' % (v,v))
570                     printer.newline ()
571                 printer ('>>')
572                 printer.newline ()
573                 
574             printer ('>>')
575             printer.newline ()
576             
577         else:
578             printer ('\\new Staff <<')
579             printer.newline ()
580             for (n,v) in nv_dict.items ():
581
582                 n = music_xml_voice_name_to_lily_name (part, n) 
583                 printer ('\\context Voice = "%s"  \\%s' % (n,n))
584             printer ('>>')
585             printer.newline ()
586             
587     printer ('>>')
588     printer.newline ()
589
590 def print_ly_preamble (printer, filename):
591     printer.dump_version ()
592     printer.print_verbatim ('%% converted from %s\n' % filename)
593
594 def read_musicxml (filename, use_lxml):
595     if use_lxml:
596         import lxml.etree
597         
598         tree = lxml.etree.parse (filename)
599         mxl_tree = musicxml.lxml_demarshal_node (tree.getroot ())
600         return mxl_tree
601     else:
602         from xml.dom import minidom, Node
603         
604         doc = minidom.parse(filename)
605         node = doc.documentElement
606         return musicxml.minidom_demarshal_node (node)
607
608     return None
609
610
611 def convert (filename, options):
612     progress ("Reading MusicXML from %s ..." % filename)
613     
614     tree = read_musicxml (filename, options.use_lxml)
615
616     part_list = []
617     id_instrument_map = {}
618     if tree.get_maybe_exist_typed_child (musicxml.Part_list):
619         mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list)
620         part_list = mxl_pl.get_named_children ("score-part")
621         
622     parts = tree.get_typed_children (musicxml.Part)
623     voices = get_all_voices (parts)
624
625     if not options.output_name:
626         options.output_name = os.path.basename (filename) 
627         options.output_name = os.path.splitext (options.output_name)[0]
628
629
630     defs_ly_name = options.output_name + '-defs.ly'
631     driver_ly_name = options.output_name + '.ly'
632
633     printer = musicexp.Output_printer()
634     progress ("Output to `%s'" % defs_ly_name)
635     printer.set_file (open (defs_ly_name, 'w'))
636
637     print_ly_preamble (printer, filename)
638     print_voice_definitions (printer, voices)
639     
640     printer.close ()
641     
642     
643     progress ("Output to `%s'" % driver_ly_name)
644     printer = musicexp.Output_printer()
645     printer.set_file (open (driver_ly_name, 'w'))
646     print_ly_preamble (printer, filename)
647     printer.dump (r'\include "%s"' % defs_ly_name)
648     print_score_setup (printer, part_list, voices)
649     printer.newline ()
650
651     return voices
652
653
654 def main ():
655     opt_parser = option_parser()
656
657     (options, args) = opt_parser.parse_args ()
658     if not args:
659         opt_parser.print_usage()
660         sys.exit (2)
661
662     voices = convert (args[0], options)
663
664 if __name__ == '__main__':
665     main()