Skip to content

feat: expose musicxml id attributes in mx::api - #401

Merged
webern merged 3 commits into
mainfrom
claude/musicxml-id-attributes-api-rfi9qv
Aug 23, 2026
Merged

webern merged 3 commits into
mainfrom
claude/musicxml-id-attributes-api-rfi9qv

Conversation

@webern

@webern webern commented Aug 18, 2026 •

Copy link
Copy Markdown
Owner

Human Summary

Model ID for those cases where the mx::api data model is close enough the MusicXML data model to make that simple.

Caveats:

  • ID strings are silently coerced into valid NCNames.
  • ID uniqueness is not yet enforced.

Summary

Starting with MusicXML 3.1 most elements can carry an id attribute naming that one element within the document. mx::api dropped nearly all of them (only a handful of recently-added direction types had one). This adds an optional id member to every api type that maps one-to-one onto an element with an id, and reads and writes it in mx::impl.

Types that gained an id:

Where MusicXML element
NoteData, LyricData note, lyric
MeasureData, BarlineData measure, barline
ClefData, KeyData, TimeChoice, TransposeData clef, key, time, transpose
DirectionData, SoundData direction, sound
WordsData, SymbolData, RehearsalData words, symbol, rehearsal
WedgeStart, WedgeStop wedge
SpannerStart, SpannerStop bracket, dashes, octave-shift
PedalLineData pedal
CurveStart, CurveContinue, CurveStop, TieLetRing slur, tied
TupletStart, TupletStop tuplet
FiguredBassData figured-bass

