From f4e6166b82ea5f1771a1b96f0e698f7c049af6a3 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Tue, 7 Jul 2026 07:04:00 +0000 Subject: [PATCH] docs: confirm isTieStart+isTieStop already model the continuation tie #291 asked whether mx::api needs a dedicated ContinuationTie class (by analogy with TieLetRing, #286) to model "ties at the start of 2nd endings and the like." Per the MusicXML spec (core::TiedType's doc comment): "For other ties that are visually attached to a single note, such as a tie leading into or out of a repeated section or coda, use two tied elements on the same note, one start and one stop." NoteData::isTieStart/isTieStop are already independent bools (unlike TieLetRing's lone, unpaired mark, this is an ordinary tie on each side), and NoteWriter already emits both and pairs when both are set (NoteWriter::addTie, called for stop then start). So the exact scenario rpatters1 described already round-trips with the existing model -- a new class would just be a second, redundant way to say the same thing, which is what #249's "one fact, one field" is explicitly meant to prevent. Added a regression test proving it (a note with isTieStop and isTieStart both true survives write -> read intact) and expanded the field's doc comment to state this directly, since the shape isn't obvious from the field declarations alone -- closing the loop on the design question with a concrete, working answer instead of new API surface. ## Testing - [x] New `tieStopAndStartOnSameNote` test (NoteDataTest.cpp) - [x] make test: all pass (4734 assertions in 379 test cases, plus examples) - [x] make test-api-roundtrip: 158 passed, 0 failed (of 158 pinned; no regressions -- no api behavior changed) - [x] make fmt / make check: clean Closes #291 --- src/include/mx/api/NoteData.h | 8 ++++ src/private/mxtest/api/NoteDataTest.cpp | 57 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/include/mx/api/NoteData.h b/src/include/mx/api/NoteData.h index 19fc1001e..9ecf24f8d 100644 --- a/src/include/mx/api/NoteData.h +++ b/src/include/mx/api/NoteData.h @@ -113,6 +113,14 @@ class NoteData // One field, two encodings: on write these emit both (sound) and // (notation), so the two can never contradict each other. + // + // The two are independent, so both may be true on the same note: this is exactly how + // MusicXML represents "a tie leading into or out of a repeated section or coda" (spec + // wording) -- e.g. the last note of a first ending, which closes the tie arriving from + // the note before it (isTieStop) and opens a new one continued into the corresponding + // note of the second ending (isTieStart). No separate "continuation tie" concept is + // needed; see TieLetRing just below for the one tie shape that genuinely doesn't fit + // this start/stop pairing. bool isTieStart; bool isTieStop; diff --git a/src/private/mxtest/api/NoteDataTest.cpp b/src/private/mxtest/api/NoteDataTest.cpp index 805c2c569..5041ec7e8 100644 --- a/src/private/mxtest/api/NoteDataTest.cpp +++ b/src/private/mxtest/api/NoteDataTest.cpp @@ -1480,4 +1480,61 @@ TEST(strongAccentDirection, NoteData) T_END; +// #291: a tie leading into or out of a repeated section (e.g. the last note of a first +// ending tied to the corresponding note of a second ending) is not a distinct api concept -- +// per the MusicXML spec (core::TiedType's doc comment) it is simply a note whose isTieStop +// (closing the tie arriving from before) and isTieStart (opening the tie continued into the +// next pass) are both set. NoteData::isTieStart/isTieStop are independent bools, so this +// already works with no new type; this test proves the full note -> both / pairs +// -> both bools round trip. +TEST(tieStopAndStartOnSameNote, NoteData) +{ + ScoreData score; + score.parts.emplace_back(); + auto &part = score.parts.back(); + part.measures.emplace_back(); + auto &measure = part.measures.back(); + measure.staves.emplace_back(); + auto &voice = measure.staves.back().voices[0]; + + // end of a first ending: closes the tie from the previous note, opens one continued + // into the second ending's first note + NoteData middleNote; + middleNote.tickTimePosition = 0; + middleNote.durationData.durationTimeTicks = score.ticksPerQuarter; + middleNote.durationData.durationName = DurationName::quarter; + middleNote.isTieStop = true; + middleNote.isTieStart = true; + voice.notes.push_back(middleNote); + + auto &mgr = DocumentManager::getInstance(); + const auto r1 = mgr.createFromScore(score); + REQUIRE(r1.ok()); + auto docId = r1.value(); + std::stringstream ss; + mgr.writeToStream(docId, ss); + mgr.destroyDocument(docId); + const std::string xml = ss.str(); + CHECK(xml.find(R"()") != std::string::npos); + CHECK(xml.find(R"()") != std::string::npos); + CHECK(xml.find(R"()") != std::string::npos); + CHECK(xml.find(R"()") != std::string::npos); + + std::istringstream iss{xml}; + const auto r2 = mgr.createFromStream(iss); + REQUIRE(r2.ok()); + docId = r2.value(); + const auto rd = mgr.getData(docId); + REQUIRE(rd.ok()); + const auto outScore = rd.value(); + mgr.destroyDocument(docId); + + const auto &outNotes = outScore.parts.back().measures.back().staves.back().voices.at(0).notes; + REQUIRE(outNotes.size() == static_cast(1)); + CHECK(outNotes.at(0).isTieStop); + CHECK(outNotes.at(0).isTieStart); +} + +T_END; + #endif