1# Orca
2#
3# Copyright 2016 Igalia, S.L.
4#
5# Author: Joanmarie Diggs <jdiggs@igalia.com>
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2.1 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, write to the
19# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
20# Boston MA  02110-1301 USA.
21
22"""Utilities for obtaining sounds to be presented for objects."""
23
24__id__        = "$Id:$"
25__version__   = "$Revision:$"
26__date__      = "$Date:$"
27__copyright__ = "Copyright (c) 2016 Igalia, S.L."
28__license__   = "LGPL"
29
30import gi
31gi.require_version('Atspi', '2.0')
32from gi.repository import Atspi
33
34import os
35import pyatspi
36
37from . import generator
38from . import settings_manager
39
40_settingsManager = settings_manager.getManager()
41
42METHOD_PREFIX = "_generate"
43
44
45class Icon:
46    """Sound file representing a particular aspect of an object."""
47
48    def __init__(self, location, filename):
49        self.path = os.path.join(location, filename)
50
51    def __str__(self):
52        return 'Icon(path: %s, isValid: %s)' % (self.path, self.isValid())
53
54    def isValid(self):
55        return os.path.isfile(self.path)
56
57class Tone:
58    """Tone representing a particular aspect of an object."""
59
60    SINE_WAVE = 0
61    SQUARE_WAVE = 1
62    SAW_WAVE = 2
63    TRIANGLE_WAVE = 3
64    SILENCE = 4
65    WHITE_UNIFORM_NOISE = 5
66    PINK_NOISE = 6
67    SINE_WAVE_USING_TABLE = 7
68    PERIODIC_TICKS = 8
69    WHITE_GAUSSIAN_NOISE = 9
70    RED_NOISE = 10
71    INVERTED_PINK_NOISE = 11
72    INVERTED_RED_NOISE = 12
73
74    def __init__(self, duration, frequency, volumeMultiplier=1, wave=SINE_WAVE):
75        self.duration = duration
76        self.frequency = min(max(0, frequency), 20000)
77        self.volume = _settingsManager.getSetting('soundVolume') * volumeMultiplier
78        self.wave = wave
79
80    def __str__(self):
81        return 'Tone(duration: %s, frequency: %s, volume: %s, wave: %s)' \
82            % (self.duration, self.frequency, self.volume, self.wave)
83
84class SoundGenerator(generator.Generator):
85    """Takes accessible objects and produces the sound(s) to be played."""
86
87    def __init__(self, script):
88        super().__init__(script, 'sound')
89        self._sounds = os.path.join(_settingsManager.getPrefsDir(), 'sounds')
90
91    def _convertFilenameToIcon(self, filename):
92        icon = Icon(self._sounds, filename)
93        if icon.isValid():
94            return icon
95
96        return None
97
98    def generateSound(self, obj, **args):
99        """Returns an array of sounds for the complete presentation of obj."""
100
101        return self.generate(obj, **args)
102
103    #####################################################################
104    #                                                                   #
105    # State information                                                 #
106    #                                                                   #
107    #####################################################################
108
109    def _generateAvailability(self, obj, **args):
110        """Returns an array of sounds indicating obj is grayed out."""
111
112        if not _settingsManager.getSetting('playSoundForState'):
113            return []
114
115        filenames = super()._generateAvailability(obj, **args)
116        result = list(map(self._convertFilenameToIcon, filenames))
117        if result:
118            return result
119
120        return []
121
122    def _generateCheckedState(self, obj, **args):
123        """Returns an array of sounds indicating the checked state of obj."""
124
125        if not _settingsManager.getSetting('playSoundForState'):
126            return []
127
128        filenames = super()._generateCheckedState(obj, **args)
129        result = list(map(self._convertFilenameToIcon, filenames))
130        if result:
131            return result
132
133        return []
134
135    def _generateClickable(self, obj, **args):
136        """Returns an array of sounds indicating obj is clickable."""
137
138        if not _settingsManager.getSetting('playSoundForState'):
139            return []
140
141        filenames = super()._generateClickable(obj, **args)
142        result = list(map(self._convertFilenameToIcon, filenames))
143        if result:
144            return result
145
146        return []
147
148    def _generateExpandableState(self, obj, **args):
149        """Returns an array of sounds indicating the expanded state of obj."""
150
151        if not _settingsManager.getSetting('playSoundForState'):
152            return []
153
154        filenames = super()._generateExpandableState(obj, **args)
155        result = list(map(self._convertFilenameToIcon, filenames))
156        if result:
157            return result
158
159        return []
160
161    def _generateHasLongDesc(self, obj, **args):
162        """Returns an array of sounds indicating obj has a longdesc."""
163
164        if not _settingsManager.getSetting('playSoundForState'):
165            return []
166
167        filenames = super()._generateHasLongDesc(obj, **args)
168        result = list(map(self._convertFilenameToIcon, filenames))
169        if result:
170            return result
171
172        return []
173
174    def _generateMenuItemCheckedState(self, obj, **args):
175        """Returns an array of sounds indicating the checked state of obj."""
176
177        if not _settingsManager.getSetting('playSoundForState'):
178            return []
179
180        filenames = super()._generateMenuItemCheckedState(obj, **args)
181        result = list(map(self._convertFilenameToIcon, filenames))
182        if result:
183            return result
184
185        return []
186
187    def _generateMultiselectableState(self, obj, **args):
188        """Returns an array of sounds indicating obj is multiselectable."""
189
190        if not _settingsManager.getSetting('playSoundForState'):
191            return []
192
193        filenames = super()._generateMultiselectableState(obj, **args)
194        result = list(map(self._convertFilenameToIcon, filenames))
195        if result:
196            return result
197
198        return []
199
200    def _generateRadioState(self, obj, **args):
201        """Returns an array of sounds indicating the selected state of obj."""
202
203        if not _settingsManager.getSetting('playSoundForState'):
204            return []
205
206        filenames = super()._generateRadioState(obj, **args)
207        result = list(map(self._convertFilenameToIcon, filenames))
208        if result:
209            return result
210
211        return []
212
213    def _generateReadOnly(self, obj, **args):
214        """Returns an array of sounds indicating obj is read only."""
215
216        if not _settingsManager.getSetting('playSoundForState'):
217            return []
218
219        filenames = super()._generateReadOnly(obj, **args)
220        result = list(map(self._convertFilenameToIcon, filenames))
221        if result:
222            return result
223
224        return []
225
226    def _generateRequired(self, obj, **args):
227        """Returns an array of sounds indicating obj is required."""
228
229        if not _settingsManager.getSetting('playSoundForState'):
230            return []
231
232        filenames = super()._generateRequired(obj, **args)
233        result = list(map(self._convertFilenameToIcon, filenames))
234        if result:
235            return result
236
237        return []
238
239    def _generateSwitchState(self, obj, **args):
240        """Returns an array of sounds indicating the on/off state of obj."""
241
242        if not _settingsManager.getSetting('playSoundForState'):
243            return []
244
245        filenames = super()._generateSwitchState(obj, **args)
246        result = list(map(self._convertFilenameToIcon, filenames))
247        if result:
248            return result
249
250        return []
251
252    def _generateToggleState(self, obj, **args):
253        """Returns an array of sounds indicating the toggled state of obj."""
254
255        if not _settingsManager.getSetting('playSoundForState'):
256            return []
257
258        filenames = super()._generateToggleState(obj, **args)
259        result = list(map(self._convertFilenameToIcon, filenames))
260        if result:
261            return result
262
263        return []
264
265    def _generateVisitedState(self, obj, **args):
266        """Returns an array of sounds indicating the visited state of obj."""
267
268        if not _settingsManager.getSetting('playSoundForState'):
269            return []
270
271        if not args.get('mode', None):
272            args['mode'] = self._mode
273
274        args['stringType'] = 'visited'
275        if obj.getState().contains(pyatspi.STATE_VISITED):
276            filenames = [self._script.formatting.getString(**args)]
277            result = list(map(self._convertFilenameToIcon, filenames))
278            if result:
279                return result
280
281        return []
282
283    #####################################################################
284    #                                                                   #
285    # Value interface information                                       #
286    #                                                                   #
287    #####################################################################
288
289    def _generatePercentage(self, obj, **args):
290        """Returns an array of sounds reflecting the percentage of obj."""
291
292        if not _settingsManager.getSetting('playSoundForValue'):
293            return []
294
295        percent = self._script.utilities.getValueAsPercent(obj)
296        if percent is None:
297            return []
298
299        return []
300
301    def _generateProgressBarValue(self, obj, **args):
302        """Returns an array of sounds representing the progress bar value."""
303
304        if args.get('isProgressBarUpdate'):
305            if not self._shouldPresentProgressBarUpdate(obj, **args):
306                return []
307        elif not _settingsManager.getSetting('playSoundForValue'):
308            return []
309
310        percent = self._script.utilities.getValueAsPercent(obj)
311        if percent is None:
312            return []
313
314        # To better indicate the progress completion.
315        if percent >= 99:
316            duration = 1
317        else:
318            duration = 0.075
319
320        # Reduce volume as pitch increases.
321        volumeMultiplier = 1 - (percent / 120)
322
323        # Adjusting so that the initial beeps are not too deep.
324        if percent < 7:
325            frequency = int(98 + percent * 5.4)
326        else:
327            frequency = int(percent * 22)
328
329        return [Tone(duration, frequency, volumeMultiplier, Tone.SINE_WAVE)]
330
331    def _getProgressBarUpdateInterval(self):
332        interval = _settingsManager.getSetting('progressBarBeepInterval')
333        if interval is None:
334            return super()._getProgressBarUpdateInterval()
335
336        return int(interval)
337
338    def _shouldPresentProgressBarUpdate(self, obj, **args):
339        if not _settingsManager.getSetting('beepProgressBarUpdates'):
340            return False
341
342        return super()._shouldPresentProgressBarUpdate(obj, **args)
343
344    #####################################################################
345    #                                                                   #
346    # Role and hierarchical information                                 #
347    #                                                                   #
348    #####################################################################
349
350    def _generatePositionInSet(self, obj, **args):
351        """Returns an array of sounds reflecting the set position of obj."""
352
353        if not _settingsManager.getSetting('playSoundForPositionInSet'):
354            return []
355
356        position, setSize = self._script.utilities.getPositionAndSetSize(obj)
357        percent = int((position / setSize) * 100)
358
359        return []
360
361    def _generateRoleName(self, obj, **args):
362        """Returns an array of sounds indicating the role of obj."""
363
364        if not _settingsManager.getSetting('playSoundForRole'):
365            return []
366
367        role = args.get('role', obj.getRole())
368        filename = Atspi.role_get_name(role).replace(' ', '_')
369        result = self._convertFilenameToIcon(filename)
370        if result:
371            return [result]
372
373        return []
374