Skip to content

Commit

Permalink
Accessibility: Improve speech output for notes and common elements
Browse files Browse the repository at this point in the history
Screen readers will now announce:

- [Fix #312625] Notehead group type (e.g. "cross", "triangle", etc.).

- [Fix #312628] When notes or rests use cross-staff notation.

- [Fix #312627] When elements are invisible.

- [Fix #307442] Tuplet length for custom tuplets (more than 9 notes).

Also speeds up speech output by stripping some punctuation from element
names to prevent unnecessary pauses.
  • Loading branch information
shoogle committed Jul 12, 2022
1 parent 54d6c4f commit 52be682
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 8 deletions.
27 changes: 23 additions & 4 deletions src/engraving/accessibility/accessibleitem.cpp
Expand Up @@ -25,14 +25,27 @@
#include "../libmscore/score.h"
#include "../libmscore/measure.h"

#include "translation.h"
#include "log.h"

using namespace mu;
using namespace mu::engraving;
using namespace mu::accessibility;
using namespace mu::engraving;

bool AccessibleItem::enabled = true;

static QString readable(QString s)
{
// Remove undesired pauses in screen reader speech output
s.remove(u':');

// Handle desired pauses
s.replace(u';', u','); // NVDA doesn't pause for semicolon

return s;
}

AccessibleItem::AccessibleItem(EngravingItem* e, Role role)
: m_element(e), m_role(role)
{
Expand Down Expand Up @@ -169,9 +182,15 @@ QString AccessibleItem::accessibleName() const
QString staffInfo = root ? root->staffInfo() : "";
QString barsAndBeats = m_element->formatBarsAndBeats();

return QString("%1%2%3").arg(!staffInfo.isEmpty() ? (staffInfo + "; ") : "")
.arg(m_element->accessibleInfo().toQString())
.arg(!barsAndBeats.isEmpty() ? ("; " + barsAndBeats) : "");
barsAndBeats.remove(u';'); // Too many pauses in speech

QString name = QString("%1%2%3%4")
.arg(!staffInfo.isEmpty() ? (staffInfo + "; ") : "")
.arg(m_element->screenReaderInfo().toQString())
.arg(m_element->visible() ? "" : " " + qtrc("engraving", "invisible"))
.arg(!barsAndBeats.isEmpty() ? ("; " + barsAndBeats) : "");

return readable(name);
}

QString AccessibleItem::accessibleDescription() const
Expand All @@ -185,7 +204,7 @@ QString AccessibleItem::accessibleDescription() const
result = accessibleName() + " ";
#endif

result += m_element->accessibleExtraInfo();
result += readable(m_element->accessibleExtraInfo());
return result;
}

Expand Down
3 changes: 2 additions & 1 deletion src/engraving/libmscore/chordrest.cpp
Expand Up @@ -683,7 +683,8 @@ String ChordRest::durationUserName() const
tupletType = mtrc("engraving", "Nonuplet");
break;
default:
tupletType = mtrc("engraving", "Custom tuplet");
//: %1 is tuplet ratio numerator (i.e. the number of notes in the tuplet)
tupletType = mtrc("engraving", "%1 note tuplet").arg(tuplet()->ratio().numerator());
}
}
String dotString;
Expand Down
10 changes: 9 additions & 1 deletion src/engraving/libmscore/note.cpp
Expand Up @@ -3332,7 +3332,15 @@ String Note::screenReaderInfo() const
pitchName = mtrc("engraving", "%1; String: %2; Fret: %3")
.arg(tpcUserName(true), String::number(string() + 1), String::number(fret()));
} else {
pitchName = tpcUserName(true);
pitchName = _headGroup == NoteHeadGroup::HEAD_NORMAL
? tpcUserName(true)
//: head as in note head. %1 is head type (circle, cross, etc.). %2 is pitch (e.g. Db4).
: mtrc("engraving", "%1 head %2").arg(subtypeName()).arg(tpcUserName(true));
if (chord()->staffMove() < 0) {
duration += u"; " + mtrc("engraving", "Cross-staff above");
} else if (chord()->staffMove() > 0) {
duration += u"; " + mtrc("engraving", "Cross-staff below");
}
}
return String(u"%1 %2 %3%4").arg(noteTypeUserName(), pitchName, duration, (chord()->isGrace() ? u"" : String(u"; %1").arg(voice)));
}
Expand Down
10 changes: 8 additions & 2 deletions src/engraving/libmscore/rest.cpp
Expand Up @@ -863,8 +863,14 @@ String Rest::screenReaderInfo() const
{
Measure* m = measure();
bool voices = m ? m->hasVoices(staffIdx()) : false;
String voice = voices ? mtrc("engraving", "Voice: %1").arg(track() % VOICES + 1) : u"";
return String(u"%1 %2 %3").arg(EngravingItem::accessibleInfo(), durationUserName(), voice);
String voice = voices ? (u"; " + mtrc("engraving", "Voice: %1").arg(track() % VOICES + 1)) : u"";
String crossStaff;
if (staffMove() < 0) {
crossStaff = u"; " + mtrc("engraving", "Cross-staff above");
} else if (staffMove() > 0) {
crossStaff = u"; " + mtrc("engraving", "Cross-staff below");
}
return String(u"%1 %2%3%4").arg(EngravingItem::accessibleInfo(), durationUserName(), crossStaff, voice);
}

//---------------------------------------------------------
Expand Down

0 comments on commit 52be682

Please sign in to comment.