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