]> git.donarmstrong.com Git - lilypond.git/commitdiff
MusicXML: Correctly convert nested staff/part groups
authorReinhold Kainhofer <reinhold@kainhofer.com>
Sun, 28 Oct 2007 20:44:03 +0000 (21:44 +0100)
committerReinhold Kainhofer <reinhold@kainhofer.com>
Sun, 28 Oct 2007 20:44:03 +0000 (21:44 +0100)
In MusicXML, part group nesting is done simply by inserting
start/stop markers in the list of parts, where the part groups
might even overlap. In Lilypond, we want the real hierarchy (so
that we can assign group names etc.). To achieve this, we need
to loop through all parts, trying to detect the correct start/stop
markers. What makes things even more complicated is the fact
that the group IDs don't have to the unique in the whole score,
just unique enough to describe overlapping part groups. The other
issue are overlapping part groups, which we need to split up into
a part, which fits into the hierarchy and the remaining part, which
we simply ignore.

input/regression/musicxml/08g-OverlappingPartGroups-Finale.xml [new file with mode: 0644]
input/regression/musicxml/14c-StaffChange-Finale.xml
python/musicexp.py
scripts/musicxml2ly.py

diff --git a/input/regression/musicxml/08g-OverlappingPartGroups-Finale.xml b/input/regression/musicxml/08g-OverlappingPartGroups-Finale.xml
new file mode 100644 (file)
index 0000000..fd2ca4b
--- /dev/null
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 1.0 Partwise//EN"\r
+                                "http://www.musicxml.org/dtds/partwise.dtd">\r
+<score-partwise>\r
+  <movement-title>Overlapping part groups</movement-title>\r
+  <identification>\r
+    <creator type="composer">In MusicXML groups can overlap (Grp1: #1-4, Grp2: #3-5)</creator>\r
+    <creator type="arranger">In Lilypond, overlaps are not possible: Grp2 ends at #4</creator>\r
+    <encoding>\r
+      <software>Finale 2007 for Windows</software>\r
+      <software>Dolet Light for Finale 2007</software>\r
+      <encoding-date>2007-10-28</encoding-date>\r
+    </encoding>\r
+  </identification>\r
+  <part-list>\r
+    <part-group number="1" type="start">\r
+      <group-name>Group 1</group-name>\r
+      <group-abbreviation>Gr1</group-abbreviation>\r
+      <group-symbol>bracket</group-symbol>\r
+      <group-barline>yes</group-barline>\r
+    </part-group>\r
+    <score-part id="P1">\r
+      <part-name>MusicXML Part</part-name>\r
+      <score-instrument id="P1-I1">\r
+        <instrument-name>Grand Piano</instrument-name>\r
+      </score-instrument>\r
+      <midi-instrument id="P1-I1">\r
+        <midi-channel>1</midi-channel>\r
+        <midi-program>1</midi-program>\r
+      </midi-instrument>\r
+    </score-part>\r
+    <score-part id="P2">\r
+      <part-name>MusicXML Part</part-name>\r
+      <score-instrument id="P2-I1">\r
+        <instrument-name>Grand Piano</instrument-name>\r
+      </score-instrument>\r
+      <midi-instrument id="P2-I1">\r
+        <midi-channel>1</midi-channel>\r
+        <midi-program>1</midi-program>\r
+      </midi-instrument>\r
+    </score-part>\r
+    <part-group number="2" type="start">\r
+      <group-name>Group 2</group-name>\r
+      <group-abbreviation>Grp2</group-abbreviation>\r
+      <group-symbol>bracket</group-symbol>\r
+      <group-barline>yes</group-barline>\r
+    </part-group>\r
+    <score-part id="P3">\r
+      <part-name>MusicXML Part</part-name>\r
+      <score-instrument id="P3-I1">\r
+        <instrument-name>Grand Piano</instrument-name>\r
+      </score-instrument>\r
+      <midi-instrument id="P3-I1">\r
+        <midi-channel>1</midi-channel>\r
+        <midi-program>1</midi-program>\r
+      </midi-instrument>\r
+    </score-part>\r
+    <score-part id="P4">\r
+      <part-name>MusicXML Part</part-name>\r
+      <score-instrument id="P4-I1">\r
+        <instrument-name>Grand Piano</instrument-name>\r
+      </score-instrument>\r
+      <midi-instrument id="P4-I1">\r
+        <midi-channel>1</midi-channel>\r
+        <midi-program>1</midi-program>\r
+      </midi-instrument>\r
+    </score-part>\r
+    <part-group number="1" type="stop"/>\r
+    <score-part id="P5">\r
+      <part-name>MusicXML Part</part-name>\r
+      <score-instrument id="P5-I1">\r
+        <instrument-name>Grand Piano</instrument-name>\r
+      </score-instrument>\r
+      <midi-instrument id="P5-I1">\r
+        <midi-channel>1</midi-channel>\r
+        <midi-program>1</midi-program>\r
+      </midi-instrument>\r
+    </score-part>\r
+    <part-group number="2" type="stop"/>\r
+  </part-list>\r
+  <!--=========================================================-->\r
+  <part id="P1">\r
+    <measure number="1">\r
+      <attributes>\r
+        <divisions>1</divisions>\r
+        <key>\r
+          <fifths>0</fifths>\r
+          <mode>major</mode>\r
+        </key>\r
+        <time symbol="common">\r
+          <beats>4</beats>\r
+          <beat-type>4</beat-type>\r
+        </time>\r
+        <clef>\r
+          <sign>G</sign>\r
+          <line>2</line>\r
+        </clef>\r
+      </attributes>\r
+      <sound tempo="120"/>\r
+      <note>\r
+        <rest/>\r
+        <duration>4</duration>\r
+        <voice>1</voice>\r
+      </note>\r
+      <barline location="right">\r
+        <bar-style>light-heavy</bar-style>\r
+      </barline>\r
+    </measure>\r
+  </part>\r
+  <!--=========================================================-->\r
+  <part id="P2">\r
+    <measure number="1">\r
+      <attributes>\r
+        <divisions>1</divisions>\r
+        <key>\r
+          <fifths>0</fifths>\r
+          <mode>major</mode>\r
+        </key>\r
+        <time symbol="common">\r
+          <beats>4</beats>\r
+          <beat-type>4</beat-type>\r
+        </time>\r
+        <clef>\r
+          <sign>G</sign>\r
+          <line>2</line>\r
+        </clef>\r
+      </attributes>\r
+      <sound tempo="120"/>\r
+      <note>\r
+        <rest/>\r
+        <duration>4</duration>\r
+        <voice>1</voice>\r
+      </note>\r
+      <barline location="right">\r
+        <bar-style>light-heavy</bar-style>\r
+      </barline>\r
+    </measure>\r
+  </part>\r
+  <!--=========================================================-->\r
+  <part id="P3">\r
+    <measure number="1">\r
+      <attributes>\r
+        <divisions>1</divisions>\r
+        <key>\r
+          <fifths>0</fifths>\r
+          <mode>major</mode>\r
+        </key>\r
+        <time symbol="common">\r
+          <beats>4</beats>\r
+          <beat-type>4</beat-type>\r
+        </time>\r
+        <clef>\r
+          <sign>G</sign>\r
+          <line>2</line>\r
+        </clef>\r
+      </attributes>\r
+      <sound tempo="120"/>\r
+      <note>\r
+        <rest/>\r
+        <duration>4</duration>\r
+        <voice>1</voice>\r
+      </note>\r
+      <barline location="right">\r
+        <bar-style>light-heavy</bar-style>\r
+      </barline>\r
+    </measure>\r
+  </part>\r
+  <!--=========================================================-->\r
+  <part id="P4">\r
+    <measure number="1">\r
+      <attributes>\r
+        <divisions>1</divisions>\r
+        <key>\r
+          <fifths>0</fifths>\r
+          <mode>major</mode>\r
+        </key>\r
+        <time symbol="common">\r
+          <beats>4</beats>\r
+          <beat-type>4</beat-type>\r
+        </time>\r
+        <clef>\r
+          <sign>G</sign>\r
+          <line>2</line>\r
+        </clef>\r
+      </attributes>\r
+      <sound tempo="120"/>\r
+      <note>\r
+        <rest/>\r
+        <duration>4</duration>\r
+        <voice>1</voice>\r
+      </note>\r
+      <barline location="right">\r
+        <bar-style>light-heavy</bar-style>\r
+      </barline>\r
+    </measure>\r
+  </part>\r
+  <!--=========================================================-->\r
+  <part id="P5">\r
+    <measure number="1">\r
+      <attributes>\r
+        <divisions>1</divisions>\r
+        <key>\r
+          <fifths>0</fifths>\r
+          <mode>major</mode>\r
+        </key>\r
+        <time symbol="common">\r
+          <beats>4</beats>\r
+          <beat-type>4</beat-type>\r
+        </time>\r
+        <clef>\r
+          <sign>G</sign>\r
+          <line>2</line>\r
+        </clef>\r
+      </attributes>\r
+      <sound tempo="120"/>\r
+      <note>\r
+        <rest/>\r
+        <duration>4</duration>\r
+        <voice>1</voice>\r
+      </note>\r
+      <barline location="right">\r
+        <bar-style>light-heavy</bar-style>\r
+      </barline>\r
+    </measure>\r
+  </part>\r
+  <!--=========================================================-->\r
+</score-partwise>\r
index d8c4a458895285dc35d31153af50e6bb7f3a54ff..5ac8eff82421b14d52be65dd7f482807ec1f3be3 100644 (file)
@@ -4,6 +4,8 @@
 <score-partwise>\r
   <movement-title>Staff change in piano staff</movement-title>\r
   <identification>\r
+      <creator type="composer">The voice from the second staff has some notes/chords on the first staff</creator>\r
+      <creator type="arranger">The final two chords have some notes on the first, some on the second staff</creator>\r
     <encoding>\r
       <software>Finale 2007 for Windows</software>\r
       <software>Dolet Light for Finale 2007</software>\r
index 3fdb7955e981ff94742d1949dc26c1585ca94ea0..050ae66c03daed3ebeaa162d38ae673e53ffe505 100644 (file)
@@ -1039,21 +1039,22 @@ class StaffGroup:
         self.symbol = None
         self.spanbar = None
         self.children = []
+        self.is_group = True
         # 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):
+    def append_staff (self, staff):
         self.children.append (staff)
 
-    def setPartInformation (self, part_name, staves_info):
+    def set_part_information (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)
+                c.set_part_information (part_name, staves_info)
 
     def print_ly_contents (self, printer):
         for c in self.children:
@@ -1073,7 +1074,7 @@ class StaffGroup:
                 printer.dump ("\\override SpanBar #'transparent = ##t")
             brack = {"brace": "SystemStartBrace",
                      "none": "f",
-                     "line": "SystemStartBar"}.get (self.symbol, None)
+                     "line": "SystemStartSquare"}.get (self.symbol, None)
             if brack:
                 printer.dump ("systemStartDelimiter = #'%s" % brack)
             printer.dump ("}")
