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