]> git.donarmstrong.com Git - lilypond.git/blob - scripts/musicxml2ly.py
Fix #129, #228, #7
[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             raise NegativeSkip(current_end, moment)
268
269         if diff > Rational (0):
270             skip = musicexp.SkipEvent()
271             skip.duration.duration_log = 0
272             skip.duration.factor = diff
273
274             evc = musicexp.EventChord ()
275             evc.elements.append (skip)
276             self.add_music (evc, diff)
277                 
278     def last_event_chord (self, starting_at):
279
280         value = None
281         if (self.elements
282             and isinstance (self.elements[-1], musicexp.EventChord)
283             and self.begin_moment == starting_at):
284             value = self.elements[-1]
285         else:
286             self.jumpto (starting_at)
287             value = None
288
289         return value
290         
291     def correct_negative_skip (self, goto):
292         self.end_moment = goto
293         self.begin_moment = goto
294         evc = musicexp.EventChord ()
295         self.elements.append (evc)
296         
297 def musicxml_voice_to_lily_voice (voice):
298     tuplet_events = []
299     modes_found = {}
300
301     voice_builder = LilyPondVoiceBuilder()
302
303     for n in voice._elements:
304         if n.get_name () == 'forward':
305             continue
306
307         if not n.get_maybe_exist_named_child ('chord'):
308             try:
309                 voice_builder.jumpto (n._when)
310             except NegativeSkip, neg:
311                 voice_builder.correct_negative_skip (n._when)
312                 n.message ("Negative skip? from %s to %s, diff %s" % (neg.here, neg.dest, neg.dest - neg.here))
313             
314         if isinstance (n, musicxml.Attributes):
315             if n.is_first () and n._measure_position == Rational (0):
316                 try:
317                     number = int (n.get_parent ().number)
318                 except ValueError:
319                     number = 0
320                 
321                 voice_builder.add_bar_check (number)
322             for a in musicxml_attributes_to_lily (n):
323                 voice_builder.add_music (a, Rational (0))
324             continue
325
326         if not n.__class__.__name__ == 'Note':
327             print 'not a Note or Attributes?', n
328             continue
329
330         rest = n.get_maybe_exist_typed_child (musicxml.Rest)
331         if (rest
332             and rest.is_whole_measure ()):
333
334             voice_builder.add_multibar_rest (n._duration)
335             continue
336
337         if n.is_first () and n._measure_position == Rational (0):
338             try: 
339                 num = int (n.get_parent ().number)
340             except ValueError:
341                 num = 0
342             voice_builder.add_bar_check (num)
343         
344         main_event = musicxml_note_to_lily_main_event (n)
345
346         try:
347             if main_event.drum_type:
348                 modes_found['drummode'] = True
349         except AttributeError:
350             pass
351
352
353         ev_chord = voice_builder.last_event_chord (n._when)
354         if not ev_chord: 
355             ev_chord = musicexp.EventChord()
356             voice_builder.add_music (ev_chord, n._duration)
357
358         ev_chord.append (main_event)
359         
360         notations = n.get_maybe_exist_typed_child (musicxml.Notations)
361         tuplet_event = None
362         span_events = []
363         if notations:
364             if notations.get_tuplet():
365                 tuplet_event = notations.get_tuplet()
366                 mod = n.get_maybe_exist_typed_child (musicxml.Time_modification)
367                 frac = (1,1)
368                 if mod:
369                     frac = mod.get_fraction ()
370                 
371                 tuplet_events.append ((ev_chord, tuplet_event, frac))
372
373             slurs = [s for s in notations.get_named_children ('slur')
374                 if s.get_type () in ('start','stop')]
375             if slurs:
376                 if len (slurs) > 1:
377                     print 'more than 1 slur?'
378
379                 lily_ev = musicxml_spanner_to_lily_event (slurs[0])
380                 ev_chord.append (lily_ev)
381
382             mxl_tie = notations.get_tie ()
383             if mxl_tie and mxl_tie.type == 'start':
384                 ev_chord.append (musicexp.TieEvent ())
385
386         mxl_beams = [b for b in n.get_named_children ('beam')
387                      if (b.get_type () in ('begin', 'end')
388                          and b.is_primary ())] 
389         if mxl_beams:
390             beam_ev = musicxml_spanner_to_lily_event (mxl_beams[0])
391             if beam_ev:
392                 ev_chord.append (beam_ev)
393             
394         if tuplet_event:
395             mod = n.get_maybe_exist_typed_child (musicxml.Time_modification)
396             frac = (1,1)
397             if mod:
398                 frac = mod.get_fraction ()
399                 
400             tuplet_events.append ((ev_chord, tuplet_event, frac))
401
402     ## force trailing mm rests to be written out.   
403     voice_builder.add_music (musicexp.EventChord (), Rational (0))
404     
405     ly_voice = group_tuplets (voice_builder.elements, tuplet_events)
406
407     seq_music = musicexp.SequentialMusic()
408
409     if 'drummode' in modes_found.keys ():
410         ## \key <pitch> barfs in drummode.
411         ly_voice = [e for e in ly_voice
412                     if not isinstance(e, musicexp.KeySignatureChange)]
413     
414     seq_music.elements = ly_voice
415
416     
417     
418     if len (modes_found) > 1:
419        print 'Too many modes found', modes_found.keys ()
420
421     return_value = seq_music
422     for mode in modes_found.keys ():
423         v = musicexp.ModeChangingMusicWrapper()
424         v.element = return_value
425         v.mode = mode
426         return_value = v
427     
428     return return_value
429
430
431 def musicxml_id_to_lily (id):
432     digits = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
433               'nine', 'ten']
434     
435     for dig in digits:
436         d = digits.index (dig) + 1
437         dig = dig[0].upper() + dig[1:]
438         id = re.sub ('%d' % d, dig, id)
439
440     id = re.sub  ('[^a-zA-Z]', 'X', id)
441     return id
442
443
444 def musicxml_pitch_to_lily (mxl_pitch):
445     p = musicexp.Pitch()
446     p.alteration = mxl_pitch.get_alteration ()
447     p.step = (ord (mxl_pitch.get_step ()) - ord ('A') + 7 - 2) % 7
448     p.octave = mxl_pitch.get_octave () - 4
449     return p
450
451 def voices_in_part (part):
452     """Return a Name -> Voice dictionary for PART"""
453     part.interpret ()
454     part.extract_voices ()
455     voice_dict = part.get_voices ()
456
457     return voice_dict
458
459 def voices_in_part_in_parts (parts):
460     """return a Part -> Name -> Voice dictionary"""
461     return dict([(p, voices_in_part (p)) for p in parts])
462
463
464 def get_all_voices (parts):
465     all_voices = voices_in_part_in_parts (parts)
466
467     all_ly_voices = {}
468     for p, name_voice in all_voices.items ():
469
470         part_ly_voices = {}
471         for n, v in name_voice.items ():
472             progress ("Converting to LilyPond expressions...")
473             part_ly_voices[n] = (musicxml_voice_to_lily_voice (v), v)
474
475         all_ly_voices[p] = part_ly_voices
476         
477     return all_ly_voices
478
479
480 def option_parser ():
481     p = ly.get_option_parser(usage=_ ("musicxml2ly FILE.xml"),
482                              version=('''%prog (LilyPond) @TOPLEVEL_VERSION@\n\n'''
483                                       +
484 _ ("""This program is free software.  It is covered by the GNU General Public
485 License and you are welcome to change it and/or distribute copies of it
486 under certain conditions.  Invoke as `%s --warranty' for more
487 information.""") % 'lilypond'
488 + """
489 Copyright (c) 2005--2006 by
490     Han-Wen Nienhuys <hanwen@xs4all.nl> and
491     Jan Nieuwenhuizen <janneke@gnu.org>
492 """),
493                              description=_ ("Convert %s to LilyPond input.") % 'MusicXML' + "\n")
494     p.add_option ('-v', '--verbose',
495                   action="store_true",
496                   dest='verbose',
497                   help=_ ("be verbose"))
498
499     p.add_option ('', '--lxml',
500                   action="store_true",
501                   default=False,
502                   dest="use_lxml",
503                   help=_ ("Use lxml.etree; uses less memory and cpu time."))
504     
505     p.add_option ('-o', '--output',
506                   metavar=_ ("FILE"),
507                   action="store",
508                   default=None,
509                   type='string',
510                   dest='output_name',
511                   help=_ ("set output filename to FILE"))
512     p.add_option_group ('bugs',
513                         description=(_ ("Report bugs via")
514                                      + ''' http://post.gmane.org/post.php'''
515                                      '''?group=gmane.comp.gnu.lilypond.bugs\n'''))
516     return p
517
518 def music_xml_voice_name_to_lily_name (part, name):
519     str = "Part%sVoice%s" % (part.id, name)
520     return musicxml_id_to_lily (str) 
521
522 def print_voice_definitions (printer, voices):
523     for (part, nv_dict) in voices.items():
524         
525         for (name, (voice, mxlvoice)) in nv_dict.items ():
526             k = music_xml_voice_name_to_lily_name (part, name)
527             printer.dump ('%s = ' % k)
528             voice.print_ly (printer)
529             printer.newline()
530
531             
532 def uniq_list (l):
533     return dict ([(elt,1) for elt in l]).keys ()
534     
535 def print_score_setup (printer, part_list, voices):
536     part_dict = dict ([(p.id, p) for p in voices.keys ()]) 
537
538     printer ('<<')
539     printer.newline ()
540     for part_definition in part_list:
541         part_name = part_definition.id
542         try:
543             part = part_dict[part_name]
544         except KeyError:
545             print 'unknown part in part-list:', part_name
546             continue
547
548         nv_dict = voices[part]
549         staves = reduce (lambda x,y: x+ y,
550                 [mxlvoice._staves.keys ()
551                  for (v, mxlvoice) in nv_dict.values ()],
552                 [])
553
554         if len (staves) > 1:
555             staves = uniq_list (staves)
556             staves.sort ()
557             printer ('\\context PianoStaff << ')
558             printer.newline ()
559             
560             for s in staves:
561                 staff_voices = [music_xml_voice_name_to_lily_name (part, voice_name)
562                         for (voice_name, (v, mxlvoice)) in nv_dict.items ()
563                         if mxlvoice._start_staff == s]
564                 
565                 printer ('\\context Staff = "%s" << ' % s)
566                 printer.newline ()
567                 for v in staff_voices:
568                     printer ('\\context Voice = "%s"  \\%s' % (v,v))
569                     printer.newline ()
570                 printer ('>>')
571                 printer.newline ()
572                 
573             printer ('>>')
574             printer.newline ()
575             
576         else:
577             printer ('\\new Staff <<')
578             printer.newline ()
579             for (n,v) in nv_dict.items ():
580
581                 n = music_xml_voice_name_to_lily_name (part, n) 
582                 printer ('\\context Voice = "%s"  \\%s' % (n,n))
583             printer ('>>')
584             printer.newline ()
585             
586     printer ('>>')
587     printer.newline ()
588
589 def print_ly_preamble (printer, filename):
590     printer.dump_version ()
591     printer.print_verbatim ('%% converted from %s\n' % filename)
592
593 def read_musicxml (filename, use_lxml):
594     if use_lxml:
595         import lxml.etree
596         
597         tree = lxml.etree.parse (filename)
598         mxl_tree = musicxml.lxml_demarshal_node (tree.getroot ())
599         return mxl_tree
600     else:
601         from xml.dom import minidom, Node
602         
603         doc = minidom.parse(filename)
604         node = doc.documentElement
605         return musicxml.minidom_demarshal_node (node)
606
607     return None
608
609
610 def convert (filename, options):
611     progress ("Reading MusicXML from %s ..." % filename)
612     
613     tree = read_musicxml (filename, options.use_lxml)
614
615     part_list = []
616     id_instrument_map = {}
617     if tree.get_maybe_exist_typed_child (musicxml.Part_list):
618         mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list)
619         part_list = mxl_pl.get_named_children ("score-part")
620         
621     parts = tree.get_typed_children (musicxml.Part)
622     voices = get_all_voices (parts)
623
624     if not options.output_name:
625         options.output_name = os.path.basename (filename) 
626         options.output_name = os.path.splitext (options.output_name)[0]
627
628
629     defs_ly_name = options.output_name + '-defs.ly'
630     driver_ly_name = options.output_name + '.ly'
631
632     printer = musicexp.Output_printer()
633     progress ("Output to `%s'" % defs_ly_name)
634     printer.set_file (open (defs_ly_name, 'w'))
635
636     print_ly_preamble (printer, filename)
637     print_voice_definitions (printer, voices)
638     
639     printer.close ()
640     
641     
642     progress ("Output to `%s'" % driver_ly_name)
643     printer = musicexp.Output_printer()
644     printer.set_file (open (driver_ly_name, 'w'))
645     print_ly_preamble (printer, filename)
646     printer.dump (r'\include "%s"' % defs_ly_name)
647     print_score_setup (printer, part_list, voices)
648     printer.newline ()
649
650     return voices
651
652
653 def main ():
654     opt_parser = option_parser()
655
656     (options, args) = opt_parser.parse_args ()
657     if not args:
658         opt_parser.print_usage()
659         sys.exit (2)
660
661     voices = convert (args[0], options)
662
663 if __name__ == '__main__':
664     main()