]> git.donarmstrong.com Git - lilypond.git/commitdiff
MusicXML: Implement part-group (i.e. StaffGroup in ly) and part-name (instrumentName)
authorReinhold Kainhofer <reinhold@kainhofer.com>
Tue, 18 Sep 2007 20:51:59 +0000 (22:51 +0200)
committerReinhold Kainhofer <reinhold@kainhofer.com>
Wed, 3 Oct 2007 16:39:42 +0000 (18:39 +0200)
Implementing part-groups and their bracketing and span-bars turned out
harder than I imagined... I build one nested set of StaffGroup and Staff
objects from the part-list, including the instrument names and the bracket
types. However, to be able to distinguish single-staff staves and piano
staves and to assign the correct voices and lyrics to a part, I need
another loop after everything has been processed. In this additional
loop through the whole score structure I extract the voice and lyrics
ids assigned to each part and assign them the StaffGroup nested
objects.

Finally, printing everything out is quite simple.

Signed-off-by: Reinhold Kainhofer <reinhold@kainhofer.com>
python/musicexp.py
python/musicxml.py
scripts/musicxml2ly.py

index 5bd1436748dcfb137c3b9ace71e8b86257b9243e..40d98aca0299d477f887fb668451d6ac9409663c 100644 (file)
@@ -6,6 +6,9 @@ import re
 from rational import Rational
 
 
+def escape_output_string (input_string):
+    return "\"" + string.replace (input_string, "\"", "\\\"") + "\""
+
 class Output_stack_element:
     def __init__ (self):
         self.factor = Rational (1)
@@ -523,7 +526,7 @@ class Event(Music):
     pass
 
 class SpanEvent (Event):
-    def __init__(self):
+    def __init__ (self):
         Event.__init__ (self)
         self.span_direction = 0 # start/stop
         self.line_type = 'solid'
@@ -871,6 +874,107 @@ class MultiMeasureRest(Music):
         return 'R%s' % self.duration.ly_expression ()
 
 
+class StaffGroup:
+    def __init__ (self, command = "StaffGroup"):
+        self.stafftype = command
+        self.id = None
+        self.instrument_name = None
+        self.short_instrument_name = None
+        self.symbol = None
+        self.spanbar = None
+        self.children = []
+        # part_information is a list with entries of the form
+        #     [staffid, voicelist]
+        # where voicelist is a list with entries of the form
+        #     [voiceid1, [lyricsid11, lyricsid12,...] ]
+        self.part_information = None
+
+    def appendStaff (self, staff):
+        self.children.append (staff)
+
+    def setPartInformation (self, part_name, staves_info):
+        if part_name == self.id:
+            self.part_information = staves_info
+        else:
+            for c in self.children:
+                c.setPartInformation (part_name, staves_info)
+
+    def print_ly_contents (self, printer):
+        for c in self.children:
+            if c:
+                c.print_ly (printer)
+    def print_ly_overrides (self, printer):
+        needs_with = False
+        needs_with |= self.spanbar == "no"
+        needs_with |= self.instrument_name != None
+        needs_with |= self.short_instrument_name != None
+        needs_with |= (self.symbol != None) and (self.symbol != "bracket")
+        if needs_with:
+            printer.dump ("\\with {")
+            if self.instrument_name or self.short_instrument_name:
+                printer.dump ("\\consists \"Instrument_name_engraver\"")
+            if self.spanbar == "no":
+                printer.dump ("\\override SpanBar #'transparent = ##t")
+            brack = {"brace": "SystemStartBrace",
+                     "none": "f",
+                     "line": "SystemStartBar"}.get (self.symbol, None)
+            if brack:
+                printer.dump ("systemStartDelimiter = #'%s" % brack)
+            printer.dump ("}")
+
+    def print_ly (self, printer):
+        if self.stafftype:
+            printer.dump ("\\new %s" % self.stafftype)
+        self.print_ly_overrides (printer)
+        printer.dump ("<<")
+        printer.newline ()
+        if self.stafftype and self.instrument_name:
+            printer.dump ("\\set %s.instrumentName = %s" % (self.stafftype, 
+                    escape_output_string (self.instrument_name)))
+            printer.newline ()
+        if self.stafftype and self.short_instrument_name:
+            printer.dump ("\\set %s.shortInstrumentName = %s\n" % (self.stafftype, 
+                    escape_output_string (self.short_instrument_name)))
+            printer.newline ()
+        self.print_ly_contents (printer)
+        printer.newline ()
+        printer.dump (">>")
+        printer.newline ()
+
+
+class Staff (StaffGroup):
+    def __init__ (self):
+        StaffGroup.__init__ (self, "Staff")
+        self.part = None
+
+    def print_ly_overrides (self, printer):
+        pass
+
+    def print_ly_contents (self, printer):
+        if not self.id or not self.part_information:
+            return
+
+        for [staff_id, voices] in self.part_information:
+            if staff_id:
+                printer ('\\context Staff = "%s" << ' % staff_id)
+            else:
+                printer ('\\context Staff << ')
+            printer.newline ()
+            for [v, lyrics] in voices:
+                printer ('\\context Voice = "%s"  \\%s' % (v,v))
+                printer.newline ()
+
+                for l in lyrics:
+                    printer ('\\new Lyrics \\lyricsto "%s" \\%s' % (v,l))
+                    printer.newline()
+            printer ('>>')
+
+    def print_ly (self, printer):
+        if self.part_information and len (self.part_information) > 1:
+            self.stafftype = "PianoStaff"
+        StaffGroup.print_ly (self, printer)
+
+
 def test_pitch ():
     bflat = Pitch()
     bflat.alteration = -1
