From 142e7eb8833a2490152a5d59bf65ff57f0bd4e40 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Thu, 9 Oct 2025 13:34:22 +0300 Subject: [PATCH 1/9] minor cleanup --- .../internal/compat/scoreelementsscanner.cpp | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index a6037c6b92d22..995aa683ce645 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -29,21 +29,22 @@ #include "engraving/dom/tempotext.h" using namespace mu::converter; +using namespace mu::engraving; struct ScannerData { - using ElementKey = std::pair; + using ElementKey = std::pair; // in ScoreElementScanner::Options options; // out ScoreElementScanner::InstrumentElementMap elements; - std::set chords; - std::set spanners; - std::map > > uniqueNames; + std::set chords; + std::set spanners; + std::map > > uniqueNames; }; -static bool itemAccepted(const mu::engraving::EngravingItem* item, const mu::engraving::ElementTypeSet& acceptedTypes) +static bool itemAccepted(const EngravingItem* item, const ElementTypeSet& acceptedTypes) { // Ignore temporary / invalid elements and elements that cannot be interacted with if (!item || !item->part() || item->generated() || !item->selectable() || !item->isInteractionAvailable()) { @@ -58,60 +59,60 @@ static bool itemAccepted(const mu::engraving::EngravingItem* item, const mu::eng return true; } - mu::engraving::ElementType type = item->type(); + ElementType type = item->type(); if (item->isNote()) { - const mu::engraving::Chord* chord = toNote(item)->chord(); + const Chord* chord = toNote(item)->chord(); if (chord->notes().size() > 1) { type = chord->type(); } } else if (item->isSpannerSegment()) { - type = mu::engraving::toSpannerSegment(item)->spanner()->type(); + type = toSpannerSegment(item)->spanner()->type(); } return muse::contains(acceptedTypes, type); } -static bool isChordArticulation(const mu::engraving::EngravingItem* item) +static bool isChordArticulation(const EngravingItem* item) { - const mu::engraving::EngravingItem* parent = item->parentItem(); + const EngravingItem* parent = item->parentItem(); if (!parent || !parent->isChord()) { return false; } - static const std::unordered_set CHORD_ARTICULATION_TYPES { - mu::engraving::ElementType::ARPEGGIO, - mu::engraving::ElementType::TREMOLO_SINGLECHORD, - mu::engraving::ElementType::ORNAMENT, + static const std::unordered_set CHORD_ARTICULATION_TYPES { + ElementType::ARPEGGIO, + ElementType::TREMOLO_SINGLECHORD, + ElementType::ORNAMENT, }; return muse::contains(CHORD_ARTICULATION_TYPES, item->type()); } -static muse::String chordToNotes(const mu::engraving::Chord* chord) +static muse::String chordToNotes(const Chord* chord) { muse::StringList notes; - for (const mu::engraving::Note* note : chord->notes()) { + for (const Note* note : chord->notes()) { notes.push_back(note->tpcUserName()); } return notes.join(u" "); } -static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::EngravingItem* item) +static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) { if (!itemAccepted(item, scannerData->options.acceptedTypes)) { return; } - mu::engraving::ElementType type = item->type(); + ElementType type = item->type(); ScoreElementScanner::ElementInfo info; bool locationIsSet = false; if (item->isNote()) { - mu::engraving::Note* note = mu::engraving::toNote(item); - mu::engraving::Chord* chord = note->chord(); + Note* note = toNote(item); + Chord* chord = note->chord(); if (chord->notes().size() > 1) { if (muse::contains(scannerData->chords, chord)) { return; @@ -123,8 +124,13 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav } else { info.name = note->tpcUserName(); } + } else if (isChordArticulation(item)) { + Chord* chord = toChord(item->parentItem()); + scannerData->chords.insert(chord); + info.name = item->translatedSubtypeUserName(); + info.notes = chordToNotes(chord); } else if (item->isSpannerSegment()) { - mu::engraving::Spanner* spanner = mu::engraving::toSpannerSegment(item)->spanner(); + Spanner* spanner = toSpannerSegment(item)->spanner(); if (muse::contains(scannerData->spanners, spanner)) { return; } @@ -134,17 +140,17 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav info.name = spanner->translatedSubtypeUserName(); scannerData->spanners.insert(spanner); - const mu::engraving::Segment* startSegment = spanner->startSegment(); + const Segment* startSegment = spanner->startSegment(); if (startSegment) { - const mu::engraving::EngravingItem::BarBeat barbeat = startSegment->barbeat(); + const EngravingItem::BarBeat barbeat = startSegment->barbeat(); info.start.measureIdx = barbeat.bar - 1; info.start.beat = barbeat.beat - 1.; info.start.trackIdx = spanner->track(); } - const mu::engraving::Segment* endSegment = spanner->endSegment(); + const Segment* endSegment = spanner->endSegment(); if (endSegment) { - const mu::engraving::EngravingItem::BarBeat barbeat = endSegment->barbeat(); + const EngravingItem::BarBeat barbeat = endSegment->barbeat(); info.end.measureIdx = barbeat.bar - 1; info.end.beat = barbeat.beat - 1.; info.end.trackIdx = spanner->track2(); @@ -152,18 +158,13 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav locationIsSet = startSegment || endSegment; } else if (item->isHarmony()) { - info.name = mu::engraving::toHarmony(item)->harmonyName(); - } else if (isChordArticulation(item)) { - mu::engraving::Chord* chord = mu::engraving::toChord(item->parentItem()); - scannerData->chords.insert(chord); - info.name = item->translatedSubtypeUserName(); - info.notes = chordToNotes(chord); + info.name = toHarmony(item)->harmonyName(); } else if (item->isTempoText()) { - info.text = mu::engraving::toTempoText(item)->tempoInfo(); + info.text = toTempoText(item)->tempoInfo(); } else if (item->isPlayTechAnnotation() || item->isDynamic()) { info.name = item->translatedSubtypeUserName(); } else if (item->isTextBase()) { - info.text = mu::engraving::toTextBase(item)->plainText(); + info.text = toTextBase(item)->plainText(); } else { info.name = item->translatedSubtypeUserName(); } @@ -172,8 +173,8 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav info.name = item->typeUserName().translated(); } - const mu::engraving::Part* part = item->part(); - const mu::engraving::InstrumentTrackId trackId { + const Part* part = item->part(); + const InstrumentTrackId trackId { part->id(), part->instrumentId(item->tick()) }; @@ -191,7 +192,7 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav } if (!locationIsSet) { - const mu::engraving::EngravingItem::BarBeat barbeat = item->barbeat(); + const EngravingItem::BarBeat barbeat = item->barbeat(); info.start.trackIdx = item->track(); info.start.measureIdx = barbeat.bar - 1; info.start.beat = barbeat.beat - 1.; @@ -201,7 +202,7 @@ static void addElementInfoIfNeed(ScannerData* scannerData, mu::engraving::Engrav scannerData->elements[trackId][type].push_back(info); } -ScoreElementScanner::InstrumentElementMap ScoreElementScanner::scanElements(mu::engraving::Score* score, const Options& options) +ScoreElementScanner::InstrumentElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) { TRACEFUNC; From f7c57f3645ffb9005aa9fbe004fe64cfd02216b4 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Wed, 8 Oct 2025 15:13:33 +0300 Subject: [PATCH 2/9] --score-elements: don't skip generated elements, but skip note parts --- .../internal/compat/scoreelementsscanner.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index 995aa683ce645..f83b07bf14225 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -47,7 +47,23 @@ struct ScannerData { static bool itemAccepted(const EngravingItem* item, const ElementTypeSet& acceptedTypes) { // Ignore temporary / invalid elements and elements that cannot be interacted with - if (!item || !item->part() || item->generated() || !item->selectable() || !item->isInteractionAvailable()) { + if (!item || !item->part() || !item->selectable() || !item->isInteractionAvailable()) { + return false; + } + + static const ElementTypeSet NOTE_PARTS { + ElementType::ACCIDENTAL, + ElementType::STEM, + ElementType::HOOK, + ElementType::BEAM, + ElementType::NOTEDOT, + }; + + if (muse::contains(NOTE_PARTS, item->type())) { + return false; + } + + if (item->isBarLine() && item->tick().ticks() == 0) { return false; } From 89fc23a87560cf43f50c34f241b5c3ee92a974d5 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Thu, 9 Oct 2025 13:52:32 +0300 Subject: [PATCH 3/9] --score-elements: export durations & simplify note name --- src/converter/internal/compat/backendapi.cpp | 4 ++++ .../internal/compat/scoreelementsscanner.cpp | 14 ++++++++++++-- .../internal/compat/scoreelementsscanner.h | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/converter/internal/compat/backendapi.cpp b/src/converter/internal/compat/backendapi.cpp index 0dfb6dbcbb376..9398a32815d26 100644 --- a/src/converter/internal/compat/backendapi.cpp +++ b/src/converter/internal/compat/backendapi.cpp @@ -726,6 +726,10 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio obj["notes"] = element.notes.toQString(); } + if (!element.duration.empty()) { + obj["duration"] = element.duration.toQString(); + } + if (!element.text.empty()) { obj["text"] = element.text.toQString(); } diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index f83b07bf14225..c7add785ef8f0 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -105,12 +105,18 @@ static bool isChordArticulation(const EngravingItem* item) return muse::contains(CHORD_ARTICULATION_TYPES, item->type()); } +static muse::String noteName(const Note* note) +{ + return tpc2name(note->tpc(), NoteSpellingType::STANDARD, NoteCaseType::AUTO) + String::number(note->octave()); +} + static muse::String chordToNotes(const Chord* chord) { muse::StringList notes; + notes.reserve(chord->notes().size()); for (const Note* note : chord->notes()) { - notes.push_back(note->tpcUserName()); + notes.push_back(noteName(note)); } return notes.join(u" "); @@ -138,13 +144,17 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) scannerData->chords.insert(chord); info.notes = chordToNotes(chord); } else { - info.name = note->tpcUserName(); + info.name = noteName(note); } + info.duration = chord->durationUserName(); } else if (isChordArticulation(item)) { Chord* chord = toChord(item->parentItem()); scannerData->chords.insert(chord); info.name = item->translatedSubtypeUserName(); info.notes = chordToNotes(chord); + info.duration = chord->durationUserName(); + } else if (item->isRest()) { + info.duration = toRest(item)->durationUserName(); } else if (item->isSpannerSegment()) { Spanner* spanner = toSpannerSegment(item)->spanner(); if (muse::contains(scannerData->spanners, spanner)) { diff --git a/src/converter/internal/compat/scoreelementsscanner.h b/src/converter/internal/compat/scoreelementsscanner.h index c9125e4698488..a4bcb82f946ac 100644 --- a/src/converter/internal/compat/scoreelementsscanner.h +++ b/src/converter/internal/compat/scoreelementsscanner.h @@ -38,6 +38,7 @@ class ScoreElementScanner { muse::String name; muse::String notes; + muse::String duration; muse::String text; struct Location { From 853f856c88bf6122e66a866dd4d907c8b998422c Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Thu, 9 Oct 2025 14:51:27 +0300 Subject: [PATCH 4/9] move ElementInfo to separate file --- src/converter/CMakeLists.txt | 1 + src/converter/internal/compat/backendapi.cpp | 6 +-- src/converter/internal/compat/backendtypes.h | 53 +++++++++++++++++++ .../internal/compat/scoreelementsscanner.cpp | 6 +-- .../internal/compat/scoreelementsscanner.h | 28 +--------- .../tests/scoreelementsscanner_tests.cpp | 17 +++--- 6 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 src/converter/internal/compat/backendtypes.h diff --git a/src/converter/CMakeLists.txt b/src/converter/CMakeLists.txt index 41cdb58088036..f84d8605f5b74 100644 --- a/src/converter/CMakeLists.txt +++ b/src/converter/CMakeLists.txt @@ -41,6 +41,7 @@ set(MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/internal/compat/backendapi.h ${CMAKE_CURRENT_LIST_DIR}/internal/compat/backendjsonwriter.cpp ${CMAKE_CURRENT_LIST_DIR}/internal/compat/backendjsonwriter.h + ${CMAKE_CURRENT_LIST_DIR}/internal/compat/backendtypes.h ${CMAKE_CURRENT_LIST_DIR}/internal/compat/notationmeta.cpp ${CMAKE_CURRENT_LIST_DIR}/internal/compat/notationmeta.h ${CMAKE_CURRENT_LIST_DIR}/internal/compat/scoreelementsscanner.cpp diff --git a/src/converter/internal/compat/backendapi.cpp b/src/converter/internal/compat/backendapi.cpp index 9398a32815d26..aec40af2d7fb2 100644 --- a/src/converter/internal/compat/backendapi.cpp +++ b/src/converter/internal/compat/backendapi.cpp @@ -690,11 +690,11 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio { mu::engraving::Score* score = notation->elements()->msScore(); ScoreElementScanner::Options options = parseScoreElementScannerOptions(optionsJson); - ScoreElementScanner::InstrumentElementMap elements = ScoreElementScanner::scanElements(score, options); + InstrumentElementMap elements = ScoreElementScanner::scanElements(score, options); QJsonArray rootArray; - auto writeLocation = [](const ScoreElementScanner::ElementInfo::Location& loc, QJsonObject& obj) { + auto writeLocation = [](const ElementInfo::Location& loc, QJsonObject& obj) { if (loc.trackIdx != muse::nidx) { obj["staffIdx"] = (int)mu::engraving::track2staff(loc.trackIdx); obj["voiceIdx"] = (int)mu::engraving::track2voice(loc.trackIdx); @@ -715,7 +715,7 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio for (const auto& pair: instrumentPair.second) { QJsonArray elementArray; - for (const ScoreElementScanner::ElementInfo& element : pair.second) { + for (const ElementInfo& element : pair.second) { QJsonObject obj; if (!element.name.empty()) { diff --git a/src/converter/internal/compat/backendtypes.h b/src/converter/internal/compat/backendtypes.h new file mode 100644 index 0000000000000..3b0c0e9cb05b2 --- /dev/null +++ b/src/converter/internal/compat/backendtypes.h @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2025 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include "global/types/string.h" +#include "global/realfn.h" + +#include "engraving/types/types.h" + +namespace mu::converter { +struct ElementInfo +{ + muse::String name; + muse::String notes; + muse::String duration; + muse::String text; + + struct Location { + mu::engraving::track_idx_t trackIdx = muse::nidx; + size_t measureIdx = muse::nidx; + float beat = -1.f; + + bool operator==(const Location& l) const + { + return trackIdx == l.trackIdx && measureIdx == l.measureIdx && muse::RealIsEqual(beat, l.beat); + } + } start, end; +}; + +// Instrument -> ElementType -> Elements +using ElementInfoList = std::vector; +using ElementMap = std::map; +using InstrumentElementMap = std::map; +} diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index c7add785ef8f0..5e0bf8343e290 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -38,7 +38,7 @@ struct ScannerData { ScoreElementScanner::Options options; // out - ScoreElementScanner::InstrumentElementMap elements; + InstrumentElementMap elements; std::set chords; std::set spanners; std::map > > uniqueNames; @@ -129,7 +129,7 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) } ElementType type = item->type(); - ScoreElementScanner::ElementInfo info; + ElementInfo info; bool locationIsSet = false; if (item->isNote()) { @@ -228,7 +228,7 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) scannerData->elements[trackId][type].push_back(info); } -ScoreElementScanner::InstrumentElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) +InstrumentElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) { TRACEFUNC; diff --git a/src/converter/internal/compat/scoreelementsscanner.h b/src/converter/internal/compat/scoreelementsscanner.h index a4bcb82f946ac..2c0a92b63238b 100644 --- a/src/converter/internal/compat/scoreelementsscanner.h +++ b/src/converter/internal/compat/scoreelementsscanner.h @@ -22,9 +22,7 @@ #pragma once -#include "engraving/types/types.h" - -#include "global/realfn.h" +#include "backendtypes.h" namespace mu::engraving { class Score; @@ -34,25 +32,6 @@ namespace mu::converter { class ScoreElementScanner { public: - struct ElementInfo - { - muse::String name; - muse::String notes; - muse::String duration; - muse::String text; - - struct Location { - mu::engraving::track_idx_t trackIdx = muse::nidx; - size_t measureIdx = muse::nidx; - float beat = -1.f; - - bool operator==(const Location& l) const - { - return trackIdx == l.trackIdx && measureIdx == l.measureIdx && muse::RealIsEqual(beat, l.beat); - } - } start, end; - }; - struct Options { Options() {} @@ -60,11 +39,6 @@ class ScoreElementScanner bool avoidDuplicates = false; }; - // Instrument -> ElementType -> Elements - using ElementInfoList = std::vector; - using ElementMap = std::map; - using InstrumentElementMap = std::map; - static InstrumentElementMap scanElements(mu::engraving::Score* score, const Options& options = {}); }; } diff --git a/src/converter/tests/scoreelementsscanner_tests.cpp b/src/converter/tests/scoreelementsscanner_tests.cpp index df15b5fe8a534..a81d89caaae21 100644 --- a/src/converter/tests/scoreelementsscanner_tests.cpp +++ b/src/converter/tests/scoreelementsscanner_tests.cpp @@ -34,9 +34,9 @@ static const muse::String CONVERTER_DATA_DIR("data/"); class Converter_ScoreElementsTests : public ::testing::Test { public: - ScoreElementScanner::ElementInfo makeInfo(const String& name, const String& notes = u"") const + ElementInfo makeInfo(const String& name, const String& notes = u"") const { - ScoreElementScanner::ElementInfo info; + ElementInfo info; info.name = name; info.notes = notes; @@ -76,10 +76,11 @@ TEST_F(Converter_ScoreElementsTests, ScanElements) }; // [WHEN] Scan the score - ScoreElementScanner::InstrumentElementMap result = ScoreElementScanner::scanElements(score, options); + InstrumentElementMap result = ScoreElementScanner::scanElements(score, options); // [THEN] The map matches the expected one - ScoreElementScanner::ElementMap expectedMap; + ElementMap expectedMap; + // 1st measure expectedMap[ElementType::KEYSIG] = { makeInfo(u"C major / A minor") }; expectedMap[ElementType::TIMESIG] = { makeInfo(u"4/4 time") }; @@ -104,19 +105,19 @@ TEST_F(Converter_ScoreElementsTests, ScanElements) const mu::engraving::InstrumentTrackId expectedTrackId { muse::ID(1), u"piano" }; EXPECT_EQ(result.begin()->first, expectedTrackId); - const ScoreElementScanner::ElementMap& actualMap = result.begin()->second; + const ElementMap& actualMap = result.begin()->second; EXPECT_EQ(actualMap.size(), expectedMap.size()); for (const auto& pair : actualMap) { auto it = expectedMap.find(pair.first); ASSERT_TRUE(it != expectedMap.end()); - const ScoreElementScanner::ElementInfoList& expectedInfoList = it->second; + const ElementInfoList& expectedInfoList = it->second; ASSERT_EQ(pair.second.size(), expectedInfoList.size()); for (size_t i = 0; i < pair.second.size(); ++i) { - const ScoreElementScanner::ElementInfo& actualInfo = pair.second.at(i); - const ScoreElementScanner::ElementInfo& expectedInfo = expectedInfoList.at(i); + const ElementInfo& actualInfo = pair.second.at(i); + const ElementInfo& expectedInfo = expectedInfoList.at(i); EXPECT_EQ(actualInfo.name, expectedInfo.name); EXPECT_EQ(actualInfo.notes, expectedInfo.notes); From ac7d3f07712798253a95aeed300777a62d831ab1 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Fri, 10 Oct 2025 15:18:42 +0300 Subject: [PATCH 5/9] --score-elements: simplify output by making "type" object property --- src/converter/internal/compat/backendapi.cpp | 86 +++++++++---------- src/converter/internal/compat/backendtypes.h | 14 +-- .../internal/compat/scoreelementsscanner.cpp | 33 +++++-- .../internal/compat/scoreelementsscanner.h | 2 +- .../tests/scoreelementsscanner_tests.cpp | 54 ++++++------ 5 files changed, 101 insertions(+), 88 deletions(-) diff --git a/src/converter/internal/compat/backendapi.cpp b/src/converter/internal/compat/backendapi.cpp index aec40af2d7fb2..bdd01e4a1a6cd 100644 --- a/src/converter/internal/compat/backendapi.cpp +++ b/src/converter/internal/compat/backendapi.cpp @@ -690,14 +690,17 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio { mu::engraving::Score* score = notation->elements()->msScore(); ScoreElementScanner::Options options = parseScoreElementScannerOptions(optionsJson); - InstrumentElementMap elements = ScoreElementScanner::scanElements(score, options); + ElementMap elements = ScoreElementScanner::scanElements(score, options); QJsonArray rootArray; auto writeLocation = [](const ElementInfo::Location& loc, QJsonObject& obj) { - if (loc.trackIdx != muse::nidx) { - obj["staffIdx"] = (int)mu::engraving::track2staff(loc.trackIdx); - obj["voiceIdx"] = (int)mu::engraving::track2voice(loc.trackIdx); + if (loc.staffIdx != muse::nidx) { + obj["staffIdx"] = static_cast(loc.staffIdx); + } + + if (loc.voiceIdx != muse::nidx) { + obj["voiceIdx"] = static_cast(loc.voiceIdx); } if (loc.measureIdx != muse::nidx) { @@ -710,55 +713,46 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio }; for (const auto& instrumentPair : elements) { - QJsonArray typeArray; - - for (const auto& pair: instrumentPair.second) { - QJsonArray elementArray; - - for (const ElementInfo& element : pair.second) { - QJsonObject obj; - - if (!element.name.empty()) { - obj["name"] = element.name.toQString(); - } - - if (!element.notes.empty()) { - obj["notes"] = element.notes.toQString(); - } - - if (!element.duration.empty()) { - obj["duration"] = element.duration.toQString(); - } - - if (!element.text.empty()) { - obj["text"] = element.text.toQString(); - } - - if (element.start == element.end) { - writeLocation(element.start, obj); - } else { - QJsonObject start, end; - writeLocation(element.start, start); - writeLocation(element.end, end); - obj["start"] = start; - obj["end"] = end; - } - - if (!obj.empty()) { - elementArray << obj; - } + QJsonArray elementArray; + + for (const ElementInfo& element : instrumentPair.second) { + QJsonObject obj; + + obj["type"] = mu::engraving::TConv::toXml(element.type).ascii(); + + if (!element.name.empty()) { + obj["name"] = element.name.toQString(); + } + + if (!element.notes.empty()) { + obj["notes"] = element.notes.toQString(); + } + + if (!element.duration.empty()) { + obj["duration"] = element.duration.toQString(); + } + + if (!element.text.empty()) { + obj["text"] = element.text.toQString(); + } + + if (element.start == element.end) { + writeLocation(element.start, obj); + } else { + QJsonObject start, end; + writeLocation(element.start, start); + writeLocation(element.end, end); + obj["start"] = start; + obj["end"] = end; } - QString type = mu::engraving::TConv::toXml(pair.first).ascii(); - QJsonObject typeObj; - typeObj[type] = elementArray; - typeArray << typeObj; + elementArray << obj; } QJsonObject instrumentObj; instrumentObj["instrumentId"] = instrumentPair.first.instrumentId.toQString(); instrumentObj["partId"] = instrumentPair.first.partId.toQString(); - instrumentObj["types"] = typeArray; + instrumentObj["elements"] = elementArray; rootArray << instrumentObj; } diff --git a/src/converter/internal/compat/backendtypes.h b/src/converter/internal/compat/backendtypes.h index 3b0c0e9cb05b2..7922371446272 100644 --- a/src/converter/internal/compat/backendtypes.h +++ b/src/converter/internal/compat/backendtypes.h @@ -29,25 +29,29 @@ namespace mu::converter { struct ElementInfo { + mu::engraving::ElementType type = mu::engraving::ElementType::INVALID; muse::String name; muse::String notes; muse::String duration; muse::String text; struct Location { - mu::engraving::track_idx_t trackIdx = muse::nidx; + mu::engraving::staff_idx_t staffIdx = muse::nidx; + mu::engraving::voice_idx_t voiceIdx = muse::nidx; size_t measureIdx = muse::nidx; float beat = -1.f; bool operator==(const Location& l) const { - return trackIdx == l.trackIdx && measureIdx == l.measureIdx && muse::RealIsEqual(beat, l.beat); + return staffIdx == l.staffIdx + && voiceIdx == l.voiceIdx + && measureIdx == l.measureIdx + && muse::RealIsEqual(beat, l.beat); } } start, end; }; -// Instrument -> ElementType -> Elements +// Instrument -> Elements using ElementInfoList = std::vector; -using ElementMap = std::map; -using InstrumentElementMap = std::map; +using ElementMap = std::map; } diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index 5e0bf8343e290..4b8d13427d18c 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -38,7 +38,7 @@ struct ScannerData { ScoreElementScanner::Options options; // out - InstrumentElementMap elements; + ElementMap elements; std::set chords; std::set spanners; std::map > > uniqueNames; @@ -171,7 +171,8 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) const EngravingItem::BarBeat barbeat = startSegment->barbeat(); info.start.measureIdx = barbeat.bar - 1; info.start.beat = barbeat.beat - 1.; - info.start.trackIdx = spanner->track(); + info.start.staffIdx = track2staff(spanner->track()); + info.start.voiceIdx = track2voice(spanner->track()); } const Segment* endSegment = spanner->endSegment(); @@ -179,7 +180,8 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) const EngravingItem::BarBeat barbeat = endSegment->barbeat(); info.end.measureIdx = barbeat.bar - 1; info.end.beat = barbeat.beat - 1.; - info.end.trackIdx = spanner->track2(); + info.end.staffIdx = track2staff(spanner->track2()); + info.end.voiceIdx = track2voice(spanner->track2()); } locationIsSet = startSegment || endSegment; @@ -199,6 +201,8 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) info.name = item->typeUserName().translated(); } + info.type = type; + const Part* part = item->part(); const InstrumentTrackId trackId { part->id(), @@ -219,16 +223,17 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) if (!locationIsSet) { const EngravingItem::BarBeat barbeat = item->barbeat(); - info.start.trackIdx = item->track(); + info.start.staffIdx = item->staffIdx(); + info.start.voiceIdx = item->voice(); info.start.measureIdx = barbeat.bar - 1; info.start.beat = barbeat.beat - 1.; info.end = info.start; } - scannerData->elements[trackId][type].push_back(info); + scannerData->elements[trackId].emplace_back(std::move(info)); } -InstrumentElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) +ElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) { TRACEFUNC; @@ -237,5 +242,21 @@ InstrumentElementMap ScoreElementScanner::scanElements(Score* score, const Optio score->scanElements([&](mu::engraving::EngravingItem* item) { addElementInfoIfNeed(&data, item); }); + // Sort elements: staff -> measure -> beat -> voice + for (auto& pair : data.elements) { + std::stable_sort(pair.second.begin(), pair.second.end(), [](const ElementInfo& a, const ElementInfo& b) { + if (a.start.staffIdx != b.start.staffIdx) { + return a.start.staffIdx < b.start.staffIdx; + } + if (a.start.measureIdx != b.start.measureIdx) { + return a.start.measureIdx < b.start.measureIdx; + } + if (a.start.beat != b.start.beat) { + return a.start.beat < b.start.beat; + } + return a.start.voiceIdx < b.start.voiceIdx; + }); + } + return data.elements; } diff --git a/src/converter/internal/compat/scoreelementsscanner.h b/src/converter/internal/compat/scoreelementsscanner.h index 2c0a92b63238b..cd03877ff05a0 100644 --- a/src/converter/internal/compat/scoreelementsscanner.h +++ b/src/converter/internal/compat/scoreelementsscanner.h @@ -39,6 +39,6 @@ class ScoreElementScanner bool avoidDuplicates = false; }; - static InstrumentElementMap scanElements(mu::engraving::Score* score, const Options& options = {}); + static ElementMap scanElements(mu::engraving::Score* score, const Options& options = {}); }; } diff --git a/src/converter/tests/scoreelementsscanner_tests.cpp b/src/converter/tests/scoreelementsscanner_tests.cpp index a81d89caaae21..24fa6ac9fbccc 100644 --- a/src/converter/tests/scoreelementsscanner_tests.cpp +++ b/src/converter/tests/scoreelementsscanner_tests.cpp @@ -34,9 +34,10 @@ static const muse::String CONVERTER_DATA_DIR("data/"); class Converter_ScoreElementsTests : public ::testing::Test { public: - ElementInfo makeInfo(const String& name, const String& notes = u"") const + ElementInfo makeInfo(ElementType type, const String& name, const String& notes = u"") const { ElementInfo info; + info.type = type; info.name = name; info.notes = notes; @@ -76,53 +77,46 @@ TEST_F(Converter_ScoreElementsTests, ScanElements) }; // [WHEN] Scan the score - InstrumentElementMap result = ScoreElementScanner::scanElements(score, options); + ElementMap result = ScoreElementScanner::scanElements(score, options); - // [THEN] The map matches the expected one - ElementMap expectedMap; + // [THEN] The list matches the expected one + ElementInfoList expectedList; // 1st measure - expectedMap[ElementType::KEYSIG] = { makeInfo(u"C major / A minor") }; - expectedMap[ElementType::TIMESIG] = { makeInfo(u"4/4 time") }; - expectedMap[ElementType::ARPEGGIO] = { makeInfo(u"Up arpeggio", u"C5 E5 G5 B5") }; - expectedMap[ElementType::CHORD] = { makeInfo(u"", u"C5 E5 G5 B5") }; - expectedMap[ElementType::TREMOLO_SINGLECHORD] = { makeInfo(u"32nd through stem", u"F4 A4 C5") }; + expectedList.emplace_back(makeInfo(ElementType::KEYSIG, u"C major / A minor")); + expectedList.emplace_back(makeInfo(ElementType::TIMESIG, u"4/4 time")); + expectedList.emplace_back(makeInfo(ElementType::ARPEGGIO, u"Up arpeggio", u"C5 E5 G5 B5")); + expectedList.emplace_back(makeInfo(ElementType::CHORD, u"", u"C5 E5 G5 B5")); + expectedList.emplace_back(makeInfo(ElementType::TREMOLO_SINGLECHORD, u"32nd through stem", u"F4 A4 C5")); // 2nd measure - expectedMap[ElementType::ORNAMENT] = { makeInfo(u"Turn", u"A4 E5") }; // skip duplicates + expectedList.emplace_back(makeInfo(ElementType::ORNAMENT, u"Turn", u"A4 E5")); // skip duplicates // 3rd measure - expectedMap[ElementType::TRILL] = { makeInfo(u"Trill line") }; + expectedList.emplace_back(makeInfo(ElementType::TRILL, u"Trill line")); // 4th measure - expectedMap[ElementType::GRADUAL_TEMPO_CHANGE] = { makeInfo(u"accel.") }; - expectedMap[ElementType::HAIRPIN] = { makeInfo(u"Crescendo hairpin") }; + expectedList.emplace_back(makeInfo(ElementType::HAIRPIN, u"Crescendo hairpin")); + expectedList.emplace_back(makeInfo(ElementType::GRADUAL_TEMPO_CHANGE, u"accel.")); // 5th measure - expectedMap[ElementType::PLAYTECH_ANNOTATION] = { makeInfo(u"Pizzicato") }; + expectedList.emplace_back(makeInfo(ElementType::PLAYTECH_ANNOTATION, u"Pizzicato")); ASSERT_EQ(result.size(), 1); const mu::engraving::InstrumentTrackId expectedTrackId { muse::ID(1), u"piano" }; EXPECT_EQ(result.begin()->first, expectedTrackId); - const ElementMap& actualMap = result.begin()->second; - EXPECT_EQ(actualMap.size(), expectedMap.size()); + const ElementInfoList& actualList = result.begin()->second; + EXPECT_EQ(expectedList.size(), actualList.size()); - for (const auto& pair : actualMap) { - auto it = expectedMap.find(pair.first); - ASSERT_TRUE(it != expectedMap.end()); + for (size_t i = 0; i < actualList.size(); ++i) { + const ElementInfo& actualInfo = actualList.at(i); + const ElementInfo& expectedInfo = expectedList.at(i); - const ElementInfoList& expectedInfoList = it->second; - ASSERT_EQ(pair.second.size(), expectedInfoList.size()); - - for (size_t i = 0; i < pair.second.size(); ++i) { - const ElementInfo& actualInfo = pair.second.at(i); - const ElementInfo& expectedInfo = expectedInfoList.at(i); - - EXPECT_EQ(actualInfo.name, expectedInfo.name); - EXPECT_EQ(actualInfo.notes, expectedInfo.notes); - EXPECT_EQ(actualInfo.text, expectedInfo.text); - } + EXPECT_EQ(actualInfo.type, expectedInfo.type); + EXPECT_EQ(actualInfo.name, expectedInfo.name); + EXPECT_EQ(actualInfo.notes, expectedInfo.notes); + EXPECT_EQ(actualInfo.text, expectedInfo.text); } delete score; From 5c066f6cac2635f191ab54345cf6792fb1ee5112 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Wed, 15 Oct 2025 19:48:00 +0300 Subject: [PATCH 6/9] --score-elements: fix last measure index --- src/converter/internal/compat/scoreelementsscanner.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index 4b8d13427d18c..0214560fcc636 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -36,6 +36,7 @@ struct ScannerData { // in ScoreElementScanner::Options options; + size_t measures = 0; // out ElementMap elements; @@ -230,6 +231,14 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) info.end = info.start; } + if (info.start.measureIdx >= scannerData->measures) { + info.start.measureIdx = scannerData->measures - 1; + } + + if (info.end.measureIdx >= scannerData->measures) { + info.end.measureIdx = scannerData->measures - 1; + } + scannerData->elements[trackId].emplace_back(std::move(info)); } @@ -239,6 +248,7 @@ ElementMap ScoreElementScanner::scanElements(Score* score, const Options& option ScannerData data; data.options = options; + data.measures = score->nmeasures(); score->scanElements([&](mu::engraving::EngravingItem* item) { addElementInfoIfNeed(&data, item); }); From 61efa7aefe1942d76b912073d1c222991ce18e40 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Thu, 16 Oct 2025 11:13:45 +0300 Subject: [PATCH 7/9] --score-elements: remove options --- src/app/internal/commandlineparser.cpp | 4 +- src/app/internal/consoleapp.cpp | 3 +- src/converter/iconvertercontroller.h | 3 +- src/converter/internal/compat/backendapi.cpp | 43 ++------------- src/converter/internal/compat/backendapi.h | 6 +-- .../internal/compat/scoreelementsscanner.cpp | 46 ++-------------- .../internal/compat/scoreelementsscanner.h | 9 +--- .../internal/convertercontroller.cpp | 4 +- src/converter/internal/convertercontroller.h | 3 +- .../tests/scoreelementsscanner_tests.cpp | 53 +++++++++---------- 10 files changed, 44 insertions(+), 130 deletions(-) diff --git a/src/app/internal/commandlineparser.cpp b/src/app/internal/commandlineparser.cpp index 3ecd99ff2d2d5..322c212c067bf 100644 --- a/src/app/internal/commandlineparser.cpp +++ b/src/app/internal/commandlineparser.cpp @@ -109,8 +109,7 @@ void CommandLineParser::init() "Transpose the given score and export the data to a single JSON file, print it to stdout", "options")); m_parser.addOption(QCommandLineOption("score-elements", - "Scan the given score and export elements to a single JSON file, print it to stdout", - "options")); + "Scan the given score and export elements to a single JSON file, print it to stdout")); m_parser.addOption(QCommandLineOption("source-update", "Update the source in the given score")); m_parser.addOption(QCommandLineOption({ "S", "style" }, "Load style file", "style")); @@ -373,7 +372,6 @@ void CommandLineParser::parse(int argc, char** argv) m_options.runMode = IApplication::RunMode::ConsoleApp; m_options.converterTask.type = ConvertType::ExportScoreElements; m_options.converterTask.inputFile = scorefiles[0]; - m_options.converterTask.params[CmdOptions::ParamKey::ScoreElementsOptions] = m_parser.value("score-elements"); } if (m_parser.isSet("source-update")) { diff --git a/src/app/internal/consoleapp.cpp b/src/app/internal/consoleapp.cpp index d40b3deee4bce..00c203d9144eb 100644 --- a/src/app/internal/consoleapp.cpp +++ b/src/app/internal/consoleapp.cpp @@ -331,8 +331,7 @@ int ConsoleApp::processConverter(const CmdOptions::ConverterTask& task) ret = converter()->exportScoreTranspose(task.inputFile, task.outputFile, scoreTranspose, openParams); } break; case ConvertType::ExportScoreElements: { - std::string options = task.params[CmdOptions::ParamKey::ScoreElementsOptions].toString().toStdString(); - ret = converter()->exportScoreElements(task.inputFile, task.outputFile, options, openParams); + ret = converter()->exportScoreElements(task.inputFile, task.outputFile, openParams); } break; case ConvertType::ExportScoreVideo: { ret = converter()->exportScoreVideo(task.inputFile, task.outputFile, openParams); diff --git a/src/converter/iconvertercontroller.h b/src/converter/iconvertercontroller.h index 95890c93fb609..1df67a23f940b 100644 --- a/src/converter/iconvertercontroller.h +++ b/src/converter/iconvertercontroller.h @@ -61,8 +61,7 @@ class IConverterController : MODULE_EXPORT_INTERFACE virtual muse::Ret exportScoreTranspose(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, const OpenParams& openParams = {}) = 0; - virtual muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, - const OpenParams& openParams = {}) = 0; + virtual muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams = {}) = 0; virtual muse::Ret exportScoreVideo(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams = {}) = 0; diff --git a/src/converter/internal/compat/backendapi.cpp b/src/converter/internal/compat/backendapi.cpp index bdd01e4a1a6cd..328212adf6671 100644 --- a/src/converter/internal/compat/backendapi.cpp +++ b/src/converter/internal/compat/backendapi.cpp @@ -69,40 +69,6 @@ static const std::string DEV_INFO_NAME = "devinfo"; static constexpr bool ADD_SEPARATOR = true; static constexpr auto NO_STYLE = ""; -static ScoreElementScanner::Options parseScoreElementScannerOptions(const std::string& json) -{ - if (json.empty()) { - return {}; - } - - QJsonParseError parseError; - const QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(json), &parseError); - if (parseError.error != QJsonParseError::NoError) { - LOGE() << "JSON parse error:" << parseError.errorString(); - return {}; - } - - const QJsonObject obj = doc.object(); - - ScoreElementScanner::Options options; - options.avoidDuplicates = obj.value("avoidDuplicates").toBool(); - - const QJsonArray typeArray = obj.value("types").toArray(); - for (const auto typeObj : typeArray) { - if (!typeObj.isString()) { - continue; - } - - const std::string typeStr = typeObj.toString().toStdString(); - const ElementType type = TConv::fromXml(typeStr, ElementType::INVALID); - if (type != ElementType::INVALID) { - options.acceptedTypes.insert(type); - } - } - - return options; -} - Ret BackendApi::exportScoreMedia(const muse::io::path_t& in, const muse::io::path_t& out, const muse::io::path_t& highlightConfigPath, const muse::io::path_t& stylePath, bool forceMode, @@ -231,7 +197,7 @@ Ret BackendApi::exportScoreTranspose(const muse::io::path_t& in, const muse::io: return result ? make_ret(Ret::Code::Ok) : make_ret(Ret::Code::InternalError); } -Ret BackendApi::exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, +Ret BackendApi::exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const muse::io::path_t& stylePath, bool forceMode) { TRACEFUNC; @@ -246,7 +212,7 @@ Ret BackendApi::exportScoreElements(const muse::io::path_t& in, const muse::io:: QFile outputFile; openOutputFile(outputFile, out); - return doExportScoreElements(notation, optionsJson, outputFile); + return doExportScoreElements(notation, outputFile); } Ret BackendApi::openOutputFile(QFile& file, const muse::io::path_t& out) @@ -686,11 +652,10 @@ Ret BackendApi::doExportScoreTranspose(const INotationPtr notation, BackendJsonW return ret; } -muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notation, const std::string& optionsJson, QIODevice& out) +muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notation, QIODevice& out) { mu::engraving::Score* score = notation->elements()->msScore(); - ScoreElementScanner::Options options = parseScoreElementScannerOptions(optionsJson); - ElementMap elements = ScoreElementScanner::scanElements(score, options); + ElementMap elements = ScoreElementScanner::scanElements(score); QJsonArray rootArray; diff --git a/src/converter/internal/compat/backendapi.h b/src/converter/internal/compat/backendapi.h index cd130037abbd6..35d9fa45ef2fb 100644 --- a/src/converter/internal/compat/backendapi.h +++ b/src/converter/internal/compat/backendapi.h @@ -59,8 +59,8 @@ class BackendApi static muse::Ret exportScoreTranspose(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, const muse::io::path_t& stylePath, bool forceMode = false, bool unrollRepeats = false); - static muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, - const muse::io::path_t& stylePath, bool forceMode = false); + static muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const muse::io::path_t& stylePath, + bool forceMode = false); static muse::Ret updateSource(const muse::io::path_t& in, const std::string& newSource, bool forceMode = false); @@ -95,7 +95,7 @@ class BackendApi static muse::Ret doExportScoreTranspose(const notation::INotationPtr notation, BackendJsonWriter& jsonWriter, bool addSeparator = false); - static muse::Ret doExportScoreElements(const notation::INotationPtr notation, const std::string& optionsJson, QIODevice& out); + static muse::Ret doExportScoreElements(const notation::INotationPtr notation, QIODevice& out); static muse::RetVal scorePartJson(mu::engraving::Score* score, const std::string& fileName); diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index 0214560fcc636..4e4e21c49affb 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -32,20 +32,16 @@ using namespace mu::converter; using namespace mu::engraving; struct ScannerData { - using ElementKey = std::pair; - // in - ScoreElementScanner::Options options; size_t measures = 0; // out ElementMap elements; std::set chords; std::set spanners; - std::map > > uniqueNames; }; -static bool itemAccepted(const EngravingItem* item, const ElementTypeSet& acceptedTypes) +static bool itemAccepted(const EngravingItem* item) { // Ignore temporary / invalid elements and elements that cannot be interacted with if (!item || !item->part() || !item->selectable() || !item->isInteractionAvailable()) { @@ -68,26 +64,7 @@ static bool itemAccepted(const EngravingItem* item, const ElementTypeSet& accept return false; } - if (!item->visible() && !item->score()->isShowInvisible()) { - return false; - } - - if (acceptedTypes.empty()) { - return true; - } - - ElementType type = item->type(); - - if (item->isNote()) { - const Chord* chord = toNote(item)->chord(); - if (chord->notes().size() > 1) { - type = chord->type(); - } - } else if (item->isSpannerSegment()) { - type = toSpannerSegment(item)->spanner()->type(); - } - - return muse::contains(acceptedTypes, type); + return true; } static bool isChordArticulation(const EngravingItem* item) @@ -97,7 +74,7 @@ static bool isChordArticulation(const EngravingItem* item) return false; } - static const std::unordered_set CHORD_ARTICULATION_TYPES { + static const ElementTypeSet CHORD_ARTICULATION_TYPES { ElementType::ARPEGGIO, ElementType::TREMOLO_SINGLECHORD, ElementType::ORNAMENT, @@ -125,7 +102,7 @@ static muse::String chordToNotes(const Chord* chord) static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) { - if (!itemAccepted(item, scannerData->options.acceptedTypes)) { + if (!itemAccepted(item)) { return; } @@ -210,18 +187,6 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) part->instrumentId(item->tick()) }; - if (scannerData->options.avoidDuplicates) { - const muse::String& name = !info.name.empty() ? info.name : info.notes; - const ScannerData::ElementKey key = std::make_pair(type, item->subtype()); - std::set& uniqueNames = scannerData->uniqueNames[trackId][key]; - - if (muse::contains(uniqueNames, name)) { - return; - } - - uniqueNames.insert(name); - } - if (!locationIsSet) { const EngravingItem::BarBeat barbeat = item->barbeat(); info.start.staffIdx = item->staffIdx(); @@ -242,12 +207,11 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) scannerData->elements[trackId].emplace_back(std::move(info)); } -ElementMap ScoreElementScanner::scanElements(Score* score, const Options& options) +ElementMap ScoreElementScanner::scanElements(Score* score) { TRACEFUNC; ScannerData data; - data.options = options; data.measures = score->nmeasures(); score->scanElements([&](mu::engraving::EngravingItem* item) { addElementInfoIfNeed(&data, item); }); diff --git a/src/converter/internal/compat/scoreelementsscanner.h b/src/converter/internal/compat/scoreelementsscanner.h index cd03877ff05a0..69ed2bcae8432 100644 --- a/src/converter/internal/compat/scoreelementsscanner.h +++ b/src/converter/internal/compat/scoreelementsscanner.h @@ -32,13 +32,6 @@ namespace mu::converter { class ScoreElementScanner { public: - struct Options { - Options() {} - - mu::engraving::ElementTypeSet acceptedTypes; - bool avoidDuplicates = false; - }; - - static ElementMap scanElements(mu::engraving::Score* score, const Options& options = {}); + static ElementMap scanElements(mu::engraving::Score* score); }; } diff --git a/src/converter/internal/convertercontroller.cpp b/src/converter/internal/convertercontroller.cpp index 467933264e27c..3308ba2c63499 100644 --- a/src/converter/internal/convertercontroller.cpp +++ b/src/converter/internal/convertercontroller.cpp @@ -600,12 +600,12 @@ Ret ConverterController::exportScoreTranspose(const muse::io::path_t& in, const return BackendApi::exportScoreTranspose(in, out, optionsJson, openParams.stylePath, openParams.forceMode, openParams.unrollRepeats); } -Ret ConverterController::exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, +Ret ConverterController::exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams) { TRACEFUNC; - return BackendApi::exportScoreElements(in, out, optionsJson, openParams.stylePath, openParams.forceMode); + return BackendApi::exportScoreElements(in, out, openParams.stylePath, openParams.forceMode); } Ret ConverterController::exportScoreVideo(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams) diff --git a/src/converter/internal/convertercontroller.h b/src/converter/internal/convertercontroller.h index 9a4ba1476797a..a251d102c9abb 100644 --- a/src/converter/internal/convertercontroller.h +++ b/src/converter/internal/convertercontroller.h @@ -66,8 +66,7 @@ class ConverterController : public IConverterController, public muse::Injectable muse::Ret exportScoreTranspose(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, const OpenParams& openParams = {}) override; - muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson, - const OpenParams& openParams = {}) override; + muse::Ret exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams = {}) override; muse::Ret exportScoreVideo(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams = {}) override; diff --git a/src/converter/tests/scoreelementsscanner_tests.cpp b/src/converter/tests/scoreelementsscanner_tests.cpp index 24fa6ac9fbccc..9ba74403c2cff 100644 --- a/src/converter/tests/scoreelementsscanner_tests.cpp +++ b/src/converter/tests/scoreelementsscanner_tests.cpp @@ -34,7 +34,7 @@ static const muse::String CONVERTER_DATA_DIR("data/"); class Converter_ScoreElementsTests : public ::testing::Test { public: - ElementInfo makeInfo(ElementType type, const String& name, const String& notes = u"") const + ElementInfo makeInfo(ElementType type, const String& name = u"", const String& notes = u"") const { ElementInfo info; info.type = type; @@ -51,56 +51,53 @@ TEST_F(Converter_ScoreElementsTests, ScanElements) Score* score = ScoreRW::readScore(CONVERTER_DATA_DIR + "score_elements.mscx"); ASSERT_TRUE(score); - // [GIVEN] Scanner options - ScoreElementScanner::Options options; - options.avoidDuplicates = true; - options.acceptedTypes = { - // 1st measure - ElementType::KEYSIG, - ElementType::TIMESIG, - ElementType::ARPEGGIO, - ElementType::CHORD, - ElementType::TREMOLO_SINGLECHORD, - - // 2nd measure - ElementType::ORNAMENT, - - // 3rd measure - ElementType::TRILL, - - // 4th measure - ElementType::GRADUAL_TEMPO_CHANGE, - ElementType::HAIRPIN, - - // 5th measure - ElementType::PLAYTECH_ANNOTATION, - }; - // [WHEN] Scan the score - ElementMap result = ScoreElementScanner::scanElements(score, options); + ElementMap result = ScoreElementScanner::scanElements(score); // [THEN] The list matches the expected one ElementInfoList expectedList; // 1st measure + expectedList.emplace_back(makeInfo(ElementType::CLEF, u"Treble clef")); expectedList.emplace_back(makeInfo(ElementType::KEYSIG, u"C major / A minor")); expectedList.emplace_back(makeInfo(ElementType::TIMESIG, u"4/4 time")); expectedList.emplace_back(makeInfo(ElementType::ARPEGGIO, u"Up arpeggio", u"C5 E5 G5 B5")); expectedList.emplace_back(makeInfo(ElementType::CHORD, u"", u"C5 E5 G5 B5")); expectedList.emplace_back(makeInfo(ElementType::TREMOLO_SINGLECHORD, u"32nd through stem", u"F4 A4 C5")); + expectedList.emplace_back(makeInfo(ElementType::REST, u"Rest(s)")); // 2nd measure - expectedList.emplace_back(makeInfo(ElementType::ORNAMENT, u"Turn", u"A4 E5")); // skip duplicates + expectedList.emplace_back(makeInfo(ElementType::BAR_LINE, u"Single barline")); + expectedList.emplace_back(makeInfo(ElementType::ORNAMENT, u"Turn", u"A4 E5")); + expectedList.emplace_back(makeInfo(ElementType::ORNAMENT, u"Turn", u"A4 E5")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"C5")); + expectedList.emplace_back(makeInfo(ElementType::REST, u"Rest(s)")); // 3rd measure + expectedList.emplace_back(makeInfo(ElementType::BAR_LINE, u"Single barline")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"A4")); expectedList.emplace_back(makeInfo(ElementType::TRILL, u"Trill line")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"C5")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"B4")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"D5")); // 4th measure + expectedList.emplace_back(makeInfo(ElementType::BAR_LINE, u"Single barline")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"A4")); expectedList.emplace_back(makeInfo(ElementType::HAIRPIN, u"Crescendo hairpin")); expectedList.emplace_back(makeInfo(ElementType::GRADUAL_TEMPO_CHANGE, u"accel.")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"B4")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"A4")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"B4")); // 5th measure + expectedList.emplace_back(makeInfo(ElementType::BAR_LINE, u"Single barline")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"A4")); expectedList.emplace_back(makeInfo(ElementType::PLAYTECH_ANNOTATION, u"Pizzicato")); + expectedList.emplace_back(makeInfo(ElementType::BAR_LINE, u"Final barline")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"B4")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"A4")); + expectedList.emplace_back(makeInfo(ElementType::NOTE, u"B4")); ASSERT_EQ(result.size(), 1); const mu::engraving::InstrumentTrackId expectedTrackId { muse::ID(1), u"piano" }; From bb99517d618b64ce449621361c3ab566b330f65f Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Tue, 18 Nov 2025 13:17:11 +0200 Subject: [PATCH 8/9] --score-elements: export ties as flag rather than separate elements --- src/converter/internal/compat/backendapi.cpp | 23 ++++++++++++++++++- src/converter/internal/compat/backendtypes.h | 18 ++++++++++++++- .../internal/compat/scoreelementsscanner.cpp | 21 +++++++++++++---- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/converter/internal/compat/backendapi.cpp b/src/converter/internal/compat/backendapi.cpp index 328212adf6671..40117aceedf0b 100644 --- a/src/converter/internal/compat/backendapi.cpp +++ b/src/converter/internal/compat/backendapi.cpp @@ -677,6 +677,25 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio } }; + auto writeFlags = [](const ElementInfo::Flags& flags, QJsonObject& obj) { + if (flags.testFlag(ElementInfo::FlagType::Tied)) { + obj["tied"] = true; + } + }; + + auto writeNotes = [&writeFlags](const ElementInfo::NoteList& notes, QJsonObject& obj) { + QJsonArray noteArray; + + for (const ElementInfo::Note& note : notes) { + QJsonObject noteObj; + noteObj["name"] = note.name.toQString(); + writeFlags(note.flags, noteObj); + noteArray << noteObj; + } + + obj["notes"] = noteArray; + }; + for (const auto& instrumentPair : elements) { QJsonArray elementArray; @@ -690,7 +709,9 @@ muse::Ret BackendApi::doExportScoreElements(const notation::INotationPtr notatio } if (!element.notes.empty()) { - obj["notes"] = element.notes.toQString(); + writeNotes(element.notes, obj); + } else { + writeFlags(element.flags, obj); } if (!element.duration.empty()) { diff --git a/src/converter/internal/compat/backendtypes.h b/src/converter/internal/compat/backendtypes.h index 7922371446272..59def20739f3e 100644 --- a/src/converter/internal/compat/backendtypes.h +++ b/src/converter/internal/compat/backendtypes.h @@ -22,6 +22,7 @@ #pragma once #include "global/types/string.h" +#include "global/types/flags.h" #include "global/realfn.h" #include "engraving/types/types.h" @@ -31,10 +32,25 @@ struct ElementInfo { mu::engraving::ElementType type = mu::engraving::ElementType::INVALID; muse::String name; - muse::String notes; muse::String duration; muse::String text; + enum class FlagType { + NoFlags = 0, + Tied, + }; + + using Flags = muse::Flags; + Flags flags; + + struct Note { + muse::String name; + muse::Flags flags; + }; + + using NoteList = std::vector; + NoteList notes; + struct Location { mu::engraving::staff_idx_t staffIdx = muse::nidx; mu::engraving::voice_idx_t voiceIdx = muse::nidx; diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index 4e4e21c49affb..faa309fb9eebf 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -54,6 +54,8 @@ static bool itemAccepted(const EngravingItem* item) ElementType::HOOK, ElementType::BEAM, ElementType::NOTEDOT, + ElementType::TIE, + ElementType::TIE_SEGMENT, }; if (muse::contains(NOTE_PARTS, item->type())) { @@ -88,16 +90,23 @@ static muse::String noteName(const Note* note) return tpc2name(note->tpc(), NoteSpellingType::STANDARD, NoteCaseType::AUTO) + String::number(note->octave()); } -static muse::String chordToNotes(const Chord* chord) +static ElementInfo::NoteList chordToNotes(const Chord* chord) { - muse::StringList notes; + ElementInfo::NoteList notes; notes.reserve(chord->notes().size()); for (const Note* note : chord->notes()) { - notes.push_back(noteName(note)); + ElementInfo::Note info; + info.name = noteName(note); + + if (note->tieFor()) { + info.flags.setFlag(ElementInfo::FlagType::Tied); + } + + notes.emplace_back(std::move(info)); } - return notes.join(u" "); + return notes; } static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) @@ -123,6 +132,10 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) info.notes = chordToNotes(chord); } else { info.name = noteName(note); + + if (note->tieFor()) { + info.flags.setFlag(ElementInfo::FlagType::Tied); + } } info.duration = chord->durationUserName(); } else if (isChordArticulation(item)) { From 20f89493643dae5d1b2f8dec74ea92aa935a2628 Mon Sep 17 00:00:00 2001 From: Roman Pudashkin Date: Fri, 21 Nov 2025 18:15:35 +0200 Subject: [PATCH 9/9] --score-elements: simplify dynamics name --- src/converter/internal/compat/scoreelementsscanner.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/converter/internal/compat/scoreelementsscanner.cpp b/src/converter/internal/compat/scoreelementsscanner.cpp index faa309fb9eebf..db56851cffcb3 100644 --- a/src/converter/internal/compat/scoreelementsscanner.cpp +++ b/src/converter/internal/compat/scoreelementsscanner.cpp @@ -22,11 +22,13 @@ #include "scoreelementsscanner.h" +#include "engraving/types/typesconv.h" #include "engraving/dom/score.h" #include "engraving/dom/part.h" #include "engraving/dom/note.h" #include "engraving/dom/harmony.h" #include "engraving/dom/tempotext.h" +#include "engraving/dom/dynamic.h" using namespace mu::converter; using namespace mu::engraving; @@ -180,8 +182,10 @@ static void addElementInfoIfNeed(ScannerData* scannerData, EngravingItem* item) info.name = toHarmony(item)->harmonyName(); } else if (item->isTempoText()) { info.text = toTempoText(item)->tempoInfo(); - } else if (item->isPlayTechAnnotation() || item->isDynamic()) { + } else if (item->isPlayTechAnnotation()) { info.name = item->translatedSubtypeUserName(); + } else if (item->isDynamic()) { + info.name = TConv::toXml(toDynamic(item)->dynamicType()).ascii(); } else if (item->isTextBase()) { info.text = toTextBase(item)->plainText(); } else {