Skip to content

feat: round-trip the midi-device port and id attributes#348

Merged
webern merged 2 commits into
mainfrom
claude/midi-device-attributes
Jul 13, 2026
Merged

feat: round-trip the midi-device port and id attributes#348
webern merged 2 commits into
mainfrom
claude/midi-device-attributes

Conversation

@webern

@webern webern commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Human Summary

TODO: human writes here

Summary

The midi-device port and id attributes were dropped on read, and the common attribute-only form
(<midi-device id="P1-I1" port="1"></midi-device>, emitted by MuseScore and Finale) vanished on
write because the emit condition required non-empty device text. This exposes the port, keeps the
id round-tripping, and emits the element whenever device text or a port is present.

Issue #347 suggested storing the id as a deviceId field. Instead the id is modeled by
containment. The midi-device id is a back-reference to the score-instrument the device belongs to,
and mx::api already models that instrument as the part's single InstrumentData — so a stored id
would just duplicate the instrument's uniqueId and force callers to keep the two in sync.

  • The writer derives the midi-device id from the part instrument's uniqueId, the same id the
    sibling midi-instrument already writes. There is no separate id for a caller to manage.
  • MidiData::writeDeviceId (unspecified/yes/no, default unspecified) records only whether the
    source spelled the link out, so an id-less midi-device stays id-less and an id-bearing one is
    preserved. Same fidelity-knob convention as ClefData::writeStaffNumber; authors can ignore it.
  • MidiData::devicePort carries the 1–16 port.

While here, InstrumentData is documented to explain a real limitation: mx::api models one
instrument per part and does not support instrument changes mid-piece. MusicXML allows several
instruments per part (notably drum kits, where each sound is its own instrument); only the first is
brought in. That gap is left for a future change, since there is no clean way yet to expose it
without complicating the common single-instrument case.

Testing

  • New midiDeviceRoundTrip.portSurvivesWriteAndRead and writeDeviceIdSurvivesWriteAndRead
  • Full api/impl suite passes (4983 assertions in 427 test cases)
  • Round-trip baseline 224 -> 227 (musescore-slur-start-stop, testChord, testPiano); the id-less
    midi-device in ksuite/k008a_Beaming.xml continues to pass

References

@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.4% 7242 / 8577
Functions 74.9% 2512 / 3353
Branches 52.4% 6320 / 12072

Core HTML report | API HTML report

Commit 7c27b1634eabc62ef7f8035335203433841c3884.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 7c27b1634eabc62ef7f8035335203433841c3884.

@webern webern force-pushed the claude/grace-chord-boundary branch from 2989c24 to de0d806 Compare July 12, 2026 20:31
@webern webern force-pushed the claude/midi-device-attributes branch from 43ba4e4 to 037a1e6 Compare July 12, 2026 20:31
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.6% 7256 / 8579
Functions 74.9% 2512 / 3353
Branches 52.5% 6334 / 12076

Core HTML report | API HTML report

Commit 24d4f74ba1ed108b8c786defb96e04d1baa4e3d7.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 24d4f74ba1ed108b8c786defb96e04d1baa4e3d7.

@webern webern force-pushed the claude/grace-chord-boundary branch from de0d806 to 541a835 Compare July 12, 2026 22:23
@webern webern force-pushed the claude/midi-device-attributes branch from 037a1e6 to b37d969 Compare July 12, 2026 22:23
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.6% 7256 / 8579
Functions 74.9% 2512 / 3353
Branches 52.5% 6334 / 12076

Core HTML report | API HTML report

Commit 2ccc0e33209fb693384d41b567d21a30ae225158.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 2ccc0e33209fb693384d41b567d21a30ae225158.

Base automatically changed from claude/grace-chord-boundary to main July 12, 2026 22:38
Comment thread src/include/mx/api/PartData.h Outdated
Comment on lines +33 to +39
// The midi-device port attribute: a number from 1 to 16 for the unofficial MIDI 1.0
// port (cable) meta event.
std::optional<int> devicePort;

// The midi-device element's id attribute, which refers to the score-instrument the
// device assignment belongs to.
std::optional<std::string> deviceId;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Hmm first of all there's something about saying "The midi-device attribute" and in the next one saying "The midi-device element's attribute" that throws me off. It makes me ask, wait, are these on the same element? And if so why are these worded differently.

Secondly, I don't like things that point somewhere else, like deviceId is doing here, when it could simply be a dangling pointer and forces bookkeeping on the client to make sure they mark the same deviceId here as they do somewhere else. Is there another way to model it?

@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.6% 7256 / 8579
Functions 74.9% 2512 / 3353
Branches 52.5% 6334 / 12076

Core HTML report | API HTML report

Commit 5a8230c80117e3f83c0c800d44ef9270a66653b7.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 5a8230c80117e3f83c0c800d44ef9270a66653b7.

MidiData modeled only the midi-device element text; the port and id
attributes were dropped, and the common attribute-only form
<midi-device id="P1-I1" port="1"></midi-device> vanished entirely
because the writer required non-empty text. Adds MidiData::devicePort
and MidiData::deviceId and wires them through PartReader/PartWriter.

Pins the 3 corpus files this flips to PASS (227 total).
@webern webern force-pushed the claude/midi-device-attributes branch from b23637b to bff85c8 Compare July 13, 2026 09:21
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.5% 7237 / 8560
Functions 74.9% 2511 / 3352
Branches 52.4% 6325 / 12066

Core HTML report | API HTML report

Commit eaf101d39b6fdf221f52c83cbb816016e9d80752.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit eaf101d39b6fdf221f52c83cbb816016e9d80752.

Model the midi-device id by containment instead of a stored reference.

The midi-device id attribute is a back-reference to the score-instrument the
device belongs to, and mx::api already models that instrument as the part's
single InstrumentData. Storing it as MidiData::deviceId duplicated the
instrument's uniqueId and forced callers to keep the two in sync.

- Remove MidiData::deviceId. The writer derives the midi-device id from the part
  instrument's uniqueId, the same id the sibling midi-instrument already uses.
- Add MidiData::writeDeviceId, a fidelity flag (unspecified/yes/no, default
  unspecified) recording whether the source spelled out the link, so an id-less
  midi-device stays id-less on write and an id-bearing one is preserved. Same
  convention as ClefData::writeStaffNumber.
- Keep MidiData::devicePort; emit the midi-device when device text or port is
  present, so an attribute-only (empty-text) midi-device is no longer dropped.
- Document on InstrumentData that mx::api models one instrument per part and does
  not support instrument changes mid-piece.

Round-trip baseline 224 -> 227 (musescore-slur-start-stop, testChord, testPiano);
the id-less midi-device in ksuite/k008a_Beaming.xml continues to pass.
@webern webern merged commit aae406a into main Jul 13, 2026
10 checks passed
@webern webern deleted the claude/midi-device-attributes branch July 13, 2026 10:08
@github-actions

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36648
Functions 74.2% 6352 / 8556
Branches 50.7% 22680 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 84.5% 7237 / 8560
Functions 74.9% 2511 / 3352
Branches 52.4% 6327 / 12068

Core HTML report | API HTML report

Commit d1573c2961e628396f8a8f5c4cabd1c8b7f8d1bc.

@github-actions

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit d1573c2961e628396f8a8f5c4cabd1c8b7f8d1bc.

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.

api: midi-device port and id attributes are dropped; attribute-only midi-device vanishes

1 participant