[orca] Initial commit of the sound support - not yet hooked up



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]