[orca] Initial commit of the sound support - not yet hooked up
- From: Joanmarie Diggs <joanied src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [orca] Initial commit of the sound support - not yet hooked up
- Date: Tue, 16 Feb 2016 19:23:49 +0000 (UTC)
commit ef3b4cbf3ea7684410cdd00a7de373927886de51
Author: Joanmarie Diggs <jdiggs igalia com>
Date: Tue Feb 16 14:13:52 2016 -0500
Initial commit of the sound support - not yet hooked up
* Created formatting strings for sound generation
* Created sound generator for the default script and web script
* Added support to play sound icons (via gstreamer)
* Added support to generate and play tones (via gstreamer)
* Added settings for presentation of role, state, position, and value
Note: I attempted to create the progress bar beep Tone based on Chrys'
patch, but it doesn't sound right. Once he fixes that, progress bar
beeps will be hooked up.
Once I finish testing the sound icon support, it will be hooked up as
an "experimental" feature to be refined and GUI-ified during the 3.21/22
cycle.
src/orca/Makefile.am | 2 +
src/orca/formatting.py | 138 ++++++++++++-
src/orca/object_properties.py | 12 +
src/orca/orca.py | 9 +
src/orca/script.py | 6 +
src/orca/scripts/web/Makefile.am | 1 +
src/orca/scripts/web/script.py | 6 +
src/orca/scripts/web/sound_generator.py | 124 +++++++++++
src/orca/settings.py | 14 ++
src/orca/settings_manager.py | 3 +
src/orca/sound.py | 153 ++++++++++++++
src/orca/sound_generator.py | 349 +++++++++++++++++++++++++++++++
12 files changed, 816 insertions(+), 1 deletions(-)
---
diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am
index a2ad57e..e6f0cfb 100644
--- a/src/orca/Makefile.am
+++ b/src/orca/Makefile.am
@@ -59,6 +59,8 @@ orca_python_PYTHON = \
script_utilities.py \
settings.py \
settings_manager.py \
+ sound.py \
+ sound_generator.py \
speech.py \
spellcheck.py \
speechdispatcherfactory.py \
diff --git a/src/orca/formatting.py b/src/orca/formatting.py
index 61ca27a..7fe8cc7 100644
--- a/src/orca/formatting.py
+++ b/src/orca/formatting.py
@@ -79,6 +79,19 @@ formatting = {
'nodelevel': object_properties.NODE_LEVEL_BRAILLE,
'nestinglevel': object_properties.NESTING_LEVEL_BRAILLE,
},
+ 'sound': {
+ 'required': object_properties.STATE_REQUIRED_SOUND,
+ 'readonly': object_properties.STATE_READ_ONLY_SOUND,
+ 'insensitive': object_properties.STATE_INSENSITIVE_SOUND,
+ 'checkbox': object_properties.CHECK_BOX_INDICATORS_SOUND,
+ 'radiobutton': object_properties.RADIO_BUTTON_INDICATORS_SOUND,
+ 'togglebutton': object_properties.TOGGLE_BUTTON_INDICATORS_SOUND,
+ 'expansion': object_properties.EXPANSION_INDICATORS_SOUND,
+ 'multiselect': object_properties.STATE_MULTISELECT_SOUND,
+ 'clickable': object_properties.STATE_CLICKABLE_SOUND,
+ 'haslongdesc': object_properties.STATE_HAS_LONGDESC_SOUND,
+ 'visited': object_properties.STATE_VISITED_SOUND,
+ },
},
####################################################################
@@ -693,7 +706,130 @@ formatting = {
#pyatspi.ROLE_TREE: 'default'
#pyatspi.ROLE_TREE_TABLE: 'default'
#pyatspi.ROLE_WINDOW: 'default'
- }
+ },
+
+ ####################################################################
+ # #
+ # Formatting for sound. #
+ # #
+ ####################################################################
+
+ 'sound': {
+ 'prefix': {
+ 'focused': '[]',
+ 'unfocused': '[]',
+ 'basicWhereAmI': '[]',
+ 'detailedWhereAmI': '[]'
+ },
+ 'suffix': {
+ 'focused': '[]',
+ 'unfocused': 'clickable + hasLongDesc',
+ 'basicWhereAmI': '[]',
+ 'detailedWhereAmI': '[]'
+ },
+ 'default': {
+ 'focused': '[]',
+ 'unfocused': 'roleName',
+ 'basicWhereAmI': '[]',
+ 'detailedWhereAmI': '[]'
+ },
+ pyatspi.ROLE_CANVAS: {
+ 'unfocused': 'roleName + positionInSet',
+ },
+ pyatspi.ROLE_CHECK_BOX: {
+ 'focused': 'checkedState',
+ 'unfocused': 'roleName + checkedState + required + availability',
+ },
+ pyatspi.ROLE_CHECK_MENU_ITEM: {
+ 'focused': 'checkedState',
+ 'unfocused': 'roleName + checkedState + availability + positionInSet',
+ },
+ pyatspi.ROLE_COMBO_BOX: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + positionInSet',
+ },
+ pyatspi.ROLE_DIAL: {
+ 'focused': 'percentage',
+ 'unfocused': 'roleName + percentage + required + availability',
+ },
+ pyatspi.ROLE_ENTRY: {
+ 'unfocused': 'roleName + readOnly + required + availability',
+ },
+ pyatspi.ROLE_HEADING: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + expandableState',
+ },
+ pyatspi.ROLE_ICON: {
+ 'unfocused': 'roleName + positionInSet',
+ },
+ pyatspi.ROLE_LINK: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + visitedState + expandableState',
+ },
+ pyatspi.ROLE_LIST: {
+ 'unfocused': 'roleName + multiselectableState',
+ },
+ pyatspi.ROLE_LIST_BOX: {
+ 'unfocused': 'roleName + multiselectableState',
+ },
+ pyatspi.ROLE_LIST_ITEM: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + expandableState + positionInSet',
+ },
+ pyatspi.ROLE_MENU_ITEM: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + expandableState + availability + positionInSet',
+ },
+ pyatspi.ROLE_PAGE_TAB: {
+ 'unfocused': 'roleName + positionInSet',
+ },
+ pyatspi.ROLE_PROGRESS_BAR: {
+ 'focused': 'progressBarValue',
+ 'unfocused': 'roleName + progressBarValue'
+ },
+ pyatspi.ROLE_PUSH_BUTTON: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + expandableState + availability',
+ },
+ pyatspi.ROLE_RADIO_BUTTON: {
+ 'focused': 'radioState',
+ 'unfocused': 'roleName + radioState + availability + positionInSet',
+ },
+ pyatspi.ROLE_RADIO_MENU_ITEM: {
+ 'focused': 'radioState',
+ 'unfocused': 'roleName + checkedState + availability + positionInSet',
+ },
+ pyatspi.ROLE_SCROLL_BAR: {
+ 'focused': 'percentage',
+ 'unfocused': 'roleName + percentage',
+ },
+ pyatspi.ROLE_SLIDER: {
+ 'focused': 'percentage',
+ 'unfocused': 'roleName + percentage + required + availability',
+ },
+ pyatspi.ROLE_SPIN_BUTTON: {
+ 'focused': 'percentage',
+ 'unfocused': 'roleName + availability + percentage + required',
+ },
+ pyatspi.ROLE_SPLIT_PANE: {
+ 'focused': 'percentage',
+ 'unfocused': 'roleName + percentage + availability',
+ },
+ pyatspi.ROLE_TABLE_CELL: {
+ 'focused': 'expandableState',
+ 'unfocused': 'roleName + expandableState',
+ },
+ pyatspi.ROLE_TABLE_ROW: {
+ 'focused': 'expandableState',
+ },
+ pyatspi.ROLE_TEXT: {
+ 'unfocused': 'roleName + readOnly + required + availability',
+ },
+ pyatspi.ROLE_TOGGLE_BUTTON: {
+ 'focused': 'expandableState or toggleState',
+ 'unfocused': 'roleName + (expandableState or toggleState) + availability',
+ },
+ },
}
class Formatting(dict):
diff --git a/src/orca/object_properties.py b/src/orca/object_properties.py
index 5626825..7c010e4 100644
--- a/src/orca/object_properties.py
+++ b/src/orca/object_properties.py
@@ -197,3 +197,15 @@ TOGGLE_BUTTON_INDICATORS_BRAILLE = ["& y", "&=y"]
TABLE_CELL_DELIMITER_BRAILLE = " "
EOL_INDICATOR_BRAILLE = " $l"
+
+CHECK_BOX_INDICATORS_SOUND = ["not_checked", "checked", "partially_checked"]
+EXPANSION_INDICATORS_SOUND = ["collapsed", "expanded"]
+RADIO_BUTTON_INDICATORS_SOUND = ["unselected", "selected"]
+TOGGLE_BUTTON_INDICATORS_SOUND = ["not_pressed", "pressed"]
+STATE_CLICKABLE_SOUND = "clickable"
+STATE_HAS_LONGDESC_SOUND = "haslongdesc"
+STATE_INSENSITIVE_SOUND = "insensitive"
+STATE_MULTISELECT_SOUND = "multiselect"
+STATE_READ_ONLY_SOUND = "readonly"
+STATE_REQUIRED_SOUND = "required"
+STATE_VISITED_SOUND = "visited"
diff --git a/src/orca/orca.py b/src/orca/orca.py
index 98017a0..632c0ed 100644
--- a/src/orca/orca.py
+++ b/src/orca/orca.py
@@ -76,6 +76,7 @@ from . import script_manager
from . import settings
from . import settings_manager
from . import speech
+from . import sound
from .input_event import BrailleEvent
from .input_event import KeyboardEvent
@@ -412,6 +413,8 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
# Shutdown the output drivers and give them a chance to die.
+ player = sound.getPlayer()
+ player.shutdown()
speech.shutdown()
braille.shutdown()
@@ -461,6 +464,9 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
msg = 'ORCA: Could not initialize connection to braille.'
debug.println(debug.LEVEL_WARNING, msg, True)
+ if _settingsManager.getSetting('enableSound'):
+ player.init()
+
# I'm not sure where else this should go. But it doesn't really look
# right here.
try:
@@ -700,6 +706,9 @@ def shutdown(script=None, inputEvent=None):
speech.shutdown()
if settings.enableBraille:
braille.shutdown()
+ if settings.enableSound:
+ player = sound.getPlayer()
+ player.shutdown()
if settings.timeoutCallback and (settings.timeoutTime > 0):
signal.alarm(0)
diff --git a/src/orca/script.py b/src/orca/script.py
index e4abebd..f31ceff 100644
--- a/src/orca/script.py
+++ b/src/orca/script.py
@@ -53,6 +53,7 @@ from . import script_manager
from . import script_utilities
from . import settings
from . import settings_manager
+from . import sound_generator
from . import speech_generator
from . import structural_navigation
from . import where_am_I
@@ -111,6 +112,7 @@ class Script:
self.formatting = self.getFormatting()
self.brailleGenerator = self.getBrailleGenerator()
+ self.soundGenerator = self.getSoundGenerator()
self.speechGenerator = self.getSpeechGenerator()
self.generatorCache = {}
self.eventCache = {}
@@ -207,6 +209,10 @@ class Script:
"""
return braille_generator.BrailleGenerator(self)
+ def getSoundGenerator(self):
+ """Returns the sound generator for this script."""
+ return sound_generator.SoundGenerator(self)
+
def getSpeechGenerator(self):
"""Returns the speech generator for this script.
"""
diff --git a/src/orca/scripts/web/Makefile.am b/src/orca/scripts/web/Makefile.am
index e74d1d7..d5cf8d7 100644
--- a/src/orca/scripts/web/Makefile.am
+++ b/src/orca/scripts/web/Makefile.am
@@ -4,6 +4,7 @@ orca_python_PYTHON = \
braille_generator.py \
script.py \
script_utilities.py \
+ sound_generator.py \
speech_generator.py \
tutorial_generator.py
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index f787731..4a53d5e 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -53,6 +53,7 @@ from orca.scripts import default
from .bookmarks import Bookmarks
from .braille_generator import BrailleGenerator
+from .sound_generator import SoundGenerator
from .speech_generator import SpeechGenerator
from .tutorial_generator import TutorialGenerator
from .script_utilities import Utilities
@@ -262,6 +263,11 @@ class Script(default.Script):
return liveregions.LiveRegionManager(self)
+ def getSoundGenerator(self):
+ """Returns the sound generator for this script."""
+
+ return SoundGenerator(self)
+
def getSpeechGenerator(self):
"""Returns the speech generator for this script."""
diff --git a/src/orca/scripts/web/sound_generator.py b/src/orca/scripts/web/sound_generator.py
new file mode 100644
index 0000000..ca381ff
--- /dev/null
+++ b/src/orca/scripts/web/sound_generator.py
@@ -0,0 +1,124 @@
+# Orca
+#
+# Copyright 2016 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA 02110-1301 USA.
+
+"""Utilities for obtaining sounds to be presented for objects."""
+
+__id__ = "$Id:$"
+__version__ = "$Revision:$"
+__date__ = "$Date:$"
+__copyright__ = "Copyright (c) 2016 Igalia, S.L."
+__license__ = "LGPL"
+
+import pyatspi
+
+from orca import settings_manager
+from orca import sound_generator
+
+_settingsManager = settings_manager.getManager()
+
+
+class SoundGenerator(sound_generator.SoundGenerator):
+
+ def __init__(self, script):
+ super().__init__(script)
+
+ def _generateClickable(self, obj, **args):
+ """Returns an array of sounds indicating obj is clickable."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ if not self._script.utilities.inDocumentContent(obj):
+ return []
+
+ if not args.get('mode', None):
+ args['mode'] = self._mode
+
+ args['stringType'] = 'clickable'
+ if self._script.utilities.isClickableElement(obj):
+ filenames = [self._script.formatting.getString(**args)]
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateHasLongDesc(self, obj, **args):
+ """Returns an array of sounds indicating obj has a longdesc."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ if not self._script.utilities.inDocumentContent(obj):
+ return []
+
+ if not args.get('mode', None):
+ args['mode'] = self._mode
+
+ args['stringType'] = 'haslongdesc'
+ if self._script.utilities.hasLongDesc(obj):
+ filenames = [self._script.formatting.getString(**args)]
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def generateSound(self, obj, **args):
+ """Returns an array of sounds for the complete presentation of obj."""
+
+ if not self._script.utilities.inDocumentContent(obj):
+ return super().generateSound(obj, **args)
+
+ result = []
+ if args.get('formatType') == 'detailedWhereAmI':
+ oldRole = self._overrideRole('default', args)
+ elif self._script.utilities.isLink(obj):
+ oldRole = self._overrideRole(pyatspi.ROLE_LINK, args)
+ elif self._script.utilities.isAnchor(obj):
+ oldRole = 'ROLE_STATIC'
+ elif self._script.utilities.treatAsDiv(obj):
+ oldRole = self._overrideRole(pyatspi.ROLE_SECTION, args)
+ else:
+ oldRole = self._overrideRole(self._getAlternativeRole(obj, **args), args)
+
+ result.extend(super().generateSound(obj, **args))
+ result = list(filter(lambda x: x, result))
+ self._restoreRole(oldRole, args)
+
+ return result
+
+ def generateContents(self, contents, **args):
+ """Returns an array of an array of sounds for the contents."""
+
+ if not len(contents):
+ return []
+
+ result = []
+ contents = self._script.utilities.filterContentsForPresentation(contents, False)
+ for i, content in enumerate(contents):
+ obj, start, end, string = content
+ icon = self.generateSound(
+ obj, startOffset=start, endOffset=end, string=string,
+ index=i, total=len(contents), **args)
+ result.append(icon)
+
+ return result
diff --git a/src/orca/settings.py b/src/orca/settings.py
index f30964e..855fdc2 100644
--- a/src/orca/settings.py
+++ b/src/orca/settings.py
@@ -69,6 +69,12 @@ userCustomizableSettings = [
"brailleSelectorIndicator",
"brailleLinkIndicator",
"brailleAlignmentStyle",
+ "enableSound",
+ "soundVolume",
+ "playSoundForRole",
+ "playSoundForState",
+ "playSoundForPositionInSet",
+ "playSoundForValue",
"enableBrailleMonitor",
"verbalizePunctuationStyle",
"presentToolTips",
@@ -246,6 +252,14 @@ brailleAlignmentStyle = BRAILLE_ALIGN_BY_EDGE
brailleAlignmentMargin = 3
brailleMaximumJump = 8
+# Sound
+enableSound = True
+soundVolume = 0.5
+playSoundForRole = False
+playSoundForState = False
+playSoundForPositionInSet = False
+playSoundForValue = False
+
# Keyboard and Echo
keyboardLayout = GENERAL_KEYBOARD_LAYOUT_DESKTOP
orcaModifierKeys = DESKTOP_MODIFIER_KEYS
diff --git a/src/orca/settings_manager.py b/src/orca/settings_manager.py
index c602392..ff8488e 100644
--- a/src/orca/settings_manager.py
+++ b/src/orca/settings_manager.py
@@ -183,6 +183,9 @@ class SettingsManager(object):
orcaSettingsDir = os.path.join(orcaDir, "app-settings")
_createDir(orcaSettingsDir)
+ orcaSoundsDir = os.path.join(orcaDir, "sounds")
+ _createDir(orcaSoundsDir)
+
# Set up $XDG_DATA_HOME/orca/orca-customizations.py empty file and
# define orcaDir as a Python package.
initFile = os.path.join(orcaDir, "__init__.py")
diff --git a/src/orca/sound.py b/src/orca/sound.py
new file mode 100644
index 0000000..7df0cb6
--- /dev/null
+++ b/src/orca/sound.py
@@ -0,0 +1,153 @@
+# Orca
+#
+# Copyright 2016 Orca Team.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA 02110-1301 USA.
+
+"""Utilities for playing sounds."""
+
+__id__ = "$Id:$"
+__version__ = "$Revision:$"
+__date__ = "$Date:$"
+__copyright__ = "Copyright (c) 2016 Orca Team"
+__license__ = "LGPL"
+
+import gi
+from gi.repository import GLib
+
+try:
+ gi.require_version('Gst', '1.0')
+ from gi.repository import Gst
+except:
+ _gstreamerAvailable = False
+else:
+ _gstreamerAvailable, args = Gst.init_check()
+
+from . import debug
+from .sound_generator import Icon, Tone
+
+class Player:
+ """Plays Icons and Tones."""
+
+ def __init__(self):
+ self._initialized = False
+
+ if not _gstreamerAvailable:
+ msg = 'SOUND ERROR: Gstreamer is not available'
+ debug.println(debug.LEVEL_INFO, mSG, True)
+ return
+
+ self.init()
+
+ def _onPlayerMessage(self, bus, message):
+ if message.type == Gst.MessageType.EOS:
+ self._player.set_state(Gst.State.NULL)
+ elif message.type == Gst.MessageType.ERROR:
+ self._player.set_state(Gst.State.NULL)
+ error, info = message.parse_error()
+ msg = 'SOUND ERROR: %s' % error
+ debug.println(debug.LEVEL_INFO, msg, True)
+
+ def _onPipelineMessage(self, bus, message):
+ if message.type == Gst.MessageType.EOS:
+ self._pipeline.set_state(Gst.State.NULL)
+ elif message.type == Gst.MessageType.ERROR:
+ self._pipeline.set_state(Gst.State.NULL)
+ error, info = message.parse_error()
+ msg = 'SOUND ERROR: %s' % error
+ debug.println(debug.LEVEL_INFO, msg, True)
+
+ def _playIcon(self, icon, interrupt=True):
+ """Plays a sound icon, interrupting the current play first unless specified."""
+
+ if interrupt:
+ self._player.set_state(Gst.State.NULL)
+
+ self._player.set_property('uri', 'file://%s' % icon.path)
+ self._player.set_state(Gst.State.PLAYING)
+
+ def _playTone(self, tone, interrupt=True):
+ """Plays a tone, interrupting the current play first unless specified."""
+
+ if interrupt:
+ self._pipeline.set_state(Gst.State.NULL)
+
+ self._source.set_property('volume', tone.volume)
+ self._source.set_property('freq', tone.frequency)
+ self._source.set_property('wave', tone.wave)
+ self._pipeline.set_state(Gst.State.PLAYING)
+ duration = int(1000 * tone.duration)
+ GLib.timeout_add(duration, self._pipeline.set_state, Gst.State.NULL)
+
+ def init(self):
+ """(Re)Initializes the Player."""
+
+ if self._initialized:
+ return
+
+ self._player = Gst.ElementFactory.make('playbin', 'player')
+ bus = self._player.get_bus()
+ bus.add_signal_watch()
+ bus.connect("message", self._onPlayerMessage)
+
+ self._pipeline = Gst.Pipeline(name='orca-pipeline')
+ bus = self._pipeline.get_bus()
+ bus.add_signal_watch()
+ bus.connect("message", self._onPipelineMessage)
+
+ self._source = Gst.ElementFactory.make('audiotestsrc', 'src')
+ self._sink = Gst.ElementFactory.make('autoaudiosink', 'output')
+ self._pipeline.add(self._source)
+ self._pipeline.add(self._sink)
+ self._source.link(self._sink)
+
+ self._initialized = True
+
+ def play(self, item, interrupt=True):
+ """Plays a sound, interrupting the current play first unless specified."""
+
+ if isinstance(item, Icon):
+ self._playIcon(item, interrupt)
+ elif isinstance(item, Tone):
+ self._playTone(item, interrupt)
+ else:
+ msg = 'SOUND ERROR: %s is not an Icon or Tone' % item
+ debug.println(debug.LEVEL_INFO, msg, True)
+
+ def stop(self):
+ """Stops play."""
+
+ if not _gstreamerAvailable:
+ return
+
+ self._player.set_state(Gst.State.NULL)
+ self._pipeline.set_state(Gst.State.NULL)
+
+ def shutdown(self):
+ """Shuts down the sound utilities."""
+
+ global _gstreamerAvailable
+ if not _gstreamerAvailable:
+ return
+
+ self.stop()
+ self._initialized = False
+ _gstreamerAvailable = False
+
+_player = Player()
+
+def getPlayer():
+ return _player
diff --git a/src/orca/sound_generator.py b/src/orca/sound_generator.py
new file mode 100644
index 0000000..9356deb
--- /dev/null
+++ b/src/orca/sound_generator.py
@@ -0,0 +1,349 @@
+# Orca
+#
+# Copyright 2016 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA 02110-1301 USA.
+
+"""Utilities for obtaining sounds to be presented for objects."""
+
+__id__ = "$Id:$"
+__version__ = "$Revision:$"
+__date__ = "$Date:$"
+__copyright__ = "Copyright (c) 2016 Igalia, S.L."
+__license__ = "LGPL"
+
+import gi
+gi.require_version('Atspi', '2.0')
+from gi.repository import Atspi
+
+import os
+import pyatspi
+
+from . import generator
+from . import settings_manager
+from . import sound_icons
+
+_settingsManager = settings_manager.getManager()
+
+METHOD_PREFIX = "_generate"
+
+
+class Icon:
+ """Sound file representing a particular aspect of an object."""
+
+ def __init__(self, location, filename):
+ self.path = os.path.join(location, filename)
+
+ def __str__(self):
+ return 'Icon(path: %s, isValid: %s)' % (self.path, self.isValid())
+
+ def isValid(self):
+ return os.path.isfile(self.path)
+
+class Tone:
+ """Tone representing a particular aspect of an object."""
+
+ SINE_WAVE = 0
+ SQUARE_WAVE = 1
+ SAW_WAVE = 2
+ TRIANGLE_WAVE = 3
+ SILENCE = 4
+ WHITE_UNIFORM_NOISE = 5
+ PINK_NOISE = 6
+ SINE_WAVE_USING_TABLE = 7
+ PERIODIC_TICKS = 8
+ WHITE_GAUSSIAN_NOISE = 9
+ RED_NOISE = 10
+ INVERTED_PINK_NOISE = 11
+ INVERTED_RED_NOISE = 12
+
+ def __init__(self, duration, frequency, volumeMultiplier=1, wave=SINE_WAVE):
+ self.duration = duration
+ self.frequency = min(max(0, frequency), 20000)
+ self.volume = _settingsManager.getSetting('soundVolume') * volumeMultiplier
+ self.wave = wave
+
+ def __str__(self):
+ return 'Tone(duration: %s, frequency: %s, volume: %s, wave: %s)' \
+ % (self.duration, self.frequency, self.volume, self.wave)
+
+class SoundGenerator(generator.Generator):
+ """Takes accessible objects and produces the sound(s) to be played."""
+
+ def __init__(self, script):
+ super().__init__(script, 'sound')
+ self._sounds = os.path.join(_settingsManager.getPrefsDir(), 'sounds')
+
+ def _convertFilenameToIcon(self, filename):
+ icon = Icon(self._sounds, filename)
+ if icon.isValid():
+ return icon
+
+ return None
+
+ def generateSound(self, obj, **args):
+ """Returns an array of sounds for the complete presentation of obj."""
+
+ return self.generate(obj, **args)
+
+ #####################################################################
+ # #
+ # State information #
+ # #
+ #####################################################################
+
+ def _generateAvailability(self, obj, **args):
+ """Returns an array of sounds indicating obj is grayed out."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateAvailability(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateCheckedState(self, obj, **args):
+ """Returns an array of sounds indicating the checked state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateCheckedState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateClickable(self, obj, **args):
+ """Returns an array of sounds indicating obj is clickable."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateClickable(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateExpandableState(self, obj, **args):
+ """Returns an array of sounds indicating the expanded state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateExpandableState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateHasLongDesc(self, obj, **args):
+ """Returns an array of sounds indicating obj has a longdesc."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateHasLongDesc(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateMenuItemCheckedState(self, obj, **args):
+ """Returns an array of sounds indicating the checked state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateMenuItemCheckedState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateMultiselectableState(self, obj, **args):
+ """Returns an array of sounds indicating obj is multiselectable."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateMultiselectableState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateRadioState(self, obj, **args):
+ """Returns an array of sounds indicating the selected state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateRadioState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateReadOnly(self, obj, **args):
+ """Returns an array of sounds indicating obj is read only."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateReadOnly(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateRequired(self, obj, **args):
+ """Returns an array of sounds indicating obj is required."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateRequired(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateToggleState(self, obj, **args):
+ """Returns an array of sounds indicating the toggled state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ filenames = super()._generateToggleState(obj, **args)
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ def _generateVisitedState(self, obj, **args):
+ """Returns an array of sounds indicating the visited state of obj."""
+
+ if not _settingsManager.getSetting('playSoundForState'):
+ return []
+
+ if not args.get('mode', None):
+ args['mode'] = self._mode
+
+ args['stringType'] = 'visited'
+ if obj.getState().contains(pyatspi.STATE_VISITED):
+ filenames = [self._script.formatting.getString(**args)]
+ result = list(map(self._convertFilenameToIcon, filenames))
+ if result:
+ return result
+
+ return []
+
+ #####################################################################
+ # #
+ # Value interface information #
+ # #
+ #####################################################################
+
+ def _generatePercentage(self, obj, **args):
+ """Returns an array of sounds reflecting the percentage of obj."""
+
+ if not _settingsManager.getSetting('playSoundForValue'):
+ return []
+
+ percent = self._script.utilities.getValueAsPercent(obj)
+ if percent is None:
+ return []
+
+ return []
+
+ def _generateProgressBarValue(self, obj, **args):
+ """Returns an array of sounds representing the progress bar value."""
+
+ if args.get('isProgressBarUpdate') \
+ and not _settingsManager.getSetting('beepProgressBarUpdates'):
+ return []
+
+ if not _settingsManager.getSetting('playSoundForValue'):
+ return []
+
+ percent = self._script.utilities.getValueAsPercent(obj)
+ if percent is None:
+ return []
+
+ # To better indicate the progress completion.
+ if percent >= 99:
+ duration = 1
+ else:
+ duration = 0.075
+
+ # Reduce volume as pitch increases.
+ volumeMultiplier = 1 - (percent / 120)
+
+ # Adjusting so that the initial beeps are not too deep.
+ if percent < 7:
+ frequency = int(98 + percent * 5.4)
+ else:
+ frequency = int(percent * 22)
+
+ return [Tone(duration, frequency, volumeMultiplier, Tone.SINE_WAVE)]
+
+ #####################################################################
+ # #
+ # Role and hierarchical information #
+ # #
+ #####################################################################
+
+ def _generatePositionInSet(self, obj, **args):
+ """Returns an array of sounds reflecting the set position of obj."""
+
+ if not _settingsManager.getSetting('playSoundForPositionInSet'):
+ return []
+
+ position, setSize = self._script.utilities.getPositionAndSetSize(obj)
+ percent = int((position / setSize) * 100)
+
+ return []
+
+ def _generateRoleName(self, obj, **args):
+ """Returns an array of sounds indicating the role of obj."""
+
+ if not _settingsManager.getSetting('playSoundForRole'):
+ return []
+
+ role = args.get('role', obj.getRole())
+ filename = Atspi.role_get_name(role).replace(' ', '_')
+ result = self._convertFilenameToIcon(filename)
+ if result:
+ return [result]
+
+ return []
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]