Skip to content

Read NWC Dur2/Pos2 chord notes and rest chords at the same offset - #2139

Open
float3 wants to merge 8 commits into
cuthbertLab:masterfrom
float3:pr/nwctxt-chord-groups-2
Open

float3 wants to merge 8 commits into
cuthbertLab:masterfrom
float3:pr/nwctxt-chord-groups-2

Conversation

@float3

@float3 float3 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

In nwctxt, a |Chord| line's Dur2/Pos2 is a second group of notes sounding with the chord (a split-stem chord), but translateChord read it as a rest placed after the chord, and |RestChord| lines were ignored. Each group, and each repeated Dur:/Pos: pair, now goes in its own voice at the chord's offset. Notes after a chord start where the shortest group ends; clefs, keys, dynamics and text go in at that position.

  • Voices get no filler rests; gaps are left as gaps.
  • The binary reader writes a rest chord as |RestChord|Dur:<rest>|Dur2:<notes>|Pos2:..., where it wrote the rest as a Dur2 group.
  • A rest chord uses up a lyric syllable, which goes on its chord. Not checked against NWC; the repo has no real |RestChord| sample.
  • Ties match on step and octave, so they hold through clef changes and over barlines. A tie stops only on a note that starts where the tied note ends.
  • A slurred note is slurred to the next note that starts once it has ended, in any voice or measure.
  • Open ties and slurs are cleared at a new staff.

Before (a chord from NWCTEXT_Really_complete_example_file.nwctxt; the E5 of Pos2:3x is lost and the next note comes late):

>>> nwt = noteworthy.translate.NoteworthyTranslator()
>>> s = nwt.parseList(['|AddStaff|', '|Clef|Type:Treble', '|Note|Dur:4th|Pos:0',
...     '|Chord|Dur:8th|Pos:-4,n-3,b-2,#-1,x0,v1,2x|Dur2:8th,DblDotted|Pos2:3x',
...     '|Note|Dur:4th|Pos:-5'])
>>> s.flatten().show('text')
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.note.Note B>
{1.0} <music21.chord.Chord E4 F4 G-4 A#4 B##4 C--5 D5>
{1.5} <music21.note.Rest 0.875ql>
{2.375} <music21.note.Note D>

In a |Chord| line, Dur2/Pos2 is a second group of notes sounding with
the chord (a split-stem chord), but translateChord read it as a rest
placed after the chord, so the notes were lost and everything after
them came late. |RestChord| (a rest with a chord sounding at the same
time) was not read at all.

Each group now goes in its own voice at the chord's offset; voices are
inserted at the start of the measure rather than appended after its
notes.
@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 93.767% (+0.04%) from 93.729% — float3:pr/nwctxt-chord-groups-2 into cuthbertLab:master

@mscuthbert

Copy link
Copy Markdown
Member

The diffs were long and this is code I don't really remember so I had my agent look at it. Some comments:


