Ticket #279: virtualSynthDriver.py

File virtualSynthDriver.py, 9.9 KB (added by TimothyLee, 3 years ago)

Updated VirtualSynthDriver class with support for "Say All" command

Line 
1#virtualSynthDriver.py
2#A part of NonVisual Desktop Access (NVDA)
3#This file is covered by the GNU General Public License.
4#See the file COPYING for more details.
5#Copyright 2011 World Light Information Limited and Hong Kong Blind Union.
6
7from collections import OrderedDict
8import config
9import globalVars
10import queueHandler
11from logHandler import log
12from synthSettingsRing import SynthSettingsRing
13from speechDictHandler import loadVoiceDict
14from speech import IndexCommand
15import synthDriverHandler
16
17class _SavedSettings(object):
18        """Stores original settings of a physical synthesizer."""
19
20        def __init__(self,synth):
21                self.params={
22                        'rate':synth.rate,
23                        'pitch':synth.pitch,
24                        'inflection':synth.inflection,
25                        'volume':synth.volume
26                }
27                self.endIndex=0xfffe
28
29        def adjustParams(self,synth,rate,pitch,inflection,volume):
30                """Make relative changes to the parameters of a synthsizer instance.
31
32                All relative parameters are ranged between 0 and 100.  0 means half of
33                the original value;  50 means original value;  100 means twice the
34                original value.
35                @param synth: Synthesizer that requires parameter update.
36                @type synth: L{SynthDriver}
37                @param rate: Relative rate.
38                @type rate: int
39                @param pitch: Relative pitch.
40                @type pitch: int
41                @param inflection: Relative inflection.
42                @type inflection: int
43                @param volume: Relative volume.
44                @type volume: int
45                """
46                synth.rate=self._relativeToActual(rate,self.params['rate'])
47                synth.pitch=self._relativeToActual(pitch,self.params['pitch'])
48                synth.inflection=self._relativeToActual(inflection,self.params['inflection'])
49                synth.volume=self._relativeToActual(volume,self.params['volume'])
50
51        @classmethod
52        def _relativeToActual(cls,relative,oldValue):
53                """Converts a percentage relatively.
54                @param relative: The relative adjustment.  Ranges between 0 and 100.
55                @type relative: int
56                @param oldValue: The original percentage value.
57                @type oldValue: int
58                """
59                if relative<0:
60                        return oldValue
61                elif relative<50:
62                        min=oldValue/2
63                        max=oldValue
64                        percent=relative*2
65                        return int(round(float(percent)/100*(max-min)+min))
66                elif relative==50:
67                        return oldValue
68                elif relative<=100:
69                        min=oldValue
70                        max=oldValue*2
71                        if max>100: max=100
72                        percent=(relative-50)*2
73                        return int(round(float(percent)/100*(max-min)+min))
74                else:
75                        return oldValue
76
77class VirtualSynthDriver(synthDriverHandler.SynthDriver):
78        """Abstract virtual synthesizer that manages multiple drivers.
79
80        Support for rate, pitch, inflection and volume is provided by this class.
81        At a minimum, multi-language synthesizer drivers inheriting from this class must override the L{speak} methods.
82        The L{speak} method should break the text into portions and feed each portion to a different synthesizer by calling L{speakPortion}.
83        """
84        synths={}
85        settings={}
86        curSynth=None
87        genID=None
88        queue=[]
89        _lastIndex=None
90
91        # Default parameters
92        relativeRate=50
93        relativePitch=50
94        relativeInflection=50
95        relativeVolume=50
96
97        def isSupported(self,settingName):
98                if settingName in ("rate", "pitch", "inflection", "volume"):
99                        return True
100                return super(VirtualSynthDriver,self).isSupported(settingName)
101
102        def saveSettings(self):
103                super(VirtualSynthDriver,self).saveSettings()
104                conf=config.conf["speech"][self.name]
105                conf["rate"]=self.relativeRate;
106                conf["pitch"]=self.relativePitch;
107                conf["inflection"]=self.relativeInflection;
108                conf["volume"]=self.relativeVolume;
109
110        def loadSettings(self):
111                conf=config.conf["speech"][self.name]
112                conf["rate"]=self.relativeRate;
113                conf["pitch"]=self.relativePitch;
114                conf["inflection"]=self.relativeInflection;
115                conf["volume"]=self.relativeVolume;
116                super(VirtualSynthDriver,self).loadSettings()
117
118        def pause(self,switch):
119                if self.curSynth in self.synths:
120                        self.synths[self.curSynth].pause(switch)
121
122        def cancel(self):
123                del self.queue[:]
124                if self.curSynth in self.synths:
125                        self.synths[self.curSynth].cancel()
126                self.curSynth=None
127
128        def terminate(self):
129                """Terminates this virtual synthesizer.
130                Subclasses should call the superclass method first.
131                """
132                self.cancel()
133                if self.genID!=None:
134                        queueHandler.cancelGeneratorObject(self.genID)
135                self.unloadAllSynthsExcept()
136
137        def _get_lastIndex(self):
138                return self._lastIndex
139
140        def _get_rate(self):
141                return self.relativeRate
142
143        def _set_rate(self,rate):
144                self.relativeRate=rate
145
146        def _get_pitch(self):
147                return self.relativePitch
148
149        def _set_pitch(self,pitch):
150                self.relativePitch=pitch
151
152        def _get_inflection(self):
153                return self.relativeInflection
154
155        def _set_inflection(self,inflection):
156                self.relativeInflection=inflection
157
158        def _get_volume(self):
159                return self.relativeVolume
160
161        def _set_volume(self,volume):
162                self.relativeVolume=volume
163
164        def _processPortion(self):
165                """Speaks next text portion in queue with the requested synthesizer.
166                @return: C{True} if speech was started, C{False} if not.
167                @rtype: bool
168                """
169                (name,portion,index,rate,pitch,inflection,volume)=self.queue.pop(0)
170                self._lastIndex=0 if index==None else index
171                if name in self.synths:
172                        self.curSynth=name
173                        synth=self.synths[name]
174                        setting=self.settings[name]
175                        setting.adjustParams(synth,rate,pitch,inflection,volume)
176                        setting.endIndex=setting.endIndex^1
177                        # Feed speech sequence to target synthesizer
178                        speechSequence=[]
179                        if index is not None:
180                                speechSequence.append(IndexCommand(index))
181                        speechSequence.append(portion)
182                        speechSequence.append(IndexCommand(setting.endIndex))
183                        speechSequence.append(" ")
184                        synth.speak(speechSequence)
185                        return True
186                else:
187                        self.curSynth=None
188                        return False
189
190        def _portionGenerator(self):
191                while True:
192                        if self.curSynth!=None:
193                                # Check whether current synthersizer has finished speaking
194                                if not self.curSynth in self.synths:
195                                        self.curSynth=None
196                                elif self.synths[self.curSynth].lastIndex==self.settings[self.curSynth].endIndex:
197                                        self.curSynth=None
198                        if self.curSynth==None and len(self.queue)>0:
199                                # Process next portion when there is no active synth
200                                self._processPortion()
201                        yield
202
203        def loadSynth(self,name):
204                """Adds a new synthesizer to the list of managed synthersizers.
205                @return: C{True} if the synthesizer is now managed, C{False} if not.
206                @rtype: bool
207                @param name: The name of the synthesizer that needs to be managed.
208                @type name: str
209                """
210                if len(name)==0:
211                        # Ignore blank names
212                        return False
213                if name==self.name:
214                        # Prevent recursive loading of synthesizer
215                        return False
216                if name in self.synths:
217                        # Open each synthesizer only once
218                        return True
219                try:
220                        newSynth=synthDriverHandler._getSynthDriver(name)()
221                        if name in config.conf["speech"]:
222                                newSynth.loadSettings()
223                        else:
224                                # Create the new section.
225                                config.conf["speech"][name]={}
226                                if newSynth.isSupported("voice"):
227                                        voice=newSynth.voice
228                                else:
229                                        voice=None
230                                synthDriverHandler.changeVoice(newSynth,voice)
231                                newSynth.saveSettings()
232                        # Start managing synthesizer now
233                        self.synths[name]=newSynth
234                        self.settings[name]=_SavedSettings(newSynth)
235                        # Speak dummy string to setup last index
236                        speechSequence=[]
237                        speechSequence.append(IndexCommand(self.settings[name].endIndex))
238                        speechSequence.append(" ")
239                        newSynth.speak(speechSequence)
240                        return True
241                except:
242                        log.error("Error while importing SynthDriver %s"%name,exc_info=True)
243                        return False
244
245        def unloadAllSynthsExcept(self,retained=()):
246                """Unloads all synthesizers managed by this driver with exception.
247                @param retained: Names of synthesizer to retain under management.
248                @type retained: list or tuple of str
249                """
250                self.cancel()
251                for name in self.synths.keys():
252                        if name in retained: continue
253                        self.synths[name].terminate()
254                        del self.synths[name]
255                        del self.settings[name]
256
257        def setMasterSynth(self,name):
258                """Uses a synthesizer as the master and initializes NVDA accordingly.
259                @param name: The name of the master synthesizer.
260                @type name: str
261                """
262                if name in self.synths:
263                        synth=self.synths[name]
264                        if synth.isSupported("voice"):
265                                #We need to call changeVoice here so voice dictionaries can be managed
266                                synthDriverHandler.changeVoice(synth,synth.voice)
267                        else:
268                                #start or update the synthSettingsRing (for those synths which do not support 'voice')
269                                if globalVars.settingsRing:
270                                        globalVars.settingsRing.updateSupportedSettings(synth)
271                                else:
272                                        globalVars.settingsRing=SynthSettingsRing(synth)
273                                loadVoiceDict(synth)
274
275        def getSynthSetting(self,name,attr):
276                """Obtains the setting of a managed synthesizer
277                @return: Value of the required setting.
278                @rtype: int
279                @param name: The name of the synthesizer.
280                @type name: str
281                @param attr: The name of the setting.
282                @type attr: str
283                """
284                if name in self.synths:
285                        setting=self.settings[name]
286                        if attr in setting.params:  return setting.params[attr]
287                return 50
288
289        def setSynthSetting(self,name,attr,value):
290                """Changes the setting of a managed synthesizer
291                @param name: The name of the synthesizer.
292                @type name: str
293                @param attr: The name of the setting.
294                @type attr: str
295                @param value: New value for the setting.
296                @type value: int
297                """
298                if name in self.synths:
299                        setting=self.settings[name]
300                        if attr in setting.params:  setting.params[attr]=value
301
302        def speakPortion(self,name,portion,index):
303                """Queues a text portion to be spoken by a particular synthesizer.
304                @param name: Name of the synthesizer that will speak the text portion.
305                @type name: str
306                @param portion: The text portion to be spoken.
307                @type portion: str
308                @param index: An index to associate with this text portion.
309                @type index: int
310                """
311                if self.genID==None:
312                        self.genID=queueHandler.registerGeneratorObject(self._portionGenerator())
313                if len(portion)>0:
314                        self.queue.append([
315                                name,
316                                portion,
317                                index,
318                                self.relativeRate,
319                                self.relativePitch,
320                                self.relativeInflection,
321                                self.relativeVolume
322                        ])