11# A part of NonVisual Desktop Access (NVDA)
2- # Copyright (C) 2006-2022 NV Access Limited
2+ # Copyright (C) 2006-2023 NV Access Limited, Leonard de Ruijter
33# This file is covered by the GNU General Public License.
44# See the file COPYING for more details.
55
4343 CharacterModeCommand ,
4444 BreakCommand ,
4545 PitchCommand ,
46+ RateCommand ,
47+ VolumeCommand ,
4648)
4749
50+
4851class SynthDriverBufSink (COMObject ):
4952 _com_interfaces_ = [ITTSBufNotifySink ]
5053
@@ -74,6 +77,11 @@ class SynthDriver(SynthDriver):
7477 name = "sapi4"
7578 description = "Microsoft Speech API version 4"
7679 supportedSettings = [SynthDriver .VoiceSetting ()]
80+ supportedCommands = {
81+ IndexCommand ,
82+ CharacterModeCommand ,
83+ BreakCommand ,
84+ }
7785 supportedNotifications = {synthIndexReached ,synthDoneSpeaking }
7886
7987 @classmethod
@@ -120,10 +128,9 @@ def speak(self,speechSequence):
120128 textList = []
121129 charMode = False
122130 item = None
123- isPitchCommand = False
124- pitch = WORD ()
125- self ._ttsAttrs .PitchGet (byref (pitch ))
126- oldPitch = pitch .value
131+ oldPitch = self .pitch
132+ oldVolume = self .volume
133+ oldRate = self .rate
127134
128135 for item in speechSequence :
129136 if isinstance (item ,str ):
@@ -136,15 +143,22 @@ def speak(self,speechSequence):
136143 elif isinstance (item , BreakCommand ):
137144 textList .append (f"\\ Pau={ item .time } \\ " )
138145 elif isinstance (item , PitchCommand ):
139- offset = int (config .conf ["speech" ]['sapi4' ]["capPitchChange" ])
140- offset = int ((self ._maxPitch - self ._minPitch ) * offset / 100 )
141- val = oldPitch + offset
142- if val > self ._maxPitch :
143- val = self ._maxPitch
144- if val < self ._minPitch :
145- val = self ._minPitch
146- self ._ttsAttrs .PitchSet (val )
147- isPitchCommand = True
146+ val = oldPitch + item .offset
147+ val = self ._percentToParam (val , self ._minPitch , self ._maxPitch )
148+ textList .append (f"\\ Pit={ val } \\ " )
149+ elif isinstance (item , RateCommand ):
150+ val = oldRate + item .offset
151+ val = self ._percentToParam (val , self ._minRate , self ._maxRate )
152+ textList .append (f"\\ Spd={ val } \\ " )
153+ elif isinstance (item , VolumeCommand ):
154+ val = oldVolume + item .offset
155+ val = self ._percentToParam (
156+ val ,
157+ self ._minVolume & 0xffff ,
158+ self ._maxVolume & 0xffff
159+ )
160+ val + = val << 16
161+ textList .append (f"\\ Vol={ val } \\ " )
148162 elif isinstance (item , SpeechCommand ):
149163 log .debugWarning ("Unsupported speech command: %s" % item )
150164 else :
@@ -161,24 +175,13 @@ def speak(self,speechSequence):
161175 textList .append ("\\ PAU=1\\ " )
162176 text = "" .join (textList )
163177 flags = TTSDATAFLAG_TAGGED
164- if isPitchCommand :
165- self ._ttsCentral .TextData (
166- VOICECHARSET .CHARSET_TEXT ,
167- flags ,
168- TextSDATA (text ),
169- self ._bufSinkPtr ,
170- ITTSBufNotifySink ._iid_
171- )
172- self ._ttsAttrs .PitchSet (oldPitch )
173- isPitchCommand = False
174- else :
175- self ._ttsCentral .TextData (
176- VOICECHARSET .CHARSET_TEXT ,
177- flags ,
178- TextSDATA (text ),
179- self ._bufSinkPtr ,
180- ITTSBufNotifySink ._iid_
181- )
178+ self ._ttsCentral .TextData (
179+ VOICECHARSET .CHARSET_TEXT ,
180+ flags ,
181+ TextSDATA (text ),
182+ self ._bufSinkPtr ,
183+ ITTSBufNotifySink ._iid_
184+ )
182185
183186 def cancel (self ):
184187 self ._ttsCentral .AudioReset ()
@@ -239,8 +242,12 @@ def _set_voice(self,val):
239242 if hasRate :
240243 if not self .isSupported ('rate' ):
241244 self .supportedSettings .insert (1 ,SynthDriver .RateSetting ())
245+ self .supportedCommands .add (RateCommand )
242246 else :
243- if self .isSupported ("rate" ): self .removeSetting ("rate" )
247+ if self .isSupported ("rate" ):
248+ self .removeSetting ("rate" )
249+ if RateCommand in self .supportedCommands :
250+ self .supportedCommands .remove (RateCommand )
244251 #Find out pitch limits
245252 hasPitch = bool (mode .dwFeatures & TTSFEATURE_PITCH )
246253 if hasPitch :
@@ -262,8 +269,12 @@ def _set_voice(self,val):
262269 if hasPitch :
263270 if not self .isSupported ('pitch' ):
264271 self .supportedSettings .insert (2 ,SynthDriver .PitchSetting ())
272+ self .supportedCommands .add (PitchCommand )
265273 else :
266- if self .isSupported ('pitch' ): self .removeSetting ('pitch' )
274+ if self .isSupported ('pitch' ):
275+ self .removeSetting ('pitch' )
276+ if PitchCommand in self .supportedCommands :
277+ self .supportedCommands .remove (PitchCommand )
267278 #Find volume limits
268279 hasVolume = bool (mode .dwFeatures & TTSFEATURE_VOLUME )
269280 if hasVolume :
@@ -285,8 +296,12 @@ def _set_voice(self,val):
285296 if hasVolume :
286297 if not self .isSupported ('volume' ):
287298 self .supportedSettings .insert (3 ,SynthDriver .VolumeSetting ())
299+ self .supportedCommands .add (VolumeCommand )
288300 else :
289- if self .isSupported ('volume' ): self .removeSetting ('volume' )
301+ if self .isSupported ('volume' ):
302+ self .removeSetting ('volume' )
303+ if VolumeCommand in self .supportedCommands :
304+ self .supportedCommands .remove (VolumeCommand )
290305
291306 def _get_voice (self ):
292307 return str (self ._currentMode .gModeID )
0 commit comments