Bugs

  1. Later notes land early in text files. getShortestStream goes back to a group the music has already passed. The PR's tests stop one note short of this.

    • The PR's example plus one more note (|Note|Dur:4th|Pos:-4): E4 lands at 1.875, overlapping D4 (1.5–2.5). By the PR's own model, it belongs at 2.5.
    • |RestChord|Dur:Half|Dur2:4th|Pos2:-1,1, |Note|Dur:Half|Pos:0, |Note|Dur:4th|Pos:1: the last note lands at 2.0 instead of 3.0.
    • Suggested fix: track a running position instead of inferring it from the shortest voice.
  2. Rest chords from binary files become notes. The description treats reading Dur2 as a rest as a mistake, but that is what the binary reader writes (restChordMember, binaryTranslate.py:1309, added with the old rest reading in Work on noteworthy 1 75 binary reader #1247).

    • The reader emits |Chord|Dur:4th|Pos:1|Dur2:Half|Pos2:0, where Dur2 is the rest.
    • Master gives a late rest. This PR gives an extra half note that is not in the score (B4 in treble, D3 in bass).
    • Only NWC 1.75 files have chord members here. This was tested with the real dump method and a hand-built chord member, because none of the four sample .nwc files has a rest chord.
    • Suggested fix: have the reader write |RestChord|. That should also cure the KeyError that master and this PR both raise when the rest and chord lengths match.
  3. Ties and slurs leak between groups, because one flag is shared across simultaneous groups.

    • |Chord|Dur:4th|Pos:1^|Dur2:4th|Pos2:3, then the same chord untied: E5 gets a stray tie stop and the second C5 loses its stop. Master gets that C5 tie right.
    • |Chord|Dur:4th|Pos:1^|Dur2:4th|Pos2:3^, then the same chord untied: E5 gets no tie at all.
    • |RestChord|Dur:Half|Dur2:4th,Slur|Pos2:-1,1: the chord is slurred to the rest that starts with it.
    • |Chord|Dur:4th,Slur|Pos:1|Dur2:4th|Pos2:3: the slur joins the two simultaneous groups.
    • It is not known whether real files mark a slur on only one group.

The suggested fixes have not been built or tested.

Not mentioned in the description

  • Filler rests: these and stepShift=3 are gone, so new voices can start with a gap. MusicXML export pads the gap, so this looks harmless.
  • Lyrics: only the first group gets one, and a rest chord now uses up a syllable. This has not been checked against NWC.
  • Repeated Dur: lines: these chords now line up correctly, which is a real improvement.
  • |RestChord| format: the repo has no real sample, so the tests rest on hand-written lines.

Also can you bump version and put "New in v11" for the new public functions. THANKS!

Notes after a chord group start where the shortest group ends, not at the
end of the shortest voice. Ties follow their staff position and slurs
their voice. Binary rest chords are dumped as |RestChord|.
@mscuthbert

Copy link
Copy Markdown
Member

Thanks looking really good! but a few more things...

My agent checked the new commits. Items 1–3 from my last comment are fixed, and the version bump and "New in v11" markers are in. These still need fixing in this PR (or a rebuttal saying my agent messed up)?

Bugs

  1. Slurs get lost or end on the wrong note. Slurs are now tracked per voice, but the next note can land in another voice or in a measure with no voices. Master and the previous commit get the first two cases right.

    • |Chord|Dur:Half|Pos:1|Dur2:4th|Pos2:3, |Note|Dur:4th,Slur|Pos:2, |Note|Dur:4th|Pos:4: there is no slur; it should run D5 to F5.
    • The same chord and slurred note, then |Bar|, |Note|Dur:4th|Pos:4: there is no slur here either.
    • |Chord|Dur:4th|Pos:1|Dur2:4th,Slur|Pos2:3, |Bar|, |Note|Dur:Whole|Pos:3, |Bar|, |Note|Dur:Whole|Pos:3, |Bar|, |Chord|Dur:4th|Pos:1|Dur2:4th|Pos2:3: the slur runs to the chord three bars later, instead of the next note.
    • Suggested fix, not built or tested: an unslurred note closes any open slur whose start note has already ended, whatever its voice. The voice would only separate groups that sound together.
  2. Clefs, dynamics, keys and text land at the end of the longest group. They should go at the new running position.

    • |Chord|Dur:4th|Pos:-1|Dur2:Whole|Pos2:3, |Dynamic|Style:p|Pos:-8, |Note|Dur:4th|Pos:1: the note is at 1.0, but the p is at 4.0.
    • |Chord|Dur:4th|Pos:-1|Dur2:Half|Pos2:3, |Clef|Type:Bass, |Note|Dur:4th|Pos:1: the note is at 1.0, but the clef is at 2.0.
  3. A tie across a clef change loses its stop. Ties now match on staff position.

    • |Note|Dur:4th|Pos:-6^, |Clef|Type:Bass, |Note|Dur:4th|Pos:6 is C4 to C4, but the second note gets no stop. Master gets this right.
    • Matching by pitch might fix it, but check a sharp tied over a barline first, because accidentals reset at the bar.

Smaller things

  • getShortestStream was removed without deprecation. -- fine -- no one was deep in this code.
  • The setDurationForObject docstring still shows Whole,Dotted,Slur, but it no longer handles slurs.
  • The # Pos2? Dur2? to-do at the top of translate.py is out of date.
  • def __init__(self) -> None: would clear the four new mypy notes. (Not sure what this means)

PR description

Optionally update it to cover the binary reader change, filler rests, lyrics (a rest chord now uses up a syllable) and repeated Dur: lines. Lyrics on rest chords still haven't been checked against NWC, and the repo still has no real |RestChord| sample.

Optional

These two are already wrong on master, so they don't need to hold up this PR, but if you're feeling generous:

  • A tie with no partner stays open: Pos:1^, Pos:2, then a later Pos:1 gets a false stop. Master's false stop lands on the very next note instead.
  • Open ties and slurs carry into the next staff. Clearing tiedPositions and slurStarts in createStaff should fix that.

THANKS!

@mscuthbert mscuthbert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requesting some more chaanges -- see issue. thanks!

Slurs: a slurred note is slurred to the next note that starts once it
has ended, in any voice or measure. Slurred notes in a row still make
one slur. Before, slurs were kept per voice, so
|Chord|Dur:Half|Pos:1|Dur2:4th|Pos2:3, |Note|Dur:4th,Slur|Pos:2,
|Note|Dur:4th|Pos:4 had no slur (also with a |Bar| before the last
note), and a slur from a Dur2 group ran to the next chord three bars
later instead of the next note.

Clefs, keys, time signatures, dynamics, hairpins, repeat marks and text
go in at the running measure position, not at the end of the longest
group. |Chord|Dur:4th|Pos:-1|Dur2:Whole|Pos2:3, |Dynamic|Style:p,
|Note|Dur:4th|Pos:1 put the p at 4.0; it is now at 1.0 with the note.

Ties match on step and octave, so they survive a clef change
(|Note|Dur:4th|Pos:-6^, |Clef|Type:Bass, |Note|Dur:4th|Pos:6 is C4 to
C4) and a sharp tied over a barline still gets its stop. A tie only
stops on a note that starts where the tied note ends, so
Pos:1^, Pos:2, Pos:1 no longer puts a stop on the third note.

Open ties and slurs are cleared at a new staff.
setDurationForObject no longer reads slurs, so its example Dur info drops
Slur. Dur2/Pos2 are read now, so the to-do entry for them goes. Annotating
__init__ makes mypy check it, so currentMeasure starts as an empty Measure
and the list and dict attributes get types. Test notes move into
docstrings.
An open tie that ends at a bar line carries over to beat 1 of the next
measure; at each bar line every other open tie is dropped. Before,
Pos:1^ (whole), |Bar|, Pos:2, |Bar|, Pos:1 put a tie stop on the last
note.
@float3

float3 commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

pushed fixes for all of these:

  • slurs run to the next note that starts after the slurred note ends, in any voice or measure
  • clefs, keys, dynamics and text go in at the running position
  • ties match on step and octave, so they survive a clef change and a sharp tied over a barline keeps its stop
  • setDurationForObject docstring and the Pos2? Dur2? todo updated, __init__ -> None added
  • also did the optional ones: a tie with no partner doesn't get a false stop later, and open ties/slurs are cleared at a new staff

updated the PR description too

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 this pull request may close these issues.

3 participants