@@ -1089,7 +1090,7 @@ class StaffGroup:
                     escape_instrument_string (self.instrument_name)))
             printer.newline ()
         if self.stafftype and self.short_instrument_name:
-            printer.dump ("\\set %s.shortInstrumentName = %s\n" % (self.stafftype, 
+            printer.dump ("\\set %s.shortInstrumentName = %s" % (self.stafftype,
                     escape_instrument_string (self.short_instrument_name)))
             printer.newline ()
         self.print_ly_contents (printer)
@@ -1101,6 +1102,7 @@ class StaffGroup:
 class Staff (StaffGroup):
     def __init__ (self):
         StaffGroup.__init__ (self, "Staff")
+        self.is_group = False
         self.part = None
 
     def print_ly_overrides (self, printer):
index 4a944b1fd294427dd3cbe3f513f3e6def3e97a4f..3adc084450cdab93e5725074f31452157a223457 100644 (file)
@@ -62,22 +62,28 @@ def extract_score_information (tree):
 
     return header
 
+class PartGroupInfo:
+    def __init__ (self):
+        self.start = {}
+        self.end = {}
+    def is_empty (self):
+        return len (self.start) + len (self.end) == 0
+    def add_start (self, g):
+        self.start[getattr (g, 'number', "1")] = g
+    def add_end (self, g):
+        self.end[getattr (g, 'number', "1")] = g
+    def print_ly (self, printer):
+        error_message ("Unprocessed PartGroupInfo %s encountered" % self)
+    def ly_expression (self):
+        error_message ("Unprocessed PartGroupInfo %s encountered" % self)
+        return ''
+
 
 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
@@ -92,48 +98,110 @@ def extract_score_layout (part_list):
         # TODO: Read in the MIDI device / instrument
         return staff
 
+    def read_score_group (el):
+        if not isinstance (el, musicxml.Part_group):
+            return
+        group = musicexp.StaffGroup ()
+        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 ()
+        return group
+
 
     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])
