Sunday, July 27, 2008

Hillegass Ch. 5,6


Hillegass has a fun example using the NSSpeechSynthesizer class, which can vocalize a string. The application shown here grew organically---it's composed of several different layers. The app window looks like what's shown above.

1. In the first part, we have a Text Field in the window whose value is bound to the variable textField, as shown previously. The speaker is an instance of NSSpeechSynthesizer with the default voice ("Alex"). The speak action just feeds the string to speaker. In this version, the second button was labeled "Stop", and just called speaker.stopSpeaking().

2. Next we change the label and action on this button to "Switch", as shown in the screenshot. We set the button to be enabled if we are not currently speaking. (In the initial example we only enable "Stop" if we were speaking). We do this by binding the button's Enabled attribute to the variable notSpeaking. We change this flag in the action "speak", and change it back in "speechSynthesizer_didFinishSpeaking", which gets called because we've set ourselves to be the delegate of speaker. (Actually, I'm not sure why this works. Technically, we should call an accessor method for notSpeaking in order to KVO compliant).

3. The third part uses a programmatic method to alter the action triggered by pushing the "Switch" button. When we push it the first time, it calls "switch_", as wired up in IB. However, as part of this function we change the action to be "method2_" as shown in the code, and we can see that this works by logging to the Console. This uses the @objc.signature decorator. Notice that the "setAction_" method gets the string corresponding to the Objective C selector for the new method. (Also, I've used the signature string "@@:v" because the method takes a second (object) argument---I'm not sure if this really matters.)

4. Finally, we allow the user to choose a voice. This example uses a popup button. The Content of the popup is bound to a variable which holds a list of names corresponding to the voices. These are extracted as shown in "setVoices". The selectedIndex of the popup is bound to our variable of the same name, so when we're asked to speak we retrieve the voice from the voice list using that index. There is a weird thing about popups where the selectedIndex is not set until the user touches the popup, so we have to check for that. Here is the code:

class PySpeakAppDelegate(NSObject):
textField = objc.ivar('textField')
notSpeaking = objc.ivar('notSpeaking')
voiceL = objc.ivar('voiceL')
nameL = objc.ivar('nameL')
selectedIndex = objc.ivar('selectedIndex')

def init(self):
s = NSSpeechSynthesizer.alloc().initWithVoice_(None)
self.speaker = s
self.speaker.setDelegate_(self)
self.setVoices()
self.notSpeaking = True
self.textField = 'Hi, my name is George Washington'
return self

def setVoices(self):
L = NSSpeechSynthesizer.availableVoices()
self.voiceL = L
DL = [NSSpeechSynthesizer.attributesForVoice_(v) for v in L]
self.nameL = [D.objectForKey_('VoiceName') for D in DL]
NSLog("%s" % self.nameL)

@objc.IBAction
def speak_(self,sender):
if self.selectedIndex: i = self.selectedIndex
else: i = 0
NSLog("%i Say: %s" % (i, self.textField))
self.speaker.setVoice_(self.voiceL[i])
self.notSpeaking = False
self.speaker.startSpeakingString_(self.textField)
pass

def speechSynthesizer_didFinishSpeaking_(
self,speechSyn,flag):
NSLog("didFinishSpeaking_")
self.notSpeaking = flag

@objc.IBAction
def switch_(self,sender):
NSLog("Method 1")
sender.setAction_("method2:")

@objc.signature('@@:f')
def method2_(self,sender):
NSLog("Method 2")