index 3ae5954e33fcbb08217fb42afe438527e3797709..637e4d62f4dd15c2e789ad850eb5a88f5d27ccab 100644 (file)
@@ -328,6 +328,11 @@ class Part_list (Music_xml_node):
         else:
             sys.stderr.write ("Opps, couldn't find instrument for ID=%s\n" % id)
             return "Grand Piano"
+
+class Part_group (Music_xml_node):
+    pass
+class Score_part (Music_xml_node):
+    pass
         
 class Measure (Music_xml_node):
     def get_notes (self):
@@ -697,10 +702,12 @@ class_dict = {
        'note': Note,
         'octave-shift': Octave_shift,
        'part': Part,
+    'part-group': Part_group,
        'part-list': Part_list,
         'pedal': Pedal,
        'pitch': Pitch,
        'rest': Rest,
+    'score-part': Score_part,
        'slur': Slur,
        'staff': Staff,
         'syllabic': Syllabic,
index a75eff825b6366d59d12d385c268fac65093768e..eca9186525909933b865eec1e43b07404441c18f 100644 (file)
@@ -63,6 +63,80 @@ def extract_score_information (tree):
     return header
 
 
+def extract_score_layout (part_list):
+    layout = musicexp.StaffGroup (None)
+    currentgroups_dict = {}
+    currentgroups = []
+    if not part_list:
+        return layout
+
+    def insert_into_layout (object):
+            if len (currentgroups) > 0:
+                group_to_insert = currentgroups_dict.get (currentgroups [-1], layout)
+            else:
+                group_to_insert = layout
+            group_to_insert.appendStaff (object)
+            return group_to_insert
+
+    def read_score_part (el):
+        if not isinstance (el, musicxml.Score_part):
+            return
+        staff = musicexp.Staff ()
+        staff.id = el.id
+        partname = el.get_maybe_exist_named_child ('part-name')
+        # Finale gives unnamed parts the name "MusicXML Part" automatically!
+        if partname and partname.get_text() != "MusicXML Part":
+            staff.instrument_name = partname.get_text ()
+        if el.get_maybe_exist_named_child ('part-abbreviation'):
+            staff.short_instrument_name = el.get_maybe_exist_named_child ('part-abbreviation').get_text ()
+        # TODO: Read in the MIDI device / instrument
+        return staff
+
+
+    parts_groups = part_list.get_all_children ()
+    # the start/end group tags are not necessarily ordered correctly, so
+    # we can't go through the children sequentially!
+
+    if len (parts_groups) == 1 and isinstance (parts_group[1], musicxml.Score_part):
+        return read_score_part (parts_group[1])
+
+    for el in parts_groups:
+        if isinstance (el, musicxml.Score_part):
+            staff = read_score_part (el)
+            insert_into_layout (staff)
+        elif isinstance (el, musicxml.Part_group):
+            if el.type == "start":
+                group = musicexp.StaffGroup ()
+                staff_group = insert_into_layout (group)
+                # If we're inserting a nested staffgroup, we need to use InnerStaffGroup
+                if staff_group != layout:
+                    group.stafftype = "InnerStaffGroup"
+                if hasattr (el, 'number'):
+                    id = el.number
+                    group.id = id
+                    currentgroups_dict[id] = group
+                    currentgroups.append (id)
+                if el.get_maybe_exist_named_child ('group-name'):
+                    group.instrument_name = el.get_maybe_exist_named_child ('group-name').get_text ()
+                if el.get_maybe_exist_named_child ('group-abbreviation'):
+                    group.short_instrument_name = el.get_maybe_exist_named_child ('group-abbreviation').get_text ()
+                if el.get_maybe_exist_named_child ('group-symbol'):
+                    group.symbol = el.get_maybe_exist_named_child ('group-symbol').get_text ()
+                if el.get_maybe_exist_named_child ('group-barline'):
+                    group.spanbar = el.get_maybe_exist_named_child ('group-barline').get_text ()
+
+            elif el.type == "stop":
+                # end the part-group, i.e. simply remove it from the lists
+                if hasattr (el, 'number'):
+                    pid = el.number
+                elif len (currentgroups) > 0:
+                    pid = el[-1]
+                if pid:
+                    del currentgroups_dict[pid]
+                    currentgroups.remove (pid)
+    return layout
+
+
 
 def musicxml_duration_to_lily (mxl_note):
     d = musicexp.Duration ()
@@ -884,15 +958,15 @@ def print_voice_definitions (printer, part_list, voices):
         part_dict[part.id] = (part, nv_dict)
 
     for part in part_list:
-        (part, nv_dict) = part_dict.get (part.id, (None, {}))
+        (p, nv_dict) = part_dict.get (part.id, (None, {}))
         for (name, ((voice, lyrics), mxlvoice)) in nv_dict.items ():
-            k = music_xml_voice_name_to_lily_name (part, name)
+            k = music_xml_voice_name_to_lily_name (p, name)
             printer.dump ('%s = ' % k)
             voice.print_ly (printer)
             printer.newline()
             
             for l in lyrics.keys ():
-                lname = music_xml_lyrics_name_to_lily_name (part, name, l)
+                lname = music_xml_lyrics_name_to_lily_name (p, name, l)
                 printer.dump ('%s = ' %lname )
                 lyrics[l].print_ly (printer)
                 printer.newline()
@@ -900,12 +974,29 @@ def print_voice_definitions (printer, part_list, voices):
             
 def uniq_list (l):
     return dict ([(elt,1) for elt in l]).keys ()
-    
-def print_score_setup (printer, part_list, voices):
-    part_dict = dict ([(p.id, p) for p in voices.keys ()]) 
 
-    printer ('<<')
-    printer.newline ()
+# format the information about the staff in the form 
+#     [staffid,
+#         [
+#            [voiceid1, [lyricsid11, lyricsid12,...] ...],
+#            [voiceid2, [lyricsid21, lyricsid22,...] ...],
+#            ...
+#         ]
+#     ]
+# raw_voices is of the form [(voicename, lyrics)*]
+def format_staff_info (part, staff_id, raw_voices):
+    voices = []
+    for (v, lyrics) in raw_voices:
+        voice_name = music_xml_voice_name_to_lily_name (part, v)
+        voice_lyrics = [music_xml_lyrics_name_to_lily_name (part, v, l)
+                   for l in lyrics.keys ()]
+        voices.append ([voice_name, voice_lyrics])
+    return [staff_id, voices]
+
+def update_score_setup (score_structure, part_list, voices):
+    part_dict = dict ([(p.id, p) for p in voices.keys ()])
+    final_part_dict = {}
+
     for part_definition in part_list:
         part_name = part_definition.id
         part = part_dict.get (part_name)
@@ -918,56 +1009,21 @@ def print_score_setup (printer, part_list, voices):
                 [mxlvoice._staves.keys ()
                  for (v, mxlvoice) in nv_dict.values ()],
                 [])
