Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MusicXML voice numbers should be unique within a multi-staff part (reuse of voice numbers confuses Dorico) #1335

Closed
gregchapman-dev opened this issue Jul 2, 2022 · 6 comments · Fixed by #1336

Comments

@gregchapman-dev
Copy link
Contributor

music21 version

7.3

Problem summary

Dorico believes the notes are in the same voice, so they end up rendered as a cross-staff two-note chord. Musescore doesn't mind.

Here's an example MusicXML file (produced by music21) that re-uses voice numbers within a multi-staff part.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE score-partwise  PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
  <movement-title></movement-title>
  <identification>
    <creator type="composer"></creator>
    <encoding>
      <encoding-date>2022-06-18</encoding-date>
    </encoding>
  </identification>
  <defaults>
    <scaling>
      <millimeters>7</millimeters>
      <tenths>40</tenths>
    </scaling>
  </defaults>
  <part-list>
    <score-part id="P1">
      <part-name />
    </score-part>
  </part-list>
  <!--=========================== Part 1 ===========================-->
  <part id="P1">
    <!--========================= Measure 1 ==========================-->
    <measure number="1">
      <attributes>
        <divisions>1</divisions>
        <staves>2</staves>
        <clef number="1">
          <sign>G</sign>
          <line>2</line>
        </clef>
        <clef number="2">
          <sign>F</sign>
          <line>4</line>
        </clef>
      </attributes>
      <note>
        <pitch>
          <step>C</step>
          <octave>5</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>1</staff>
      </note>
      <note>
        <pitch>
          <step>D</step>
          <octave>5</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>1</staff>
      </note>
      <note>
        <pitch>
          <step>E</step>
          <octave>5</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>1</staff>
      </note>
      <note>
        <pitch>
          <step>F</step>
          <octave>5</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>1</staff>
      </note>
      <backup>
        <duration>4</duration>
      </backup>
      <note>
        <pitch>
          <step>C</step>
          <octave>3</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>2</staff>
      </note>
      <note>
        <pitch>
          <step>D</step>
          <octave>3</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>2</staff>
      </note>
      <note>
        <pitch>
          <step>E</step>
          <octave>3</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>2</staff>
      </note>
      <note>
        <pitch>
          <step>F</step>
          <octave>3</octave>
        </pitch>
        <duration>1</duration>
        <voice>1</voice>
        <type>quarter</type>
        <staff>2</staff>
      </note>
      <barline location="right">
        <bar-style>regular</bar-style>
      </barline>
    </measure>
  </part>
</score-partwise>

And here's the Dorico rendering of that file:
DoricoConfusion

I'm working on a simple test case that can trigger generation of that MusicXML (the test case I have involves my Humdrum parser).

@gregchapman-dev
Copy link
Contributor Author

I don't blame Dorico, they're trying to pay correct attention to which notes go in which voice, and I'm happy about that.

@gregchapman-dev
Copy link
Contributor Author

gregchapman-dev commented Jul 2, 2022

I would be happy to work on this, once I get an opinion about whether this is a fix you would want. My proposal would be to do something simple in the MusicXML writer, like pre-allocate 4 voice numbers to each staff (start staff 2 with voice 5), and let the chips fall where they may.

@gregchapman-dev
Copy link
Contributor Author

Looks like the voice numbering happens in moveMeasureContents (in partStaffExporter.py). It looks suspiciously like it's trying to make voice numbers unique, but perhaps it doesn't know enough, and I can plumb more info down to it (and update it's uniqueness algorithm a bit)? Definitely looking for advice.

@gregchapman-dev
Copy link
Contributor Author

Ah. Looks like the following code is the culprit:

            if elem.tag == 'note':
                voice = elem.find('voice')
                if voice is not None:
                    if otherMeasureLackedVoice and voice.text:
                        # otherMeasure assigned voice 1; Bump voice number here
                        voice.text = str(int(voice.text) + 1)
                    else:
                        pass  # No need to alter existing voice numbers <-- HERE BE DRAGONS
                else:
                    voice = Element('voice')
                    voice.text = str(maxVoices + 1)

@gregchapman-dev
Copy link
Contributor Author

Simple (well, sort of) test that I have added to partStaffExporter.py, which fails without my fix, and succeeds with my fix:

    def testJoinPartStaffsD2(self):
        '''
        Add measures and voices and check for unique voice numbers across the StaffGroup.
        '''
        from music21 import layout
        from music21 import note
        s = stream.Score()
        ps1 = stream.PartStaff()
        m1 = stream.Measure()
        ps1.insert(0, m1)
        v1 = stream.Voice()
        v2 = stream.Voice()
        m1.insert(0, v1)
        m1.insert(0, v2)
        v1.repeatAppend(note.Note('C4'), 4)
        v2.repeatAppend(note.Note('E4'), 4)
        ps1.makeNotation(inPlace=True)  # makeNotation to freeze notation

        ps2 = stream.PartStaff()
        m2 = stream.Measure()
        ps2.insert(0, m2)
        v3 = stream.Voice()
        v4 = stream.Voice()
        m2.insert(0, v3)
        m2.insert(0, v4)
        v3.repeatAppend(note.Note('C3'), 4)
        v4.repeatAppend(note.Note('G3'), 4)
        ps2.makeNotation(inPlace=True)  # makeNotation to freeze notation

        s.insert(0, ps2)
        s.insert(0, ps1)
        s.insert(0, layout.StaffGroup([ps1, ps2]))
        root = self.getET(s)
        measures = root.findall('.//measure')
        notes = root.findall('.//note')
        # from music21.musicxml.helpers import dump
        # dump(root)
        self.assertEqual(len(measures), 1)
        self.assertEqual(len(notes), 16)

        # check those voice and staff numbers
        for mxNote in notes:
            mxPitch = mxNote.find('pitch')
            if mxPitch.find('step').text == 'C' and mxPitch.find('octave').text == '4':
                self.assertEqual(mxNote.find('voice').text, '1')
                self.assertEqual(mxNote.find('staff').text, '1')
            elif mxPitch.find('step').text == 'E' and mxPitch.find('octave').text == '4':
                self.assertEqual(mxNote.find('voice').text, '2')
                self.assertEqual(mxNote.find('staff').text, '1')
            elif mxPitch.find('step').text == 'C' and mxPitch.find('octave').text == '3':
                self.assertEqual(mxNote.find('voice').text, '3') # FAIL: it is '1' instead of '3'
                self.assertEqual(mxNote.find('staff').text, '2')
            elif mxPitch.find('step').text == 'G' and mxPitch.find('octave').text == '3':
                self.assertEqual(mxNote.find('voice').text, '4') # FAIL: it is '2' instead of '4'
                self.assertEqual(mxNote.find('staff').text, '2')

gregchapman-dev added a commit to gregchapman-dev/music21 that referenced this issue Jul 2, 2022
@gregchapman-dev
Copy link
Contributor Author

Fixed in PR #1336

mscuthbert pushed a commit that referenced this issue Aug 23, 2022
* Voice numbers written in MusicXML must be unique within the StaffGroup.  Changes the numbers on the deepcopy during a preprocessing step.  (Greg Chapman)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant