From bb27a90b5c333b7f46c8d44e46fd4de231c1b51c Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Thu, 14 May 2026 17:36:20 +0200 Subject: [PATCH 1/8] Fix running Converter tests on CI * Enable the module in the UTESTS configuration * Fix a type (although the test passes with and without this typo) --- SetupConfigure.cmake | 1 - src/converter/tests/scoreelementsscanner_tests.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/SetupConfigure.cmake b/SetupConfigure.cmake index b933893ce425b..9b96ae4f62a4c 100644 --- a/SetupConfigure.cmake +++ b/SetupConfigure.cmake @@ -298,7 +298,6 @@ if(BUILD_CONFIGURATION STREQUAL "UTEST") set(MUE_BUILD_APPSHELL_MODULE OFF) set(MUE_ENABLE_SPLASHSCREEN OFF) set(MUE_BUILD_BRAILLE_QML OFF) - set(MUE_BUILD_CONVERTER_MODULE OFF) set(MUE_BUILD_INSPECTOR_MODULE OFF) set(MUE_BUILD_INSTRUMENTSSCENE_MODULE OFF) set(MUE_BUILD_PALETTE_MODULE OFF) diff --git a/src/converter/tests/scoreelementsscanner_tests.cpp b/src/converter/tests/scoreelementsscanner_tests.cpp index 5b2c263f6a392..831e48397f53d 100644 --- a/src/converter/tests/scoreelementsscanner_tests.cpp +++ b/src/converter/tests/scoreelementsscanner_tests.cpp @@ -126,7 +126,7 @@ TEST_F(Converter_ScoreElementsTests, ScanElements) const ElementInfo::Note& expectedNote = expectedInfo.notes.at(j); EXPECT_EQ(actualNote.name, expectedNote.name); - EXPECT_EQ(actualInfo.data, expectedInfo.data); + EXPECT_EQ(actualNote.data, expectedNote.data); } } From 2898fa6260f9aa4e95b93c47a75458abf124a1e1 Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Thu, 18 Jun 2026 11:44:50 +0200 Subject: [PATCH 2/8] Braille: follow default MBC rule to double articulations Fix #27829 --- src/braille/ibrailleconfiguration.h | 4 + src/braille/internal/braille.cpp | 108 +++++++++++++++++- src/braille/internal/braille.h | 25 ++++ src/braille/internal/brailleconfiguration.cpp | 20 ++++ src/braille/internal/brailleconfiguration.h | 5 + .../Preferences/BraillePreferencesPage.qml | 5 + .../Preferences/braillepreferencesmodel.cpp | 19 +++ .../Preferences/braillepreferencesmodel.h | 4 + .../internal/BrailleAdvancedSection.qml | 18 +++ 9 files changed, 206 insertions(+), 2 deletions(-) diff --git a/src/braille/ibrailleconfiguration.h b/src/braille/ibrailleconfiguration.h index ab1d10ab2ca05..449b9026ce767 100644 --- a/src/braille/ibrailleconfiguration.h +++ b/src/braille/ibrailleconfiguration.h @@ -49,6 +49,10 @@ class IBrailleConfiguration : MODULE_GLOBAL_INTERFACE virtual QString brailleTable() const = 0; virtual void setBrailleTable(const QString& table) = 0; virtual QStringList brailleTableList() const = 0; + + virtual muse::async::Notification articulationDoublingChanged() const = 0; + virtual bool articulationDoubling() const = 0; + virtual void setArticulationDoubling(const bool enabled) = 0; }; } diff --git a/src/braille/internal/braille.cpp b/src/braille/internal/braille.cpp index 76a76aca4c6cc..1816b7dc74e34 100644 --- a/src/braille/internal/braille.cpp +++ b/src/braille/internal/braille.cpp @@ -24,6 +24,8 @@ #include +#include + #include "containers.h" #include "engraving/dom/accidental.h" @@ -949,6 +951,84 @@ void Braille::resetOctaves() } } +// 22. Doubling of Signs. Music Braille Code 2015. +// When the same articulation mark appears on more than three consecutive notes +// (ignoring any rests between them), it is written twice before the first note of the group +// and once after the last note, and omitted on the notes in between. This precomputes, per +// articulation instance, whether it should be doubled, omitted, closed (suffix on the last +// note) or rendered singly (the default). +void Braille::computeArticulationDoubling() +{ + m_context.articulationDoublingComputed = true; + m_context.articulationDoubling.clear(); + + // When disabled, the empty map means every articulation is rendered singly (legacy behaviour). + // The configuration may be unavailable (e.g. in unit tests that don't register the braille + // module); in that case fall back to the default of doubling enabled. + if (brailleConfiguration() && !brailleConfiguration()->articulationDoubling()) { + return; + } + + auto finalizeRun = [this](const std::vector& run) { + if (run.size() < ARTICULATION_DOUBLING_MIN_GROUP) { + return; + } + // Double (prefix) on the first note, omit on the middle notes, and a single closing sign + // as a suffix after the last note. run.size() >= 4 here, so front and back differ. + m_context.articulationDoubling[run.front()] = ArticulationDoubling::Double; + for (size_t i = 1; i + 1 < run.size(); ++i) { + m_context.articulationDoubling[run[i]] = ArticulationDoubling::Omit; + } + m_context.articulationDoubling[run.back()] = ArticulationDoubling::CloseSuffix; + }; + + const size_t ntracks = m_score->staves().size() * VOICES; + for (track_idx_t track = 0; track < ntracks; ++track) { + // Runs of consecutive chords sharing the same articulation SymId, grouped per sign. + std::map > openRuns; + + for (Segment* seg = m_score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { + EngravingItem* el = seg->element(track); + if (!el) { + continue; + } + if (el->isRest()) { + // Rests never break a run. + continue; + } + if (!el->isChord()) { + continue; + } + + Chord* chord = toChord(el); + + std::set present; + for (Articulation* artic : chord->articulations()) { + present.insert(artic->symId()); + } + + // End any open run whose articulation is not present on this chord. + for (auto it = openRuns.begin(); it != openRuns.end();) { + if (present.find(it->first) == present.end()) { + finalizeRun(it->second); + it = openRuns.erase(it); + } else { + ++it; + } + } + + // Extend or start a run for each articulation on this chord. + for (Articulation* artic : chord->articulations()) { + openRuns[artic->symId()].push_back(artic); + } + } + + for (const auto& pair : openRuns) { + finalizeRun(pair.second); + } + } +} + void Braille::credits(QIODevice& device) { QTextStream out(&device); @@ -1817,9 +1897,32 @@ QString Braille::brailleChord(Chord* chord) } } - QString articulationsBraille = QString(); + if (!m_context.articulationDoublingComputed) { + computeArticulationDoubling(); + } + QString articulationsBraille = QString(); // prefix, before the note + QString articulationsBrailleAfter = QString(); // suffix, after the note (doubling closing sign) for (Articulation* artic : chord->articulations()) { - articulationsBraille += brailleArticulation(artic); + auto it = m_context.articulationDoubling.find(artic); + ArticulationDoubling state = (it != m_context.articulationDoubling.end()) ? it->second : ArticulationDoubling::Single; + const QString code = brailleArticulation(artic); + switch (state) { + case ArticulationDoubling::Omit: + // Middle of a doubled group: the sign is not written. + break; + case ArticulationDoubling::Double: + // First note of a doubled group: write the sign twice, before the note. + articulationsBraille += code + code; + break; + case ArticulationDoubling::CloseSuffix: + // Last note of a doubled group: write the closing sign once, after the note. + articulationsBrailleAfter += code; + break; + case ArticulationDoubling::Single: + default: + articulationsBraille += code; + break; + } } std::vector chordSlurs = slurs(chord); @@ -1843,6 +1946,7 @@ QString Braille::brailleChord(Chord* chord) result += tupletBraille; result += rootNoteBraille; result += intervals; + result += articulationsBrailleAfter; result += tremoloBraille; result += chordTieBraille; result += slurBrailleAfter; diff --git a/src/braille/internal/braille.h b/src/braille/internal/braille.h index 8ae1e726f18bf..c1fce236ffe45 100644 --- a/src/braille/internal/braille.h +++ b/src/braille/internal/braille.h @@ -23,8 +23,13 @@ #include +#include + #include "engraving/types/types.h" +#include "modularity/ioc.h" +#include "../ibrailleconfiguration.h" + namespace mu::engraving { class Arpeggio; class Articulation; @@ -148,10 +153,23 @@ class TextToUEBBraille QMap textToBrailleASCII; }; +// Rendering decision for an articulation under the Music Braille Code 2015 doubling rule. +// Absence from the map means Single (render the sign once, the default behaviour). +enum class ArticulationDoubling { + Single = 0, // default: write the sign once, as a prefix before the note + Double = 1, // first note of a group: write the sign twice, as a prefix + Omit = 2, // middle notes of a group: do not write the sign + CloseSuffix = 3, // last note of a group: write the sign once, as a suffix after the note +}; + struct BrailleContext { std::vector previousNote; std::vector currentClefType; std::vector currentKey; + + // Per-articulation-instance decision for the doubling rule, computed once per Braille run. + std::map articulationDoubling; + bool articulationDoublingComputed = false; }; // Braille export is implemented according to Music Braille Code 2015 @@ -169,12 +187,19 @@ class Braille private: static constexpr int MAX_CHARS_PER_LINE = 40; + // Music Braille Code 2015: the same sign on this many notes in a row is doubled. + static constexpr size_t ARTICULATION_DOUBLING_MIN_GROUP = 4; + + muse::GlobalInject brailleConfiguration; + Score* m_score = nullptr; BrailleContext m_context; void resetOctave(size_t stave); void resetOctaves(); + void computeArticulationDoubling(); + void credits(QIODevice& device); void instruments(QIODevice& device); diff --git a/src/braille/internal/brailleconfiguration.cpp b/src/braille/internal/brailleconfiguration.cpp index e57564c82db3a..9f4842239b9bb 100644 --- a/src/braille/internal/brailleconfiguration.cpp +++ b/src/braille/internal/brailleconfiguration.cpp @@ -33,6 +33,7 @@ static const std::string module_name("braille"); static const Settings::Key BRAILLE_STATUS(module_name, "score/braille/status"); static const Settings::Key BRAILLE_TABLE(module_name, "score/braille/table"); static const Settings::Key BRAILLE_INTERVAL_DIRECTION(module_name, "score/braille/intervalDirection"); +static const Settings::Key BRAILLE_ARTICULATION_DOUBLING(module_name, "score/braille/articulationDoubling"); void BrailleConfiguration::init() { @@ -48,6 +49,10 @@ void BrailleConfiguration::init() settings()->valueChanged(BRAILLE_INTERVAL_DIRECTION).onReceive(this, [this](const Val&) { m_intervalDirectionChanged.notify(); }); + settings()->setDefaultValue(BRAILLE_ARTICULATION_DOUBLING, Val(true)); + settings()->valueChanged(BRAILLE_ARTICULATION_DOUBLING).onReceive(this, [this](const Val&) { + m_articulationDoublingChanged.notify(); + }); } muse::async::Notification BrailleConfiguration::braillePanelEnabledChanged() const @@ -95,6 +100,21 @@ void BrailleConfiguration::setBrailleTable(const QString& table) settings()->setSharedValue(BRAILLE_TABLE, Val(table)); } +muse::async::Notification BrailleConfiguration::articulationDoublingChanged() const +{ + return m_articulationDoublingChanged; +} + +bool BrailleConfiguration::articulationDoubling() const +{ + return settings()->value(BRAILLE_ARTICULATION_DOUBLING).toBool(); +} + +void BrailleConfiguration::setArticulationDoubling(const bool enabled) +{ + settings()->setSharedValue(BRAILLE_ARTICULATION_DOUBLING, Val(enabled)); +} + QStringList BrailleConfiguration::brailleTableList() const { return { diff --git a/src/braille/internal/brailleconfiguration.h b/src/braille/internal/brailleconfiguration.h index 03339f0c04a7d..37858ba056da1 100644 --- a/src/braille/internal/brailleconfiguration.h +++ b/src/braille/internal/brailleconfiguration.h @@ -46,10 +46,15 @@ class BrailleConfiguration : public mu::braille::IBrailleConfiguration, public m void setBrailleTable(const QString& table) override; QStringList brailleTableList() const override; + muse::async::Notification articulationDoublingChanged() const override; + bool articulationDoubling() const override; + void setArticulationDoubling(const bool enabled) override; + private: muse::async::Notification m_braillePanelEnabledChanged; muse::async::Notification m_brailleTableChanged; muse::async::Notification m_intervalDirectionChanged; + muse::async::Notification m_articulationDoublingChanged; }; } diff --git a/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml b/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml index 3ab66d42d0382..2000b5f16f03b 100644 --- a/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml +++ b/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml @@ -61,6 +61,7 @@ PreferencesPage { directions: preferencesModel.intervalDirections() brailleTable: preferencesModel.brailleTable intervalDirection: preferencesModel.intervalDirection + articulationDoubling: preferencesModel.articulationDoubling navigation.section: root.navigationSection navigation.order: root.navigationOrderStart + 2 @@ -75,6 +76,10 @@ PreferencesPage { preferencesModel.intervalDirection = direction; } + onArticulationDoublingChangeRequested: function(enabled) { + preferencesModel.articulationDoubling = enabled; + } + onFocusChanged: { if (activeFocus) { root.ensureContentVisibleRequested(Qt.rect(x, y, width, height)); diff --git a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp index db4f8be542db9..34f7db79e25aa 100644 --- a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp +++ b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp @@ -47,6 +47,10 @@ void BraillePreferencesModel::load() brailleConfiguration()->brailleTableChanged().onNotify(this, [this]() { emit brailleTableChanged(brailleTable()); }); + + brailleConfiguration()->articulationDoublingChanged().onNotify(this, [this]() { + emit articulationDoublingChanged(articulationDoubling()); + }); } bool BraillePreferencesModel::braillePanelEnabled() const @@ -64,6 +68,11 @@ int BraillePreferencesModel::intervalDirection() const return static_cast(brailleConfiguration()->intervalDirection()); } +bool BraillePreferencesModel::articulationDoubling() const +{ + return brailleConfiguration()->articulationDoubling(); +} + QStringList BraillePreferencesModel::brailleTables() const { return brailleConfiguration()->brailleTableList(); @@ -119,3 +128,13 @@ void BraillePreferencesModel::setIntervalDirection(int direction) brailleConfiguration()->setIntervalDirection(static_cast(direction)); emit intervalDirectionChanged(direction); } + +void BraillePreferencesModel::setArticulationDoubling(bool value) +{ + if (value == articulationDoubling()) { + return; + } + + brailleConfiguration()->setArticulationDoubling(value); + emit articulationDoublingChanged(value); +} diff --git a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h index 5b12e9ba53f26..1c938556318c2 100644 --- a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h +++ b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h @@ -39,6 +39,7 @@ class BraillePreferencesModel : public QObject, public muse::Contextable, public Q_PROPERTY(bool braillePanelEnabled READ braillePanelEnabled WRITE setBraillePanelEnabled NOTIFY braillePanelEnabledChanged) Q_PROPERTY(QString brailleTable READ brailleTable WRITE setBrailleTable NOTIFY brailleTableChanged) Q_PROPERTY(int intervalDirection READ intervalDirection WRITE setIntervalDirection NOTIFY intervalDirectionChanged) + Q_PROPERTY(bool articulationDoubling READ articulationDoubling WRITE setArticulationDoubling NOTIFY articulationDoublingChanged) muse::GlobalInject brailleConfiguration; @@ -50,6 +51,7 @@ class BraillePreferencesModel : public QObject, public muse::Contextable, public bool braillePanelEnabled() const; QString brailleTable() const; int intervalDirection() const; + bool articulationDoubling() const; Q_INVOKABLE QStringList brailleTables() const; Q_INVOKABLE QVariantList intervalDirections() const; @@ -58,10 +60,12 @@ public slots: void setBraillePanelEnabled(bool value); void setBrailleTable(QString table); void setIntervalDirection(int direction); + void setArticulationDoubling(bool value); signals: void braillePanelEnabledChanged(bool value); void brailleTableChanged(QString value); void intervalDirectionChanged(int value); + void articulationDoublingChanged(bool value); }; } diff --git a/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml b/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml index d17f8c6dbfdf9..4c81febe660c3 100644 --- a/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml +++ b/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml @@ -34,8 +34,11 @@ BaseSection { property var directions: null property int intervalDirection: -1 + property alias articulationDoubling: articulationDoublingBox.checked + signal brailleTableChangeRequested(string table) signal intervalDirectionChangeRequested(int direction) + signal articulationDoublingChangeRequested(bool enabled) ComboBoxWithTitle { title: qsTrc("preferences", "Braille table for lyrics") @@ -68,4 +71,19 @@ BaseSection { root.intervalDirectionChangeRequested(newValue); } } + + CheckBox { + id: articulationDoublingBox + width: parent.width + + text: qsTrc("preferences", "Double repeated articulation marks (Braille rule)") + + navigation.name: "ArticulationDoublingBox" + navigation.panel: root.navigation + navigation.row: 3 + + onClicked: { + root.articulationDoublingChangeRequested(!checked); + } + } } From abcbcee2e90d3fd7782f204f1a99b51a064beccf Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Thu, 18 Jun 2026 12:13:01 +0200 Subject: [PATCH 3/8] Auto update Live panel on settings change --- src/braille/internal/notationbraille.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/braille/internal/notationbraille.cpp b/src/braille/internal/notationbraille.cpp index d1e8b84d1699c..13e2503b43f64 100644 --- a/src/braille/internal/notationbraille.cpp +++ b/src/braille/internal/notationbraille.cpp @@ -74,6 +74,12 @@ void NotationBraille::init() setIntervalDirection(direction); }); + // The Braille generator reads articulationDoubling directly from configuration, so no value + // needs threading here; we just force a re-render so the panel reflects the change immediately. + brailleConfiguration()->articulationDoublingChanged().onNotify(this, [this]() { + doBraille(true); + }); + globalContext()->currentNotationChanged().onNotify(this, [this]() { if (notation()) { notation()->interaction()->selectionChanged().onNotify(this, [this]() { From 1a6a1ca7f784177921fe0c322178f2e0a1679708 Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Thu, 18 Jun 2026 12:41:11 +0200 Subject: [PATCH 4/8] Add unittest --- src/braille/tests/braille_tests.cpp | 1 + ...testArticulationDoubling_1.12_MBC2015.mscx | 1019 +++++++++++++++++ ...tArticulationDoubling_1.12_MBC2015_ref.brf | 15 + 3 files changed, 1035 insertions(+) create mode 100644 src/braille/tests/data/testArticulationDoubling_1.12_MBC2015.mscx create mode 100644 src/braille/tests/data/testArticulationDoubling_1.12_MBC2015_ref.brf diff --git a/src/braille/tests/braille_tests.cpp b/src/braille/tests/braille_tests.cpp index f7b257f44c258..67d993a5612cc 100644 --- a/src/braille/tests/braille_tests.cpp +++ b/src/braille/tests/braille_tests.cpp @@ -235,6 +235,7 @@ TEST_F(Braille_Tests, breath) { } TEST_F(Braille_Tests, articulations) { brailleSaveTest("testArticulations_Example_22.1_MBC2015"); + brailleSaveTest("testArticulationDoubling_1.12_MBC2015"); } TEST_F(Braille_Tests, hairpins) { // removed the 4th measure from the example as MuseScore does not have a representations for mordents with accidentals diff --git a/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015.mscx b/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015.mscx new file mode 100644 index 0000000000000..2905c811e78a7 --- /dev/null +++ b/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015.mscx @@ -0,0 +1,1019 @@ + + + 4.5.1 + 603eca8 + + sYRhNibursL_6RNJIIDBQeD + 480 + 1 + 1 + 1 + 0 + 1 + + + Composer / arranger + + 2025-04-16 + + + + musicxml + Microsoft Windows + + + + + XML Test Score + + + + stdNormal + + 3 + + 1 + + + + stdNormal + + F + 3 + + Piano + none + + Piano + Pno. + Piano + 21 + 108 + 21 + 108 + keyboard.piano + F + + + Fluid + + + + + + 10 + CLJdpMlH9AM_xCQBTmRRjPM + + mF7lVzxA5OG_vJspfeE/LPN + + Doubling in Braille + + + 43zV5xGmqJF_bmAv1KHAEVC + + Composer / arranger + + + + GwVdf4fJKlE_3+L7T9mysBO + + + G + G + 1 + VpHSt1m/aHM_PWb/DjgILJO + + + iKjTx6kp3iN_sx6rpR6MlfB + 3 + + + H4wEIUBkHfO_6r+xq3Eh74I + 4 + 4 + + + SL8VdIVKGTE_cTZHQPI/69P + quarter + + articStaccatoBelow + gsU93VMS60O_avVbtwK8GbM + + + vFWSrPpwEwJ_ap62+yXRX2B + 69 + 17 + + + + IXJQpxMh1CD_jR3yr/h0PgH + quarter + + articStaccatoBelow + Z6TrtFN+PJK_0rGqHHj1M1B + + + QTApY05v5u_n+ParzcAAOE + 69 + 17 + + + + BaVTASHilaL_+bSHOUhAVjJ + quarter + + articStaccatoBelow + K8LGS6RsB4M_LdeA8DgyHrO + + + FKq83rmKvfH_7YVE3SWCwIK + 69 + 17 + + + + Z8V8KLZ+RmE_/C1XZGEmzKP + quarter + + articStaccatoBelow + 79181kNsKFC_Zu1+vmFVYWH + + + sNBSjNQsDSI_M2hveaVaDtG + 69 + 17 + + + + + + fpdlytC+xAI_c1GkJPINEDB + + + NuYtKzSK6jL_xmbUUrpH/DB + quarter + + articMarcatoAbove + KyzU7mnmHhN_V7mI4En0SpI + + + ULKUyRo3gAH_Fvrx58shfVD + 71 + 19 + + + + u87589Nz1iO_NdTqe9BlFL + quarter + + articMarcatoAbove + H8VFoKgTL8P_13UslxiiB/F + + + /7KxtIni0xO_xflzJBiKuyM + 71 + 19 + + + + EnNmuCgrFiE_MQKOCJqeH9N + quarter + + articMarcatoAbove + w7A14eMg7gM_txZJC6qtWoM + + + BIc6qUlPOyO_sx7BDiWTUrD + 71 + 19 + + + + S04bcLVS5wM_TFfojkhNnMF + quarter + + articMarcatoAbove + aurSKB4wLDM_IdA4Ycq3V+I + + + rA3ppljtGVI_vQgvspG0o8D + 71 + 19 + + + + + + FZH0ywHJaZ_9pA+1Ag9GDP + + D+tSLmNeKqN_V6vdp1Hui+N + line + + + + W8bmQzGJNIL_dpLmDBSTDeL + quarter + + articTenutoAbove + y3EhyKQjnjL_AEmiP5r5WTN + + + oTqhirhjdrK_ga4jBXuPJOE + 73 + 21 + + + + R8dbL8FVK1O_QsZlQ6on8WD + quarter + + articTenutoAbove + +CEJ2gcJ9bG_63bUINUZh9G + + + 1oNMFgzxeMK_2NfDAO3Ix1J + 73 + 21 + + + + hDU2Bwip1vE_a5a6MPxZPGO + quarter + + articTenutoAbove + UIYBBxXjXKC_EYxF0q2UsFL + + + Os18HEMzsmN_KEw0Dr6ZhkK + 73 + 21 + + + + vmKFDQBEdeJ_vlo7nf+tN/O + quarter + + articTenutoAbove + x8idXOJSYOF_hwAegnIxXGB + + + o/LCERN0fBI_4QcWmmsta6O + 73 + 21 + + + + + + C6Bl5AZAjw_9dQvX9xqd1F + 2 + + + 3ZQZOu4qo0M_HWgcXHX7hdJ + quarter + + articAccentAbove + IcmF9pTblF_h/TpNKB46cC + + + W0DcV1YvcSP_1NiEOurfS9I + 74 + 16 + + + + 0tYTfkkVEwC_acDUPLh9twI + quarter + + articAccentAbove + ophqvHJxOfN_km8/EvNmxdE + + + MoTOXxF07zP_JUeejMuNKbC + 74 + 16 + + + + IUEte789MBP_Zp0kW0E0y5 + quarter + + articAccentAbove + dEUcIFRqoUD_P9sDIzbSu6F + + + zFnjJpzUIEJ_1iq116aIOrF + 74 + 16 + + + + KNNWp5+Bk9L_ZY4+LvHWFYK + quarter + + articAccentAbove + GoIpTwe+lhH_weF4XmVOVTC + + + XM0Ks4sFUaG_uIr7jWZCmyP + 74 + 16 + + + + + + hqCtBlKplZD_0Ck3bloz3B + + + mZysz0r37QM_5jSRE07NzyO + whole + + ZB8iKchBUkO_SYUGS3WOYeL + 69 + 17 + + + + + + c4/h0CqvgxK_txx7MU+Ut4O + + + JimQBh7WciO_JXRrimdz8xD + quarter + + + yR7u5wBuidD_g22dB/5IBHL + + + + 1 + 3/4 + + + + + ZLX4rCMRNwO_UYY2TU5Da8G + 69 + 17 + + + + vTJ1W0b07gM_E7weoPpC9+G + quarter + + VjjVJaUD5fH_uQWyrf9dx2K + 71 + 19 + + + + ivyQ7yUl+bP_BLmPtyCrzOH + quarter + + nrg2IJK80eG_ZLz8eDq4TPD + 73 + 21 + + + + AjYbzf3XFdB_Ap417GtPFQO + quarter + + pXeCx8+ea1B_7LkSHLty3y + 74 + 16 + + + + + + JR/KbGLQetC_TVO0OMgpDLH + + + /HTT+/Xz8VF_oJgFXIyRnmI + quarter + + WXaZCgUnELC_ADjWFrHvkkF + 76 + 18 + + + + 1iFufYAVggF_FFoSmRy6W1L + quarter + + oY/q4+6rdFP_wDDp3wzZrFI + 78 + 20 + + + + C9iPTngqMbL_628OwGnfYnP + quarter + + DxKVFzgQiYN_fwHVEZRvFYI + 80 + 22 + + + + p/hs9a0TRuO_391xIQ8Q22C + quarter + + + + -1 + -3/4 + + + + + AF/FYJYBNYE_SlooekGA4bP + 81 + 17 + + + + + + QtjZEQK2UnG_waZIjsFQafP + + + JVsNw3Rbd2N_PnVnTkMYtpK + measure + 4/4 + + + + + w5Q2D/EZh1N_tVBtyQk7QPL + + + gJsiLZjVcd_ciW1thd14fC + measure + 4/4 + + + + + P286kjDDKC_QVtQGI7mYhL + + + +r3r0wPEkhN_iy3Qz6Uy/II + measure + 4/4 + + + + + yegZwvCp3uF_x/Ffs4UkjrM + + + SmkzpGElO3B_uPeRS8RhzEB + measure + 4/4 + + + + + KjD3a02hn4L_RmwgiWa70WB + + + juL56MRHYPC_K7bt1I8EwnI + measure + 4/4 + + + + + l+Sv2SilnwE_zNlADVLf0VK + + + R3+1qILv31O_wLGa8XeEDXN + measure + 4/4 + + + + + 59g7oigUUEL_aa/9N3UVCKH + + J5Tn5ucXW0C_Y11/97oI5AB + line + + + + 2Kt+N7jc2iC_IRuUClQyjkB + measure + 4/4 + + + + + DTqu0QuOQ0B_dWkh8t0p1MC + + + EMTIInFQHcD_SCrr4EXw2Y + measure + 4/4 + + + + + J6fuxV6BRrB_INlDpOq09vG + + + bL46BRVKMt_5pP3phiO4vP + measure + 4/4 + + + + + Z9VMGvzt+cG_q9X1pISvdPJ + + + b67EbHK54aE_p2OxBxgMJrK + measure + 4/4 + + + + + +2WioorS2RF_FLST1THX1mJ + + + /GnRXu4AMrF_EzXO/4qBTrH + measure + 4/4 + + + + + tOSjEzjGKwD_Z+YIM4DV9ON + + + big1GZwCplC_WfqXS1qxxdK + measure + 4/4 + + + + + PIEXpARUIEC_hSCyAxUSS4D + + + LU2S/XsKnEL_bPgFE0uoTe + measure + 4/4 + + + + + 8pXTTGdWjqP_bgDBdmSysJC + + + 2kAunJZYSBJ_2EhYQQVKzCJ + measure + 4/4 + + + + + BWuDaT+z97H_92kfK/GqnDK + + + uHe+VyYj5AG_EyJKkJWllQC + measure + 4/4 + + + + + Elw4p2AxmPI_wjh6VSjG0BL + + + nrH17zvM1cL_pK7YrycQ36J + measure + 4/4 + + + + + I6Z9zVttj9I_dpHiz68tflI + + + cJG99YMAtFL_C0UBQVdINKL + measure + 4/4 + + + + + W6k41IMfVYH_2YimtYTwSjC + + lKFdy3s9r2C_UmZYSlWqVAN + line + + + + 8kIfgmdzaWD_lCmQaSgZCi + measure + 4/4 + + + + + zA/846MPLZF_VY0a9y+dq0M + + + i3bBvbw55RE_OqQob81aQ2P + measure + 4/4 + + + + + uRAWGHQypOM_pVIwRctiYTI + + + AcOkgehvgxB_65ZF0OyFWDK + measure + 4/4 + + + + + Oa1IPAYOrOB_iSfGlkOHHUE + + + j5wGNpioNsO_vVbdiLDhSvO + measure + 4/4 + + + + + WvKSoWlwfQD_PKdO26tDc9H + + + wni57TCWraK_uk6rdhM5C0P + measure + 4/4 + + + + + BinA4EvCIbO_wKQN3pIzwg + + + gg+VNGeQiZ_BLnXPP7scLD + measure + 4/4 + + + + + uXXlw4/tLJJ_jU+ySEAYIOF + + + kTE5gf36i3K_xcTswEtGgVF + measure + 4/4 + + + + + A2I6m1SVVfJ_qEosw4DzQUC + + + 04vuHXKaa7O_NTeRCEIQGrG + measure + 4/4 + + + end + ngLNTngWhuC_ldjrHZivK8H + + + + + + + + + F + F + 1 + GBJKpA0Hl3B_8DepMELS/aK + + + tUdO1btIJAF_pYlEsrAcWUC + 3 + + + QMW3AnECM1J_giyq0sOWz9H + 4 + 4 + + + 9FK0cseAR5F_xWucJzA/0oI + measure + 4/4 + + + + + + + YQsyOmKyTlI_M8rKhXe8inF + measure + 4/4 + + + + + + + z4sCWckGi4_1omrfmX2hxG + measure + 4/4 + + + + + + + QAQKrMmeLwJ_c4kEx7gi+lM + measure + 4/4 + + + + + + + 8qXjw+OijiL_vGprvNIuE9B + measure + 4/4 + + + + + + + PHI9c4jDLVD_elVaxWXJMgG + measure + 4/4 + + + + + + + qfprmk5PFJB_NFcM4eddnxI + measure + 4/4 + + + + + + + XTSxMSs2Z1L_zFzZnxRSOeG + measure + 4/4 + + + + + + + lBCRflCF9aD_IVMoR1+N4pC + measure + 4/4 + + + + + + + 22F7tjyTC0L_An6ceXqs3TG + measure + 4/4 + + + + + + + XS8OxTpcVjE_RrX5g6+CnfH + measure + 4/4 + + + + + + + disBcGHZEpI_nPUxunmxPtN + measure + 4/4 + + + + + + + 7onRi0c6F9O_x7sKuHs3lUH + measure + 4/4 + + + + + + + 5I1bAJj9ckE_TO2/6LfCINM + measure + 4/4 + + + + + + + V0jFU1jOzzC_fu5NfeXyJHG + measure + 4/4 + + + + + + + XJaD+qgcvjB_UYtW7Vwx3sN + measure + 4/4 + + + + + + + Uixci9JIt7F_BLCfv+Fq6aJ + measure + 4/4 + + + + + + + 5bZDY4TY6/O_+nX60/02aNJ + measure + 4/4 + + + + + + + WWhN2DsMOaH_U2z0A0XgA/C + measure + 4/4 + + + + + + + RzDg0agjggK_j/xuViEr7cL + measure + 4/4 + + + + + + + +W1U+S0+znL_eBZMM+uMs9H + measure + 4/4 + + + + + + + UEfhHmMFRbM_GqYEmrSbOjP + measure + 4/4 + + + + + + + brdP0v/kIdH_TBcX0yxz5cH + measure + 4/4 + + + + + + + TQObxRd676G_w/H3x366ovI + measure + 4/4 + + + + + + + +wKhLrfy6UG_yZK1HdUPvK + measure + 4/4 + + + + + + + wfOZaE+y3WN_/r62zr1Ut6I + measure + 4/4 + + + + + + + xnAOPH/aPHG_8WGUNl4OzXH + measure + 4/4 + + + + + + + Hf+5y8erUoG_4WhglfEmLmJ + measure + 4/4 + + + + + + + ZVB2cawbFSP_m5hjJsL5utC + measure + 4/4 + + + + + + + Xvz/QCckEJH_2AtOC9B4VDL + measure + 4/4 + + + + + + + VLHDbRNqI7F_kwQkfntsiJK + measure + 4/4 + + + + + + + /jNpMhXV0SF_4333ksDvdHG + measure + 4/4 + + + end + xfsGKIN+s0M_eO/1k0aRazL + + + + + + + GwVdf4fJKlE_3+L7T9mysBO + GwVdf4fJKlE_3+L7T9mysBO + + + + diff --git a/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015_ref.brf b/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015_ref.brf new file mode 100644 index 0000000000000..092349bd96ce5 --- /dev/null +++ b/src/braille/tests/data/testArticulationDoubling_1.12_MBC2015_ref.brf @@ -0,0 +1,15 @@ +,doubling in ,braille +,composer / arranger +composer ,composer / arranger + +#a ,piano +#b ,piano + +a >/l%%%#d4 88"[[[[8 ;8;8WWWW;8 + >#l%%%#d4 m m +c _8_8.????_8 .8.8::::.8<2 "! ;b[W?: + m m<2 m m +g .$]\[^2 m m m m m m m m m m m m m m m + m m m m m m m m m m m m m m m m +bc m m m m m m m m m m Date: Fri, 19 Jun 2026 11:03:03 +0200 Subject: [PATCH 5/8] Adjust group doubling for stem-direction flip --- src/braille/internal/braille.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/braille/internal/braille.cpp b/src/braille/internal/braille.cpp index 1816b7dc74e34..3cbb8729ab407 100644 --- a/src/braille/internal/braille.cpp +++ b/src/braille/internal/braille.cpp @@ -985,7 +985,7 @@ void Braille::computeArticulationDoubling() const size_t ntracks = m_score->staves().size() * VOICES; for (track_idx_t track = 0; track < ntracks; ++track) { // Runs of consecutive chords sharing the same articulation SymId, grouped per sign. - std::map > openRuns; + std::map > openRuns; for (Segment* seg = m_score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { EngravingItem* el = seg->element(track); @@ -1002,9 +1002,13 @@ void Braille::computeArticulationDoubling() Chord* chord = toChord(el); - std::set present; + std::set present; for (Articulation* artic : chord->articulations()) { - present.insert(artic->symId()); + const QString key = brailleArticulation(artic); + if (key.isEmpty()) { + continue; + } + present.insert(key); } // End any open run whose articulation is not present on this chord. @@ -1019,7 +1023,11 @@ void Braille::computeArticulationDoubling() // Extend or start a run for each articulation on this chord. for (Articulation* artic : chord->articulations()) { - openRuns[artic->symId()].push_back(artic); + const QString key = brailleArticulation(artic); + if (key.isEmpty()) { + continue; + } + openRuns[key].push_back(artic); } } From 286342867a41d9ad00c0173f344db6aed7f09c15 Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Fri, 26 Jun 2026 17:32:23 +0200 Subject: [PATCH 6/8] add intervals to doubling as well --- src/braille/internal/braille.cpp | 174 ++++++++++++++++++++++--------- src/braille/internal/braille.h | 22 ++-- 2 files changed, 138 insertions(+), 58 deletions(-) diff --git a/src/braille/internal/braille.cpp b/src/braille/internal/braille.cpp index 3cbb8729ab407..8407c298d3894 100644 --- a/src/braille/internal/braille.cpp +++ b/src/braille/internal/braille.cpp @@ -952,40 +952,53 @@ void Braille::resetOctaves() } // 22. Doubling of Signs. Music Braille Code 2015. -// When the same articulation mark appears on more than three consecutive notes +// When the same articulation or interval sign appears on more than three consecutive notes // (ignoring any rests between them), it is written twice before the first note of the group // and once after the last note, and omitted on the notes in between. This precomputes, per -// articulation instance, whether it should be doubled, omitted, closed (suffix on the last -// note) or rendered singly (the default). -void Braille::computeArticulationDoubling() +// sign instance, whether it should be doubled, omitted, closed, or rendered singly +// (the default). +void Braille::computeSignDoubling() { - m_context.articulationDoublingComputed = true; + m_context.signDoublingComputed = true; m_context.articulationDoubling.clear(); + m_context.intervalDoubling.clear(); - // When disabled, the empty map means every articulation is rendered singly (legacy behaviour). + // When disabled, the empty maps mean every sign is rendered singly (legacy behaviour). // The configuration may be unavailable (e.g. in unit tests that don't register the braille // module); in that case fall back to the default of doubling enabled. if (brailleConfiguration() && !brailleConfiguration()->articulationDoubling()) { return; } - auto finalizeRun = [this](const std::vector& run) { - if (run.size() < ARTICULATION_DOUBLING_MIN_GROUP) { + auto finalizeArticulationRun = [this](const std::vector& run) { + if (run.size() < SIGN_DOUBLING_MIN_GROUP) { return; } // Double (prefix) on the first note, omit on the middle notes, and a single closing sign // as a suffix after the last note. run.size() >= 4 here, so front and back differ. - m_context.articulationDoubling[run.front()] = ArticulationDoubling::Double; + m_context.articulationDoubling[run.front()] = SignDoubling::Double; for (size_t i = 1; i + 1 < run.size(); ++i) { - m_context.articulationDoubling[run[i]] = ArticulationDoubling::Omit; + m_context.articulationDoubling[run[i]] = SignDoubling::Omit; } - m_context.articulationDoubling[run.back()] = ArticulationDoubling::CloseSuffix; + m_context.articulationDoubling[run.back()] = SignDoubling::CloseSuffix; + }; + + auto finalizeIntervalRun = [this](const std::vector& run) { + if (run.size() < SIGN_DOUBLING_MIN_GROUP) { + return; + } + m_context.intervalDoubling[run.front()] = SignDoubling::Double; + for (size_t i = 1; i + 1 < run.size(); ++i) { + m_context.intervalDoubling[run[i]] = SignDoubling::Omit; + } + m_context.intervalDoubling[run.back()] = SignDoubling::CloseSuffix; }; const size_t ntracks = m_score->staves().size() * VOICES; for (track_idx_t track = 0; track < ntracks; ++track) { - // Runs of consecutive chords sharing the same articulation SymId, grouped per sign. - std::map > openRuns; + // Runs of consecutive chords sharing the same Braille sign. + std::map > openArticulationRuns; + std::map > openIntervalRuns; for (Segment* seg = m_score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { EngravingItem* el = seg->element(track); @@ -1002,20 +1015,20 @@ void Braille::computeArticulationDoubling() Chord* chord = toChord(el); - std::set present; + std::set presentArticulations; for (Articulation* artic : chord->articulations()) { const QString key = brailleArticulation(artic); if (key.isEmpty()) { continue; } - present.insert(key); + presentArticulations.insert(key); } // End any open run whose articulation is not present on this chord. - for (auto it = openRuns.begin(); it != openRuns.end();) { - if (present.find(it->first) == present.end()) { - finalizeRun(it->second); - it = openRuns.erase(it); + for (auto it = openArticulationRuns.begin(); it != openArticulationRuns.end();) { + if (presentArticulations.find(it->first) == presentArticulations.end()) { + finalizeArticulationRun(it->second); + it = openArticulationRuns.erase(it); } else { ++it; } @@ -1027,12 +1040,61 @@ void Braille::computeArticulationDoubling() if (key.isEmpty()) { continue; } - openRuns[key].push_back(artic); + openArticulationRuns[key].push_back(artic); + } + + std::vector notes; + const ClefType clefType = chord->staff() ? chord->staff()->clef(chord->tick()) : ClefType::INVALID; + if (ascendingChords(clefType)) { + for (auto it = chord->notes().begin(); it != chord->notes().end(); ++it) { + notes.push_back(*it); + } + } else { + for (auto it = chord->notes().rbegin(); it != chord->notes().rend(); ++it) { + notes.push_back(*it); + } + } + + Note* rootNote = notes.empty() ? nullptr : notes.front(); + std::set presentIntervals; + if (notes.size() > 1) { + for (auto it = notes.begin() + 1; it != notes.end(); ++it) { + const QString key = brailleIntervalSign(computeInterval(rootNote, *it, true)); + if (key.isEmpty()) { + continue; + } + presentIntervals.insert(key); + } + } + + // End any open run whose interval sign is not present on this chord. + for (auto it = openIntervalRuns.begin(); it != openIntervalRuns.end();) { + if (presentIntervals.find(it->first) == presentIntervals.end()) { + finalizeIntervalRun(it->second); + it = openIntervalRuns.erase(it); + } else { + ++it; + } + } + + if (notes.size() <= 1) { + continue; + } + + for (auto it = notes.begin() + 1; it != notes.end(); ++it) { + const QString key = brailleIntervalSign(computeInterval(rootNote, *it, true)); + if (key.isEmpty()) { + continue; + } + openIntervalRuns[key].push_back(*it); } } - for (const auto& pair : openRuns) { - finalizeRun(pair.second); + for (const auto& pair : openArticulationRuns) { + finalizeArticulationRun(pair.second); + } + for (const auto& pair : openIntervalRuns) { + finalizeIntervalRun(pair.second); } } } @@ -1851,7 +1913,7 @@ QString Braille::brailleBreath(Breath* breath) QString Braille::brailleChord(Chord* chord) { - // 9.3 Interval Doubling and 9.5 Moving notes methods are not implemented as they are optional methods + // 9.5 Moving notes methods are not implemented as they are optional methods if (!chord || chord->notes().empty()) { return QString(); @@ -1905,28 +1967,28 @@ QString Braille::brailleChord(Chord* chord) } } - if (!m_context.articulationDoublingComputed) { - computeArticulationDoubling(); + if (!m_context.signDoublingComputed) { + computeSignDoubling(); } QString articulationsBraille = QString(); // prefix, before the note QString articulationsBrailleAfter = QString(); // suffix, after the note (doubling closing sign) for (Articulation* artic : chord->articulations()) { auto it = m_context.articulationDoubling.find(artic); - ArticulationDoubling state = (it != m_context.articulationDoubling.end()) ? it->second : ArticulationDoubling::Single; + SignDoubling state = (it != m_context.articulationDoubling.end()) ? it->second : SignDoubling::Single; const QString code = brailleArticulation(artic); switch (state) { - case ArticulationDoubling::Omit: + case SignDoubling::Omit: // Middle of a doubled group: the sign is not written. break; - case ArticulationDoubling::Double: + case SignDoubling::Double: // First note of a doubled group: write the sign twice, before the note. articulationsBraille += code + code; break; - case ArticulationDoubling::CloseSuffix: + case SignDoubling::CloseSuffix: // Last note of a doubled group: write the closing sign once, after the note. articulationsBrailleAfter += code; break; - case ArticulationDoubling::Single: + case SignDoubling::Single: default: articulationsBraille += code; break; @@ -1968,6 +2030,21 @@ QString Braille::brailleChord(Chord* chord) return result; } +QString Braille::brailleIntervalSign(int interval) +{ + // Table 9. Intervals. Page 7. Music Braille Code 2015 + switch (interval) { + case 1: return BRAILLE_OCTAVE; + case 2: return BRAILLE_SECOND; + case 3: return BRAILLE_THIRD; + case 4: return BRAILLE_FOURTH; + case 5: return BRAILLE_FIFTH; + case 6: return BRAILLE_SIXTH; + case 7: return BRAILLE_SEVENTH; + default: return QString(); + } +} + //----------------------------------------------------------------------------------- // brailleChordInterval // rootNote: The root note of the chord. @@ -1982,24 +2059,7 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& } int interval = computeInterval(rootNote, note, true); - QString intervalBraille; - //Table 9. Intervals. Page 7. Music Braille Code 2015 - switch (interval) { - case 1: intervalBraille = BRAILLE_OCTAVE; - break; - case 2: intervalBraille = BRAILLE_SECOND; - break; - case 3: intervalBraille = BRAILLE_THIRD; - break; - case 4: intervalBraille = BRAILLE_FOURTH; - break; - case 5: intervalBraille = BRAILLE_FIFTH; - break; - case 6: intervalBraille = BRAILLE_SIXTH; - break; - case 7: intervalBraille = BRAILLE_SEVENTH; - break; - } + QString intervalBraille = brailleIntervalSign(interval); QString noteOctaveBraille = QString(); QString noteAccidentalBraille = QString(); if (note->accidental() != nullptr) { @@ -2036,6 +2096,24 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& } } + if (!m_context.signDoublingComputed) { + computeSignDoubling(); + } + auto it = m_context.intervalDoubling.find(note); + SignDoubling state = (it != m_context.intervalDoubling.end()) ? it->second : SignDoubling::Single; + switch (state) { + case SignDoubling::Omit: + intervalBraille.clear(); + break; + case SignDoubling::Double: + intervalBraille += intervalBraille; + break; + case SignDoubling::CloseSuffix: + case SignDoubling::Single: + default: + break; + } + return noteAccidentalBraille + noteOctaveBraille diff --git a/src/braille/internal/braille.h b/src/braille/internal/braille.h index c1fce236ffe45..a2798d3f26553 100644 --- a/src/braille/internal/braille.h +++ b/src/braille/internal/braille.h @@ -153,13 +153,13 @@ class TextToUEBBraille QMap textToBrailleASCII; }; -// Rendering decision for an articulation under the Music Braille Code 2015 doubling rule. +// Rendering decision for a sign under the Music Braille Code 2015 doubling rule. // Absence from the map means Single (render the sign once, the default behaviour). -enum class ArticulationDoubling { - Single = 0, // default: write the sign once, as a prefix before the note - Double = 1, // first note of a group: write the sign twice, as a prefix +enum class SignDoubling { + Single = 0, // default: write the sign once + Double = 1, // first note of a group: write the sign twice Omit = 2, // middle notes of a group: do not write the sign - CloseSuffix = 3, // last note of a group: write the sign once, as a suffix after the note + CloseSuffix = 3, // last note of a group: write the sign once after the note }; struct BrailleContext { @@ -167,9 +167,10 @@ struct BrailleContext { std::vector currentClefType; std::vector currentKey; - // Per-articulation-instance decision for the doubling rule, computed once per Braille run. - std::map articulationDoubling; - bool articulationDoublingComputed = false; + // Per-sign-instance decisions for the doubling rule, computed once per Braille run. + std::map articulationDoubling; + std::map intervalDoubling; + bool signDoublingComputed = false; }; // Braille export is implemented according to Music Braille Code 2015 @@ -188,7 +189,7 @@ class Braille static constexpr int MAX_CHARS_PER_LINE = 40; // Music Braille Code 2015: the same sign on this many notes in a row is doubled. - static constexpr size_t ARTICULATION_DOUBLING_MIN_GROUP = 4; + static constexpr size_t SIGN_DOUBLING_MIN_GROUP = 4; muse::GlobalInject brailleConfiguration; @@ -198,7 +199,7 @@ class Braille void resetOctave(size_t stave); void resetOctaves(); - void computeArticulationDoubling(); + void computeSignDoubling(); void credits(QIODevice& device); void instruments(QIODevice& device); @@ -213,6 +214,7 @@ class Braille bool isShortShortSlurConvergence(const std::vector& slurs); bool isLongLongSlurConvergence(const std::vector& slurs); bool hasTies(ChordRest* chordRest); + QString brailleIntervalSign(int interval); bool ascendingChords(ClefType clefType); BarLine* firstBarline(Measure* measure, track_idx_t track); BarLine* lastBarline(Measure* measure, track_idx_t track); From 0ce3187bcde578278595765d7ba34945b7e774b4 Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Fri, 3 Jul 2026 14:29:42 +0200 Subject: [PATCH 7/8] change method name to sign doubling, also cover arpeggio --- src/braille/ibrailleconfiguration.h | 6 +- src/braille/internal/braille.cpp | 136 +++++++++--------- src/braille/internal/braille.h | 3 +- src/braille/internal/brailleconfiguration.cpp | 20 +-- src/braille/internal/brailleconfiguration.h | 8 +- src/braille/internal/notationbraille.cpp | 4 +- .../Preferences/BraillePreferencesPage.qml | 6 +- .../Preferences/braillepreferencesmodel.cpp | 16 +-- .../Preferences/braillepreferencesmodel.h | 8 +- .../internal/BrailleAdvancedSection.qml | 12 +- 10 files changed, 111 insertions(+), 108 deletions(-) diff --git a/src/braille/ibrailleconfiguration.h b/src/braille/ibrailleconfiguration.h index fd7bedaa61c9e..835fd28a925d7 100644 --- a/src/braille/ibrailleconfiguration.h +++ b/src/braille/ibrailleconfiguration.h @@ -50,9 +50,9 @@ class IBrailleConfiguration : MODULE_GLOBAL_INTERFACE virtual void setBrailleTable(const QString& table) = 0; virtual QStringList brailleTableList() const = 0; - virtual muse::async::Notification articulationDoublingChanged() const = 0; - virtual bool articulationDoubling() const = 0; - virtual void setArticulationDoubling(const bool enabled) = 0; + virtual muse::async::Notification signDoublingChanged() const = 0; + virtual bool signDoubling() const = 0; + virtual void setSignDoubling(const bool enabled) = 0; }; } diff --git a/src/braille/internal/braille.cpp b/src/braille/internal/braille.cpp index d0f960e008f10..6b8d024c29b5e 100644 --- a/src/braille/internal/braille.cpp +++ b/src/braille/internal/braille.cpp @@ -952,7 +952,7 @@ void Braille::resetOctaves() } // 22. Doubling of Signs. Music Braille Code 2015. -// When the same articulation or interval sign appears on more than three consecutive notes +// When the same sign appears on more than three consecutive notes // (ignoring any rests between them), it is written twice before the first note of the group // and once after the last note, and omitted on the notes in between. This precomputes, per // sign instance, whether it should be doubled, omitted, closed, or rendered singly @@ -960,45 +960,32 @@ void Braille::resetOctaves() void Braille::computeSignDoubling() { m_context.signDoublingComputed = true; - m_context.articulationDoubling.clear(); - m_context.intervalDoubling.clear(); + m_context.signDoubling.clear(); // When disabled, the empty maps mean every sign is rendered singly (legacy behaviour). // The configuration may be unavailable (e.g. in unit tests that don't register the braille // module); in that case fall back to the default of doubling enabled. - if (brailleConfiguration() && !brailleConfiguration()->articulationDoubling()) { + if (brailleConfiguration() && !brailleConfiguration()->signDoubling()) { return; } - auto finalizeArticulationRun = [this](const std::vector& run) { + auto finalizeRun = [this](const std::vector& run) { if (run.size() < SIGN_DOUBLING_MIN_GROUP) { return; } // Double (prefix) on the first note, omit on the middle notes, and a single closing sign // as a suffix after the last note. run.size() >= 4 here, so front and back differ. - m_context.articulationDoubling[run.front()] = SignDoubling::Double; + m_context.signDoubling[run.front()] = SignDoubling::Double; for (size_t i = 1; i + 1 < run.size(); ++i) { - m_context.articulationDoubling[run[i]] = SignDoubling::Omit; + m_context.signDoubling[run[i]] = SignDoubling::Omit; } - m_context.articulationDoubling[run.back()] = SignDoubling::CloseSuffix; - }; - - auto finalizeIntervalRun = [this](const std::vector& run) { - if (run.size() < SIGN_DOUBLING_MIN_GROUP) { - return; - } - m_context.intervalDoubling[run.front()] = SignDoubling::Double; - for (size_t i = 1; i + 1 < run.size(); ++i) { - m_context.intervalDoubling[run[i]] = SignDoubling::Omit; - } - m_context.intervalDoubling[run.back()] = SignDoubling::CloseSuffix; + m_context.signDoubling[run.back()] = SignDoubling::CloseSuffix; }; const size_t ntracks = m_score->staves().size() * VOICES; for (track_idx_t track = 0; track < ntracks; ++track) { - // Runs of consecutive chords sharing the same Braille sign. - std::map > openArticulationRuns; - std::map > openIntervalRuns; + // Runs of consecutive chords sharing the same typed Braille sign. + std::map > openSignRuns; for (Segment* seg = m_score->firstSegment(SegmentType::ChordRest); seg; seg = seg->next1(SegmentType::ChordRest)) { EngravingItem* el = seg->element(track); @@ -1015,32 +1002,18 @@ void Braille::computeSignDoubling() Chord* chord = toChord(el); - std::set presentArticulations; + std::set presentSigns; for (Articulation* artic : chord->articulations()) { const QString key = brailleArticulation(artic); if (key.isEmpty()) { continue; } - presentArticulations.insert(key); + presentSigns.insert(QStringLiteral("articulation:") + key); } - // End any open run whose articulation is not present on this chord. - for (auto it = openArticulationRuns.begin(); it != openArticulationRuns.end();) { - if (presentArticulations.find(it->first) == presentArticulations.end()) { - finalizeArticulationRun(it->second); - it = openArticulationRuns.erase(it); - } else { - ++it; - } - } - - // Extend or start a run for each articulation on this chord. - for (Articulation* artic : chord->articulations()) { - const QString key = brailleArticulation(artic); - if (key.isEmpty()) { - continue; - } - openArticulationRuns[key].push_back(artic); + const QString arpeggioCode = brailleArpeggio(chord->arpeggio()); + if (!arpeggioCode.isEmpty()) { + presentSigns.insert(QStringLiteral("arpeggio:") + arpeggioCode); } std::vector notes; @@ -1056,45 +1029,52 @@ void Braille::computeSignDoubling() } Note* rootNote = notes.empty() ? nullptr : notes.front(); - std::set presentIntervals; if (notes.size() > 1) { for (auto it = notes.begin() + 1; it != notes.end(); ++it) { const QString key = brailleIntervalSign(computeInterval(rootNote, *it, true)); if (key.isEmpty()) { continue; } - presentIntervals.insert(key); + presentSigns.insert(QStringLiteral("interval:") + key); } } - // End any open run whose interval sign is not present on this chord. - for (auto it = openIntervalRuns.begin(); it != openIntervalRuns.end();) { - if (presentIntervals.find(it->first) == presentIntervals.end()) { - finalizeIntervalRun(it->second); - it = openIntervalRuns.erase(it); + // End any open run whose typed sign is not present on this chord. + for (auto it = openSignRuns.begin(); it != openSignRuns.end();) { + if (presentSigns.find(it->first) == presentSigns.end()) { + finalizeRun(it->second); + it = openSignRuns.erase(it); } else { ++it; } } - if (notes.size() <= 1) { - continue; - } - - for (auto it = notes.begin() + 1; it != notes.end(); ++it) { - const QString key = brailleIntervalSign(computeInterval(rootNote, *it, true)); + // Extend or start a run for each sign instance on this chord. + for (Articulation* artic : chord->articulations()) { + const QString key = brailleArticulation(artic); if (key.isEmpty()) { continue; } - openIntervalRuns[key].push_back(*it); + openSignRuns[QStringLiteral("articulation:") + key].push_back(artic); + } + + if (!arpeggioCode.isEmpty()) { + openSignRuns[QStringLiteral("arpeggio:") + arpeggioCode].push_back(chord->arpeggio()); } - } - for (const auto& pair : openArticulationRuns) { - finalizeArticulationRun(pair.second); + if (notes.size() > 1) { + for (auto it = notes.begin() + 1; it != notes.end(); ++it) { + const QString key = brailleIntervalSign(computeInterval(rootNote, *it, true)); + if (key.isEmpty()) { + continue; + } + openSignRuns[QStringLiteral("interval:") + key].push_back(*it); + } + } } - for (const auto& pair : openIntervalRuns) { - finalizeIntervalRun(pair.second); + + for (const auto& pair : openSignRuns) { + finalizeRun(pair.second); } } } @@ -1973,8 +1953,8 @@ QString Braille::brailleChord(Chord* chord) QString articulationsBraille = QString(); // prefix, before the note QString articulationsBrailleAfter = QString(); // suffix, after the note (doubling closing sign) for (Articulation* artic : chord->articulations()) { - auto it = m_context.articulationDoubling.find(artic); - SignDoubling state = (it != m_context.articulationDoubling.end()) ? it->second : SignDoubling::Single; + auto it = m_context.signDoubling.find(artic); + SignDoubling state = (it != m_context.signDoubling.end()) ? it->second : SignDoubling::Single; const QString code = brailleArticulation(artic); switch (state) { case SignDoubling::Omit: @@ -2001,7 +1981,30 @@ QString Braille::brailleChord(Chord* chord) QString hairpinBrailleAfter = brailleHairpinAfter(chord, chordHairpins); - QString arpeggio = brailleArpeggio(chord->arpeggio()); + QString arpeggioBraille = QString(); // prefix, before the note + QString arpeggioBrailleAfter = QString(); // suffix, after the note (doubling closing sign) + if (Arpeggio* arpeggio = chord->arpeggio()) { + auto it = m_context.signDoubling.find(arpeggio); + SignDoubling state = (it != m_context.signDoubling.end()) ? it->second : SignDoubling::Single; + const QString code = brailleArpeggio(arpeggio); + switch (state) { + case SignDoubling::Omit: + // Middle of a doubled group: the sign is not written. + break; + case SignDoubling::Double: + // First note of a doubled group: write the sign twice, before the note. + arpeggioBraille += code + code; + break; + case SignDoubling::CloseSuffix: + // Last note of a doubled group: write the closing sign once, after the note. + arpeggioBrailleAfter += code; + break; + case SignDoubling::Single: + default: + arpeggioBraille += code; + break; + } + } QString tremoloBraille = brailleTremolo(chord); QString glissandoLastNoteBraille = brailleGlissando(notes.back()); @@ -2010,12 +2013,13 @@ QString Braille::brailleChord(Chord* chord) result += hairpinBrailleBefore; result += graceNotesBefore; result += graceNoteMarking; - result += arpeggio; + result += arpeggioBraille; result += articulationsBraille; result += slurBrailleBefore; result += tupletBraille; result += rootNoteBraille; result += intervals; + result += arpeggioBrailleAfter; result += articulationsBrailleAfter; result += tremoloBraille; result += chordTieBraille; @@ -2099,8 +2103,8 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& if (!m_context.signDoublingComputed) { computeSignDoubling(); } - auto it = m_context.intervalDoubling.find(note); - SignDoubling state = (it != m_context.intervalDoubling.end()) ? it->second : SignDoubling::Single; + auto it = m_context.signDoubling.find(note); + SignDoubling state = (it != m_context.signDoubling.end()) ? it->second : SignDoubling::Single; switch (state) { case SignDoubling::Omit: intervalBraille.clear(); diff --git a/src/braille/internal/braille.h b/src/braille/internal/braille.h index 6f5e5f2a66193..98f5cb4b49bb6 100644 --- a/src/braille/internal/braille.h +++ b/src/braille/internal/braille.h @@ -168,8 +168,7 @@ struct BrailleContext { std::vector currentKey; // Per-sign-instance decisions for the doubling rule, computed once per Braille run. - std::map articulationDoubling; - std::map intervalDoubling; + std::map signDoubling; bool signDoublingComputed = false; }; diff --git a/src/braille/internal/brailleconfiguration.cpp b/src/braille/internal/brailleconfiguration.cpp index e8f84bbe64a41..d5cee53f96c07 100644 --- a/src/braille/internal/brailleconfiguration.cpp +++ b/src/braille/internal/brailleconfiguration.cpp @@ -33,7 +33,7 @@ static const std::string module_name("braille"); static const Settings::Key BRAILLE_STATUS(module_name, "score/braille/status"); static const Settings::Key BRAILLE_TABLE(module_name, "score/braille/table"); static const Settings::Key BRAILLE_INTERVAL_DIRECTION(module_name, "score/braille/intervalDirection"); -static const Settings::Key BRAILLE_ARTICULATION_DOUBLING(module_name, "score/braille/articulationDoubling"); +static const Settings::Key BRAILLE_SIGN_DOUBLING(module_name, "score/braille/signDoubling"); void BrailleConfiguration::init() { @@ -49,9 +49,9 @@ void BrailleConfiguration::init() settings()->valueChanged(BRAILLE_INTERVAL_DIRECTION).onReceive(this, [this](const Val&) { m_intervalDirectionChanged.notify(); }); - settings()->setDefaultValue(BRAILLE_ARTICULATION_DOUBLING, Val(true)); - settings()->valueChanged(BRAILLE_ARTICULATION_DOUBLING).onReceive(this, [this](const Val&) { - m_articulationDoublingChanged.notify(); + settings()->setDefaultValue(BRAILLE_SIGN_DOUBLING, Val(true)); + settings()->valueChanged(BRAILLE_SIGN_DOUBLING).onReceive(this, [this](const Val&) { + m_signDoublingChanged.notify(); }); } @@ -100,19 +100,19 @@ void BrailleConfiguration::setBrailleTable(const QString& table) settings()->setSharedValue(BRAILLE_TABLE, Val(table)); } -muse::async::Notification BrailleConfiguration::articulationDoublingChanged() const +muse::async::Notification BrailleConfiguration::signDoublingChanged() const { - return m_articulationDoublingChanged; + return m_signDoublingChanged; } -bool BrailleConfiguration::articulationDoubling() const +bool BrailleConfiguration::signDoubling() const { - return settings()->value(BRAILLE_ARTICULATION_DOUBLING).toBool(); + return settings()->value(BRAILLE_SIGN_DOUBLING).toBool(); } -void BrailleConfiguration::setArticulationDoubling(const bool enabled) +void BrailleConfiguration::setSignDoubling(const bool enabled) { - settings()->setSharedValue(BRAILLE_ARTICULATION_DOUBLING, Val(enabled)); + settings()->setSharedValue(BRAILLE_SIGN_DOUBLING, Val(enabled)); } QStringList BrailleConfiguration::brailleTableList() const diff --git a/src/braille/internal/brailleconfiguration.h b/src/braille/internal/brailleconfiguration.h index d018a82094e56..f5b673c3d648d 100644 --- a/src/braille/internal/brailleconfiguration.h +++ b/src/braille/internal/brailleconfiguration.h @@ -46,15 +46,15 @@ class BrailleConfiguration : public mu::braille::IBrailleConfiguration, public m void setBrailleTable(const QString& table) override; QStringList brailleTableList() const override; - muse::async::Notification articulationDoublingChanged() const override; - bool articulationDoubling() const override; - void setArticulationDoubling(const bool enabled) override; + muse::async::Notification signDoublingChanged() const override; + bool signDoubling() const override; + void setSignDoubling(const bool enabled) override; private: muse::async::Notification m_braillePanelEnabledChanged; muse::async::Notification m_brailleTableChanged; muse::async::Notification m_intervalDirectionChanged; - muse::async::Notification m_articulationDoublingChanged; + muse::async::Notification m_signDoublingChanged; }; } diff --git a/src/braille/internal/notationbraille.cpp b/src/braille/internal/notationbraille.cpp index 4b94eced715cf..be12189b38bf8 100644 --- a/src/braille/internal/notationbraille.cpp +++ b/src/braille/internal/notationbraille.cpp @@ -74,9 +74,9 @@ void NotationBraille::init() setIntervalDirection(direction); }); - // The Braille generator reads articulationDoubling directly from configuration, so no value + // The Braille generator reads signDoubling directly from configuration, so no value // needs threading here; we just force a re-render so the panel reflects the change immediately. - brailleConfiguration()->articulationDoublingChanged().onNotify(this, [this]() { + brailleConfiguration()->signDoublingChanged().onNotify(this, [this]() { doBraille(true); }); diff --git a/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml b/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml index 95c4a1647b80f..b41773feb098b 100644 --- a/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml +++ b/src/preferences/qml/MuseScore/Preferences/BraillePreferencesPage.qml @@ -61,7 +61,7 @@ PreferencesPage { directions: preferencesModel.intervalDirections() brailleTable: preferencesModel.brailleTable intervalDirection: preferencesModel.intervalDirection - articulationDoubling: preferencesModel.articulationDoubling + signDoubling: preferencesModel.signDoubling navigation.section: root.navigationSection navigation.order: root.navigationOrderStart + 2 @@ -76,8 +76,8 @@ PreferencesPage { preferencesModel.intervalDirection = direction; } - onArticulationDoublingChangeRequested: function(enabled) { - preferencesModel.articulationDoubling = enabled; + onSignDoublingChangeRequested: function(enabled) { + preferencesModel.signDoubling = enabled; } onFocusChanged: { diff --git a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp index 29c4bb95678be..7e90d1379ea9e 100644 --- a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp +++ b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.cpp @@ -48,8 +48,8 @@ void BraillePreferencesModel::load() emit brailleTableChanged(brailleTable()); }); - brailleConfiguration()->articulationDoublingChanged().onNotify(this, [this]() { - emit articulationDoublingChanged(articulationDoubling()); + brailleConfiguration()->signDoublingChanged().onNotify(this, [this]() { + emit signDoublingChanged(signDoubling()); }); } @@ -68,9 +68,9 @@ int BraillePreferencesModel::intervalDirection() const return static_cast(brailleConfiguration()->intervalDirection()); } -bool BraillePreferencesModel::articulationDoubling() const +bool BraillePreferencesModel::signDoubling() const { - return brailleConfiguration()->articulationDoubling(); + return brailleConfiguration()->signDoubling(); } QStringList BraillePreferencesModel::brailleTables() const @@ -129,12 +129,12 @@ void BraillePreferencesModel::setIntervalDirection(int direction) emit intervalDirectionChanged(direction); } -void BraillePreferencesModel::setArticulationDoubling(bool value) +void BraillePreferencesModel::setSignDoubling(bool value) { - if (value == articulationDoubling()) { + if (value == signDoubling()) { return; } - brailleConfiguration()->setArticulationDoubling(value); - emit articulationDoublingChanged(value); + brailleConfiguration()->setSignDoubling(value); + emit signDoublingChanged(value); } diff --git a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h index 49c901847260e..8b9f3e9b30774 100644 --- a/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h +++ b/src/preferences/qml/MuseScore/Preferences/braillepreferencesmodel.h @@ -39,7 +39,7 @@ class BraillePreferencesModel : public QObject, public muse::Contextable, public Q_PROPERTY(bool braillePanelEnabled READ braillePanelEnabled WRITE setBraillePanelEnabled NOTIFY braillePanelEnabledChanged) Q_PROPERTY(QString brailleTable READ brailleTable WRITE setBrailleTable NOTIFY brailleTableChanged) Q_PROPERTY(int intervalDirection READ intervalDirection WRITE setIntervalDirection NOTIFY intervalDirectionChanged) - Q_PROPERTY(bool articulationDoubling READ articulationDoubling WRITE setArticulationDoubling NOTIFY articulationDoublingChanged) + Q_PROPERTY(bool signDoubling READ signDoubling WRITE setSignDoubling NOTIFY signDoublingChanged) muse::GlobalInject brailleConfiguration; @@ -51,7 +51,7 @@ class BraillePreferencesModel : public QObject, public muse::Contextable, public bool braillePanelEnabled() const; QString brailleTable() const; int intervalDirection() const; - bool articulationDoubling() const; + bool signDoubling() const; Q_INVOKABLE QStringList brailleTables() const; Q_INVOKABLE QVariantList intervalDirections() const; @@ -60,12 +60,12 @@ public slots: void setBraillePanelEnabled(bool value); void setBrailleTable(QString table); void setIntervalDirection(int direction); - void setArticulationDoubling(bool value); + void setSignDoubling(bool value); signals: void braillePanelEnabledChanged(bool value); void brailleTableChanged(QString value); void intervalDirectionChanged(int value); - void articulationDoublingChanged(bool value); + void signDoublingChanged(bool value); }; } diff --git a/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml b/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml index 12c7e58af7735..1c24af3f0a714 100644 --- a/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml +++ b/src/preferences/qml/MuseScore/Preferences/internal/BrailleAdvancedSection.qml @@ -34,11 +34,11 @@ BaseSection { property var directions: null property int intervalDirection: -1 - property alias articulationDoubling: articulationDoublingBox.checked + property alias signDoubling: signDoublingBox.checked signal brailleTableChangeRequested(string table) signal intervalDirectionChangeRequested(int direction) - signal articulationDoublingChangeRequested(bool enabled) + signal signDoublingChangeRequested(bool enabled) ComboBoxWithTitle { title: qsTrc("preferences", "Braille table for lyrics") @@ -73,17 +73,17 @@ BaseSection { } CheckBox { - id: articulationDoublingBox + id: signDoublingBox width: parent.width - text: qsTrc("preferences", "Double repeated articulation marks (Braille rule)") + text: qsTrc("preferences", "Double repeated Braille signs") - navigation.name: "ArticulationDoublingBox" + navigation.name: "SignDoublingBox" navigation.panel: root.navigation navigation.row: 3 onClicked: { - root.articulationDoublingChangeRequested(!checked); + root.signDoublingChangeRequested(!checked); } } } From 7bd0b9277c180979651f9cdf6cfa4c7c91a46c26 Mon Sep 17 00:00:00 2001 From: Julian Dreykorn Date: Fri, 3 Jul 2026 18:28:13 +0200 Subject: [PATCH 8/8] add missing notehead signs --- src/braille/internal/braille.cpp | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/braille/internal/braille.cpp b/src/braille/internal/braille.cpp index 6b8d024c29b5e..0b81204c79442 100644 --- a/src/braille/internal/braille.cpp +++ b/src/braille/internal/braille.cpp @@ -135,6 +135,11 @@ namespace mu::engraving { #define BRAILLE_BREVE_SUFFIX QString("k") #define BRAILLE_256TH_PREFIX QString(";<1") #define BRAILLE_DURATION_DOT QString("'") +#define BRAILLE_SOLID_NOTEHEAD_ONLY QString("5a") +#define BRAILLE_X_SHAPED_NOTEHEAD QString("5b") +#define BRAILLE_DIAMOND_SHAPED_NOTEHEAD QString("5l") +#define BRAILLE_INDETERMINATE_PITCH QString("5k") +#define BRAILLE_APPROX_PITCH_LINE_END QString("5'") // Table 3. Page 4. Music Braille Code 2015. #define BRAILLE_BELOW_FIRST_OCTAVE QString("@@") @@ -1891,6 +1896,64 @@ QString Braille::brailleBreath(Breath* breath) } } +static bool isGlissandoEndNote(Note* note) +{ + if (!note) { + return false; + } + + for (Spanner* spanner : note->spannerBack()) { + if (spanner->isGlissando()) { + return true; + } + } + + return false; +} + +static QString brailleSpecialNoteheadPrefix(Note* note) +{ + if (!note) { + return QString(); + } + + if (isGlissandoEndNote(note)) { + return BRAILLE_APPROX_PITCH_LINE_END; + } + + switch (note->headGroup()) { + case NoteHeadGroup::HEAD_CROSS: + case NoteHeadGroup::HEAD_WITHX: + case NoteHeadGroup::HEAD_XCIRCLE: + case NoteHeadGroup::HEAD_HEAVY_CROSS: + case NoteHeadGroup::HEAD_HEAVY_CROSS_HAT: + return BRAILLE_X_SHAPED_NOTEHEAD; + + case NoteHeadGroup::HEAD_DIAMOND: + case NoteHeadGroup::HEAD_DIAMOND_OLD: + case NoteHeadGroup::HEAD_LARGE_DIAMOND: + return BRAILLE_DIAMOND_SHAPED_NOTEHEAD; + + case NoteHeadGroup::HEAD_SLASH: + case NoteHeadGroup::HEAD_SLASHED1: + case NoteHeadGroup::HEAD_SLASHED2: + return BRAILLE_INDETERMINATE_PITCH; + + case NoteHeadGroup::HEAD_NORMAL: { + Chord* chord = note->chord(); + if (chord && chord->noStem() && chord->durationType().headType() == NoteHeadType::HEAD_QUARTER) { + return BRAILLE_SOLID_NOTEHEAD_ONLY; + } + break; + } + + default: + break; + } + + return QString(); +} + QString Braille::brailleChord(Chord* chord) { // 9.5 Moving notes methods are not implemented as they are optional methods @@ -2092,6 +2155,7 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& } QString noteTieBraille = brailleTie(note); + QString specialNoteheadPrefix = brailleSpecialNoteheadPrefix(note); QString fingeringAfterBraille = QString(); for (EngravingItem* el : rootNote->el()) { @@ -2108,6 +2172,7 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& switch (state) { case SignDoubling::Omit: intervalBraille.clear(); + specialNoteheadPrefix.clear(); break; case SignDoubling::Double: intervalBraille += intervalBraille; @@ -2121,6 +2186,7 @@ QString Braille::brailleChordInterval(Note* rootNote, const std::vector& return noteAccidentalBraille + noteOctaveBraille + + specialNoteheadPrefix + intervalBraille + fingeringAfterBraille + noteTieBraille; @@ -2157,6 +2223,7 @@ QString Braille::brailleChordRootNote(Chord* chord, Note* rootNote) accidentalBraille = brailleAccidentalType(rootNote->accidental()->accidentalType()); } + QString specialNoteheadPrefix = brailleSpecialNoteheadPrefix(rootNote); QString noteTieBraille = brailleTie(rootNote); QString fingeringAfterBraille = QString(); @@ -2169,6 +2236,7 @@ QString Braille::brailleChordRootNote(Chord* chord, Note* rootNote) return accidentalBraille + octaveBraille + + specialNoteheadPrefix + noteBraille + fingeringAfterBraille + noteTieBraille;