-
+        staves_info = []
         if len (staves) > 1:
+            staves_info = []
             staves = uniq_list (staves)
             staves.sort ()
-            printer ('\\context PianoStaff << ')
-            printer.newline ()
-            
             for s in staves:
-                staff_voices = [(music_xml_voice_name_to_lily_name (part, voice_name), voice_name, v)
-                        for (voice_name, (v, mxlvoice)) in nv_dict.items ()
-                        if mxlvoice._start_staff == s]
-                
-                printer ('\\context Staff = "%s" << ' % s)
-                printer.newline ()
-                for (v, voice_name, (music, lyrics)) in staff_voices:
-                    printer ('\\context Voice = "%s"  \\%s' % (v,v))
-                    printer.newline ()
-                    
-                    # Assign the lyrics to that voice
-                    for l in lyrics.keys ():
-                        ll = music_xml_lyrics_name_to_lily_name (part, voice_name, l)
-                        printer ('\\new Lyrics \\lyricsto "%s" \\%s' % (v, ll))
-                        printer.newline()
-                        printer.newline()
-                    
-                printer ('>>')
-                printer.newline ()
-                
-            printer ('>>')
-            printer.newline ()
-            
+                thisstaff_raw_voices = [(voice_name, lyrics) 
+                    for (voice_name, ((music, lyrics), mxlvoice)) in nv_dict.items ()
+                    if mxlvoice._start_staff == s]
+                staves_info.append (format_staff_info (part, s, thisstaff_raw_voices))
         else:
-            printer ('\\new Staff <<')
-            printer.newline ()
-            for (n,v) in nv_dict.items ():
-                ((music, lyrics), voice) = v
-                nn = music_xml_voice_name_to_lily_name (part, n) 
-                printer ('\\context Voice = "%s"  \\%s' % (nn,nn))
-
-                # Assign the lyrics to that voice
-                for l in lyrics.keys ():
-                    ll = music_xml_lyrics_name_to_lily_name (part, n, l)
-                    printer ('\\new Lyrics \\lyricsto "%s" \\%s' % (nn, ll))
-                    printer.newline()
-
-            printer ('>>')
-            printer.newline ()
-            
-    printer ('>>')
-    printer.newline ()
+            thisstaff_raw_voices = [(voice_name, lyrics) 
+                for (voice_name, ((music, lyrics), mxlvoice)) in nv_dict.items ()]
+            staves_info.append (format_staff_info (part, None, thisstaff_raw_voices))
+        score_structure.setPartInformation (part_name, staves_info)
 
 def print_ly_preamble (printer, filename):
     printer.dump_version ()
@@ -995,16 +1051,17 @@ def convert (filename, options):
     
     tree = read_musicxml (filename, options.use_lxml)
 
-    part_list = []
-    id_instrument_map = {}
-    if tree.get_maybe_exist_typed_child (musicxml.Part_list):
-        mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list)
+    score_structure = None
+    mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list)
+    if mxl_pl:
+        score_structure = extract_score_layout (mxl_pl)
         part_list = mxl_pl.get_named_children ("score-part")
-        
+
     # score information is contained in the <work>, <identification> or <movement-title> tags
     score_information = extract_score_information (tree)
     parts = tree.get_typed_children (musicxml.Part)
     voices = get_all_voices (parts)
+    update_score_setup (score_structure, part_list, voices)
 
     if not options.output_name:
         options.output_name = os.path.basename (filename) 
@@ -1032,7 +1089,7 @@ def convert (filename, options):
     printer.set_file (codecs.open (driver_ly_name, 'wb', encoding='utf-8'))
     print_ly_preamble (printer, filename)
     printer.dump (r'\include "%s"' % os.path.basename (defs_ly_name))
-    print_score_setup (printer, part_list, voices)
+    score_structure.print_ly (printer)
     printer.newline ()
 
     return voices