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