Supporting changes:

  • New src/private/mx/impl/IdFunctions.h with the two helpers (getId, setId) the readers and writers call.
  • ApiCommon.h explains what ids are for, and that mx repairs a malformed id but does not check uniqueness (see ID uniqueness is not constrained by mx::core. #397).
  • A carried-forward (implicit) time signature no longer keeps the previous measure's id; only the measure that states the <time> element carries it.
  • SoundData::isSpecified now counts an id, so a <sound> that has nothing but an id is not dropped.

Left out, because placing the id would need a design decision:

  • Grouping elements mx::api does not model as their own type: direction-type, notations, articulations, ornaments, technical, beam.
  • Marks folded into MarkData: dynamics, fermata, accidental-mark. These would need new MarkDataChoice alternatives.
  • harmony and frame: one DirectionData holds several ChordData that serialize into a single <harmony>, so it is not clear which chord owns the id.
  • credit: both <credit> and <credit-words> have an id and PageTextData models the pair.
  • Elements mx::api does not model at all: print, measure-style, grouping, for-part, glissando, slide (see api: model note-attached paired spanners (glissando, slide, wavy-line) #389).

Testing

  • New src/private/mxtest/api/IdAttributeApiTest.cpp: 7 cases, 53 assertions, covering every new field through roundTrip, plus one case reading ids straight off source XML and one pinning the implicit-time-signature rule
  • make test-all green: core roundtrip (839 cases), core unit (44 cases), api/impl suite (6018 assertions in 542 cases), api roundtrip (366 pinned, 0 failed)
  • Discovery went from 366 to 368 passing files; synthetic/bracket.3.1.xml and synthetic/dashes.3.1.xml newly pass because of the bracket and dashes ids, and are pinned in roundtrip-baseline.txt. No file regressed.
  • make fmt-check passes

References

Beginning with MusicXML 3.1 most elements can carry an id attribute that names
that one element within the document. mx::api dropped nearly all of them. Add an
optional id member to every api type that maps one-to-one onto an element with
an id, and read and write it in mx::impl.

New id fields: NoteData, MeasureData, ClefData, KeyData, TimeChoice,
TransposeData, BarlineData, LyricData, DirectionData, WordsData, SymbolData,
RehearsalData, WedgeStart, WedgeStop, SpannerStart, SpannerStop, PedalLineData,
CurveStart, CurveContinue, CurveStop, TieLetRing, TupletStart, TupletStop,
SoundData and FiguredBassData.

A new IdFunctions.h holds the two impl helpers that read and write the
attribute. ApiCommon.h explains what ids are for and the uniqueness rule that mx
does not enforce.

Left out are the elements where placing the id needs a design decision: the
grouping elements mx::api does not model (direction-type, notations,
articulations, ornaments, technical, beam), the marks folded into MarkData
(dynamics, fermata, accidental-mark), harmony and frame (one DirectionData holds
several ChordData that serialize into a single harmony), credit (both credit and
credit-words carry an id and PageTextData models the pair), and the elements
mx::api does not model at all (print, measure-style, grouping, for-part,
glissando, slide).

synthetic/bracket.3.1.xml and synthetic/dashes.3.1.xml now survive the strict
api round trip and are pinned in roundtrip-baseline.txt.
@webern webern added feature new feature request non-breaking fixes or implementation that do not require breaking changes api Affects the mx::api layer ai Issues opened by, or through, a coding agent. labels Aug 18, 2026 — with Claude
webern added 2 commits August 22, 2026 19:37
Merges into #401, not into `main`. This is the "what would it look like"
experiment for replacing the bare `std::optional<std::string> id` with a
type.

## The problem

An id attribute is not any old string. MusicXML requires it to be an XML
name: a letter or an underscore first, then letters, digits, dots,
hyphens, or underscores.

On #401 as it stands, mx enforces that at the last possible moment.
`mx::impl` hands the caller's text to `core::Token` on the way out,
`core::Token` scrubs it, and the file gets an id the caller never asked
for and never hears about:

```cpp
note.id = "3 blind mice";   // the file gets id="blindmice"
```

## The change

Every id member in `mx::api` becomes a `std::optional<Id>`.

`api::Id` holds a `core::Token` behind a pimpl. That is the point of the
design: the text is scrubbed once, when the `Id` is built, and from then
on `mx::impl` hands `mx::core` the token itself. A `core::Token` cannot
hold a malformed id, so there is no second conversion left to make.
`Id.cpp` is now the only place in mx where an api id string becomes a
token.

The public header names no core type. `IdAccess`, in
`src/private/mx/api/IdAccess.h`, is the only door between an `Id` and
its token, and it is not part of the public api.

## What a caller sees

Scrubbing is silent, so `Id` documents the check rather than inventing
an error channel:

```cpp
const auto id = Id{myText};
if (id.value() != myText)
{
    // myText was not a legal id; decide what to do about it
}
```

Uniqueness is unchanged. It is a property of the whole score rather than
of one name, so `Id` cannot check it (#397).

## Value semantics

`Id` copies, assigns, compares, sorts, and hashes like any other value.
`std::hash` is specialized for it, so an `Id` works as a key in both
`std::map` and `std::unordered_map`. Copies share one immutable token
instead of allocating another, and a moved-from `Id` still holds a
token, so reading one is harmless rather than undefined.

## Scope

All 43 id members are converted, so the api has one way to say this and
not three:

- the 25 added by #401;
- the 16 that already shipped as `std::optional<std::string>`
(`DampData`, `EyeglassesData`, `HarpPedalsData`, `ImageData`,
`MarkDataChoice`, `OtherDirectionData`, `PercussionData`,
`PrincipalVoiceData`, `ScordaturaData`, `StaffDivideData`,
`StringMuteData`, `TempoData`, `AccordionRegistrationData`);
- `SegnoData` and `CodaData`, which held a `std::string id` plus an
`isIdSpecified` flag. Both flags are gone, along with their
equality-block lines.

The reader and writer sites that hand-rolled the id attribute now go
through `getId` / `setId` alongside the rest.

`PartData::uniqueId` and `InstrumentData::uniqueId` are left alone. They
are the required part-id join between `<score-part>` and `<part>`, not
the `optional-unique-id` attribute this change is about.

## Two things worth a look

- **`Id{""}` is `"X"`.** There is no nearest legal id for empty text, so
it gets `core::Token`'s natural zero. To write no id attribute, leave
the `std::optional` empty. This is not new behavior — #401 already
writes `id="X"` for an unusable string — but the type makes it visible.
- **The reader no longer reports what the file literally said.** A file
containing `id="3 mice"` reaches the api as `mice`. By doctrine that is
right (an invalid document should be hard to express, and this is
documented normalization), but it is a real change and worth naming.

## Gates

Run natively with `MX_RUNNING_IN_DOCKER=1`; the Docker daemon is not
available in this environment.

| Gate | Result |
|---|---|
| `make fmt-check` | passed |
| `make api-test` | 6050 assertions in 546 test cases |
| `make core-roundtrip-test` | 839 test cases |
| unit tests | 221 assertions in 44 test cases |
| `make api-roundtrip` | 366 passed, 0 failed (of 366 pinned) |
| unity build (`BATCH_SIZE=0`) | built |

`make wasm-test` was not run locally (no Emscripten here); CI covers it.

New tests in `IdAttributeApiTest.cpp` cover scrubbing, the
compare-your-input check, a scrubbed id surviving a round trip, and the
value semantics including both map kinds. The existing
`segnoAndCodaRoundTrip` test in `DirectionWriterTest.cpp` compares whole
objects, so it covers the two converted ids through the equality block.
LineData lineData;

SpannerStart() : number{}, tickTimePosition{0}, positionData{}, printData{}, lineData{}
// The id attribute of the element this spanner end is written as -- <bracket>,

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.

End? This is a spammer start.

@webern
webern merged commit 786a7ab into main Aug 23, 2026
8 checks passed
@webern
webern deleted the claude/musicxml-id-attributes-api-rfi9qv branch August 23, 2026 09:17
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. api Affects the mx::api layer feature new feature request non-breaking fixes or implementation that do not require breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant