Skip to content

fix: read a voice change without a backup as another voice - #492

Merged
webern merged 5 commits into
mainfrom
m/mxdev-voicemap
Nov 7, 2026
Merged

webern merged 5 commits into
mainfrom
m/mxdev-voicemap

Conversation

@webern

@webern webern commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

AI Summary

musuite/testVoiceMapper1.xml writes three notes labelled voice 4, 5 and 4 one after another, with no backup between them. mx's reader only started a new voice at a backup, so all three notes went into one voice with one name, and the middle note came back as voice 4.

Reader: between two backups, each staff's notes are now split into voices by the text of their voice element. A chord member stays with the note it sounds with, and a note without a voice element stays with the note before it. A stretch that names at most one voice is one voice, as before, and the same name after a backup is still a separate voice.

Writer: when a voice only plays in the rests of the voice before it on the same staff, it is written in those rests with its own voice text, instead of after a forward and a backup. This happens only when the notes of the voices form one line in time with no two voices starting on the same tick, each voice first appears in voice order, every voice on the staff has its own voice text, and every note has a position. Otherwise voices are written one after another as before, and a voice that starts after the one before it ends is written as before. A staff with one voice is written exactly as before.

The writer rule also applies to scores built by hand through mx::api, so their output can change. The new output is an equivalent encoding that reads back to the same voices. The spanner number resolver now walks notes in the order the writer writes them, so slurs and tuplets given an identity get different numbers when they overlap in the shared stream.

Four other corpus files are grouped differently by the reader: Berlioz Le Corsaire, Black Note Study Op 10 no 5, Moments Musicaux Op16 No4 and Nocturne No. 20 in C Minor. Their written output differs from main only in voice text, and every change brings back the source's text (326 notes in Berlioz, one in each of the others). None of them has a voice written in another voice's rests.

Testing

  • VoiceInterleaveApiTest: 15 test cases, 2549 assertions. One test writes and reads back 400 random one-staff measures of one to three voices with chords, grace notes and identity slurs, and checks the voices come back unchanged and no slur number is reused while open
  • The slur, repeated-name and random tests fail before the follow-up fix (a write with an identity slur in a merged voice failed outright)
  • make api-test: 9173 assertions in 799 test cases
  • make api-roundtrip: 459 of 459 pinned files pass, with musuite/testVoiceMapper1.xml added to the baseline
  • make api-roundtrip-discover: 460 pass, 381 fail (main: 459 pass); the only new pass is testVoiceMapper1.xml and no file stops passing
  • Unity build of mx (batch size 0) and make wasm-test
  • make fmt-check

Not covered:

  • A cross-staff beam group in a voice whose rests another voice fills. The reader finds cross-staff beam groups per backup stretch, so a group broken by another voice's note is treated like one broken by a forward, as on main.
  • Two voices with the same name on one staff that meet without a backup are still read as one voice. The writer does not merge voices on such a staff.

References

@webern webern added bug software defect testing impl Affects the mx::impl layer ai Issues opened by, or through, a coding agent. labels Nov 5, 2026
Comment thread src/private/mx/impl/MeasureReader.cpp Resolved
Comment thread src/private/mx/impl/VoicePass.h Resolved
MusicXML can change voice between two notes with nothing but the <voice>
text: the next note starts where the last one ended, no <backup> needed.
The reader keyed voices only by <backup> runs, so notes labelled 4, 5, 4
in a row became one voice with one name, and the middle note was written
back as voice 4.

Within a backup run, the reader now groups each staff's notes by their
<voice> text, and each group is a voice of its own. A <chord/> note stays
with the note it sounds with, and a note without a <voice> continues the
voice of the note before it. A run that names at most one voice reads as
before, and the same name after a <backup> is still another voice.

The writer, which writes a staff's voices one after another, now writes a
voice inside the voice before it when it exactly fills that voice's gaps:
it starts before that voice ends, the two form one line in time with no
two voices starting at the same tick, the voices enter in order, and their
<voice> texts differ. Such a voice is written in place, rather than as a
<forward> in the first voice and a <backup> to the second, and reads back
as the same voices. Every other voice is written as before.

Fixes #425.
A voice written in another voice's gap was written from a copy of its
notes. Spanner numbers are looked up by the address of the note's
spanner objects, so an author's identity slur or tuplet on such a note
was not found and the whole write failed. The spanner resolver also
walked the staff voice by voice, not in the order the notes are
written, so two identity spanners open at once in the shared stream
could be given the same number.

The grouping of a staff's voices into passes moves to VoicePass so the
writer and the resolver walk the same order, and the writer writes each
note from the score itself.

A voice could also be written in a gap when it shared its voice name
with a voice of an earlier or later pass that met the merged stream
without a backup, and a reader then put the two voices together. Now a
staff writes voices into one another's gaps only when every voice on it
has a name of its own and every note has a position.
@webern
webern marked this pull request as ready for review November 7, 2026 12:42
@webern
webern merged commit 5f4e680 into main Nov 7, 2026
9 checks passed
@webern
webern deleted the m/mxdev-voicemap branch November 7, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. bug software defect impl Affects the mx::impl layer testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possible problem with backup multi-voice logic found with musuite/testVoiceMapper1.xml.

1 participant