Repository navigation
fix: read a voice change without a backup as another voice - #492
Merged
Merged
Conversation
webern
commented
Nov 7, 2026
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
force-pushed
the
m/mxdev-voicemap
branch
from
November 7, 2026 12:10
74f4db7 to
0e1a1a0
Compare
webern
marked this pull request as ready for review
November 7, 2026 12:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Not covered:
References
musuite/testVoiceMapper1.xml. #425