+    # the start/end group tags are not necessarily ordered correctly and groups
+    # might even overlap, so we can't go through the children sequentially!
 
+    # 1) Replace all Score_part objects by their corresponding Staff objects,
+    #    also collect all group start/stop points into one PartGroupInfo object
+    staves = []
+    group_info = PartGroupInfo ()
     for el in parts_groups:
         if isinstance (el, musicxml.Score_part):
-            staff = read_score_part (el)
-            insert_into_layout (staff)
+            if not group_info.is_empty ():
+                staves.append (group_info)
+                group_info = PartGroupInfo ()
+            staves.append (read_score_part (el))
         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 ()
-
+                group_info.add_start (el)
             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)
+                group_info.add_end (el)
+    if not group_info.is_empty ():
+        staves.append (group_info)
+
+    # 2) Now, detect the groups:
+    group_starts = []
+    pos = 0
+    while pos < len (staves):
+        el = staves[pos]
+        if isinstance (el, PartGroupInfo):
+            prev_start = 0
+            if len (group_starts) > 0:
+                prev_start = group_starts[-1]
+            elif len (el.end) > 0: # no group to end here
+                el.end = {}
+            if len (el.end) > 0: # closes an existing group
+                ends = el.end.keys ()
+                prev_started = staves[prev_start].start.keys ()
+                grpid = None
+                intersection = filter(lambda x:x in ends, prev_started)
+                if len (intersection) > 0:
+                    grpid = intersection[0]
+                else:
+                    # Close the last started group
+                    grpid = staves[prev_start].start.keys () [0]
+                    # Find the corresponding closing tag and remove it!
+                    j = pos + 1
+                    foundclosing = False
+                    while j < len (staves) and not foundclosing:
+                        if isinstance (staves[j], PartGroupInfo) and staves[j].end.has_key (grpid):
+                            foundclosing = True
+                            del staves[j].end[grpid]
+                            if staves[j].is_empty ():
+                                del staves[j]
+                        j += 1
+                grpobj = staves[prev_start].start[grpid]
+                group = read_score_group (grpobj)
+                # remove the id from both the start and end
+                if el.end.has_key (grpid):
+                    del el.end[grpid]
+                del staves[prev_start].start[grpid]
+                if el.is_empty ():
+                    del staves[pos]
+                # replace the staves with the whole group
+                for j in staves[(prev_start + 1):pos]:
+                    if j.is_group:
+                        j.stafftype = "InnerStaffGroup"
+                    group.append_staff (j)
+                del staves[(prev_start + 1):pos]
+                staves.insert (prev_start + 1, group)
+                # reset pos so that we continue at the correct position
+                pos = prev_start
+                # remove an empty start group
+                if staves[prev_start].is_empty ():
+                    del staves[prev_start]
+                    group_starts.remove (prev_start)
+                    pos -= 1
+            elif len (el.start) > 0: # starts new part groups
+                group_starts.append (pos)
+        pos += 1
+
+    if len (staves) == 1:
+        return staves[0]
+    for i in staves:
+        layout.append_staff (i)
     return layout
 
 
@@ -1428,7 +1496,7 @@ def update_score_setup (score_structure, part_list, voices):
             thisstaff_raw_voices = [(voice_name, voice.lyrics_order) 
                 for (voice_name, voice) in nv_dict.items ()]
             staves_info.append (format_staff_info (part, None, thisstaff_raw_voices))
-        score_structure.setPartInformation (part_name, staves_info)
+        score_structure.set_part_information (part_name, staves_info)
 
 def print_ly_preamble (printer, filename):
     printer.dump_version ()