Help with NSSpeechSynthesizer class

As a hobbyist Applescripter, I’m taking my first steps into ApplescriptObjC in X code.

I am not working on any specific project, but am doing various little exercises in an attempt
to learn the ApplescriptObjC language, and also the Cocoa Framework’s classes and objects.

I’m getting error messages in the debug console concerning my calls to the NSSpeechSynthesizer
class, the errors are on the setDelegate: startSpeakingString: and stopSpeaking: methods in the
attached code, I know that I could use the say command in the Standard Additions to accomplish
this example, but that would defeat the object of learning the Cocoa Framework’s.

I have tried using the alloc()'s initWithVoice_() method in the startButton’s handler, and also in a
tell class “NSSpeechSynthesizer” of current application end tell block, but the unrecognised
selector sent to class error still appears in the debug console.

I have read through the documentation on the NSSpeechSynthesizer class, but I have not found
anything to solve my problem, although this is not an important project for me, solving it would
also possibly solve future problems on other Cocoa classes, where the solution may also not be
that obvious.

[code]script SpeaklineAppDelegate

property parent : class “NSObject”
property textField : missing value
property startButton : missing value
property stopButton : missing value
property speechSynth : class “NSSpeechSynthesizer” of current application

on run {}
initialize()
end run

on initialize()
speechSynth’s alloc()'s initWithVoice_(missing value)
speechSynth’s setDelegate_(me) --problem line of code Here–
return me
end initialize

on sayIt_(sender)
set speechText to textField’s stringValue() as string
if (length of speechText) is equal to 0 then
return
else
my stopButton’s setEnabled_(true)
my startButton’s setEnabled_(false)
speechSynth’s startSpeakingString_(textField’s stringValue()) --problem line of code Here–
end if
end sayIt_

on stopIt_(sender)
speechSynth’s stopSpeaking() --problem line of code Here–
end stopIt_

on speechSynthesizer_didFinishSpeaking_(speechSynth, complete)
my stopButton’s setEnabled_(false)
my startButton’s setEnabled_(true)
end speechSynthesizer_didFinishSpeaking_

on applicationWillFinishLaunching_(aNotification)
– Insert code here to initialize your application before any files are opened
end applicationWillFinishLaunching_

on applicationShouldTerminate_(sender)
– Insert code here to do any housekeeping before your application quits
return current application’s NSTerminateNow
end applicationShouldTerminate_

end script[/code]
Many Thanks if anyone can help.

Regards Mark

You need to create an instance of the speech synthesizer. So your line :
speechSynth’s alloc()'s initWithVoice_(missing value) should be something like:
set talker to speechSynth’s alloc()'s initWithVoice_(missing value)

The setDelegate: and start and stop speaking methods should then be addressed to “talker” not to “speechSynth” which is a reference to the class, not an instance.

Also, you don’t need to have run and initialize methods – the code in the initialize method should go inside the applicationWillFinishLaunching method.

Ric

Thanks rdelmar

I will try that.

Sorry if my post was a bit beginner, but that’s what I am.

Regards Markl

Hi,

I’m doing something substantially similar, and have the bulk of it working, with just a couple of issues:

  1. I can’t set the voice:

if chosen_voice = "DEFAULT" then
			log "Speaking"
			voice's startSpeakingString_(complete_text)
			else
			try
				log "Custom Speaking"
				voice's setVoice_(chosen_voice)
				log "Speaking with: " & chosen_voice
				voice's startSpeakingString_(complete_text)
			end try
		end if

In the above I have a list of voices to choose from. By default, DEFAULT is selected. Everything works fine. It speaks.

If I choose a different voice (In this case, Fiona, which I have installed), it doesn’t work.

I get the following logged:

Previously using just AS I could use something like “on error errmsg” and handle that. How can I handle the error using ASOC? At the moment, it just speaks using the default voice in both cases - not ideal.

And, more to the point, why doesn’t it speak with the voice that I know is installed?

  1. It doesn’t get the signal that it’s finished speaking:

Using the same code above, ie. with an instance called voice, like above:


	on speechSynthesizer_didFinishSpeaking_(voice,complete)
		log "Finished"
		my stopButton's setEnabled_(false)
		my defaultStartButton's setEnabled_(true)
	end speechSynthesizer_didFinishSpeaking_

Basically I turn off the start button when I start speaking, and turn on the stop button. That works fine. I have a method calling the stopSpeaking which works just fine, and sets the buttons correctly. It’s just this automatic one that doesn’t work.

Doesn’t even log “Finished” like it should. This doesn’t seem to get triggered at all.

Many thanks,

Alex

Your log suggests chosen_voice is “Fiona”, but that’s not a valid voice name – it needs to be something like “com.apple.speech.synthesis.voice.Agnes”.

You don’t. You should check whether the voice you want is in the array returned by availableVoices(). Error-trapping is not the Cocoa way.

I’m guessing that you didn’t set the script as the delegate of the synthesizer. Something like:

voice's setDelegate_(me)

Shane,

  1. Thanks for the reply. Originally I wrote this is pure AS, and I’m converting it all over, bit by bit. Your book is immensely helpful, I have it open at all times.

That’s why I was using the voice name, not the complete com.apple…bit. I’ll change that over.

  1. Thanks - I’ll have to read that in and parse it then.

  2. I do, in the startup, as recommended earlier:


	on applicationWillFinishLaunching_(aNotification)
		set voice to speechSynth's alloc()'s initWithVoice_(missing value) -- default voice
		voice's setDelegate_(me)
		return me
	end applicationWillFinishLaunching_

Regards,

Alex

What happens if you implement one of the other delegate methods, like speechSynthesizer:willSpeakWord:ofString:
? Also, try changing the argument name to something other than voice.

Shane,

Thank you. I’ve moved the setDelegate_ into each method I’m running, and that seems to work. Doesn’t seem to work if I put it into the applicationWillFinishLaunching_ method.

That also means that the finishedSpeaking method works too.

Great progress.

I add the com.apple bit to the beginning of each voice chosen, and that works very well too.

I’m now adding in a new method to check the available voices and alert.

Thank you for all help.

Regards,

Alex

It should. Do you have voice declared as a property?

Also, you shouldn’t have the line “return me” in your applicationWillFinishLaunching method, since that method doesn’t return anything.

Ric

Hi Alex

I made several posts on a NSSpeechSynthersizer project this post was started on, and after several problems with the delegate methods, Shane gave me a lot of help, but found out and advised me to there being a Framework bug, when using the NSSpeechSynthersizer class with ASOC.

After pulling my hair out with the projects problems, I changed it over to Objective-C code, and never had any problems again, so I know this does not help you solve these problems, but I thought you should be aware that this class does not seem to work correctly, with ASOC code.

But good luck with it.

Regards Mark

I don’t think this bug causes any harm though. I got this error when running my trial code:

SpeechTest(7035,0x1068bf000) malloc: *** auto malloc[7035]: error: GC operation on unregistered thread. Thread registered implicitly. Break on auto_zone_thread_registration_error() to debug.

But this type of error doesn’t stop the app, and it only appears the first time I have the app speak.

Ric

Mark,

Yes, I had this project in pure AS, then decided to extend it, and started using ASOC. It’s an odd mix. The further I go, the more I think about starting again in just ObjC.

I actually have everything working as it should though at the moment. A little kludgy in places, but it’s working.

The next goal is to get availableVoices to return something so I can parse it out nicely.

Thanks for your comments.

Ric,

I get that too, but from other googling it seems to be a known non-impacting bug.

Regards,

Alex

OK Alex

I have dug up some of the methods, I used in the voice selection part of my project, and from memory the voice selection parts did work, it was only some of the delegate methods that I was strugling with, so if you can make use of these voice selection parts of the code then good luck, this is not the whole project but just the relevant parts for voice selection.

[code]property tableView : missing value
property voiceList : {}
property speechSynth : class “NSSpeechSynthesizer” of current application
global defaultVoiceIndex

on initialize()
	speechSynth's alloc()'s initWithVoice_(missing value)
	--speechSynth's setDelegate_(me)
	set voiceList to speechSynth's availableVoices() as list
	set defaultVoice to speechSynth's defaultVoice() as text
	set defaultTextItemDelimiters to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "."
	repeat with i from 1 to (length of voiceList)
		set voiceString to item i of voiceList as text
		if voiceString is equal to defaultVoice then
			set defaultVoiceIndex to i as integer
		end if
		set voiceStringList to get every text item of voiceString as list
		set voiceName to item (length of voiceStringList) of voiceStringList as text
		set item i of voiceList to voiceName as text
	end repeat
	set AppleScript's text item delimiters to defaultTextItemDelimiters
	return me
end initialize

on awakeFromNib()
	set NSIndexSet to class "NSIndexSet" of current application
	set defaultRow to (defaultVoiceIndex - 1) as integer
	my tableView's selectRowIndexes_byExtendingSelection_(NSIndexSet's indexSetWithIndex_(defaultRow), false)
	my tableView's scrollRowToVisible_(defaultRow)
end awakeFromNib

on tableViewSelectionDidChange_(notification)
	set row to tableView's selectedRow() as integer
	if row is equal to -1 then
		return
	else
		set selectedVoice to item (row + 1) of voiceList as text
		--speechSynth's setVoice_(selectedVoice)
	end if
end tableViewSelectionDidChange_

on tableView_objectValueForTableColumn_row_(tableView, tableColumn, row)
	set voiceName to item (row + 1) of voiceList
	return voiceName
end tableView_objectValueForTableColumn_row_[/code]

Regards Mark

Thanks Mark, I actually used the following:


			set allVoices to NSSpeechSynthesizer's availableVoices()
			repeat with thisVoice in allVoices
				set thisVoice to thisVoice as text
				set voiceAttrs to NSSpeechSynthesizer's attributesForVoice_(thisVoice as text)
				set keys to voiceAttrs's allKeys()
				repeat with aKey in keys
					set aKey to aKey as text
					if aKey is "VoiceName" then
						set voiceName to voiceAttrs's objectForKey_(aKey) as text
						if voiceName = chosen_voice then
							log "Voice is usable: " & thisVoice
							set found to "FOUND"
							set usableVoice to thisVoice as text
						end if
					end if
				end repeat
			end repeat


Basically, it’s part of a greater loop, going through each voice, comparing the voice names to the one selected by the user (the attribute which is nicely formatted), then taking the voice class name (the com.apple… one) and feeding that into a setVoice_() later in the program.

Feels good to give a little code back, no matter how messy it is.

I rather want to rewrite the interface at some point to just detect the voices installed and supply that as a drop-down, but I’m using a matrix at the moment, so have to supply the list.

Regards,

Alex

I’m not sure you need all that looping. Particularly the looping through the keys, since you know which key you are testing. I’m sure this could be done with an NSPredicate rather than loops, but a simple loop that works to find out whether a name is in the available voices is:

set allVoices to current application's NSSpeechSynthesizer's availableVoices() as list
        set chosen_voice to "com.apple.speech.synthesis.voice." & chosen_voice
        if chosen_voice is in allVoices  then 
            log "I found it"
            else
            log "That voice is not available"
        end if

This is assuming that chosen_voice is just a simple voice name that has been selected somehow by the user.

Ric

Ric,

The problem with that is that the voices aren’t just com.apple.speech.synthesis.voice.<>.

That was my first approach, but if you take one of the new Lion voices, Fiona for example, the name is actually com.apple.speech.synthesis.voice.fiona_premium, or similar. The other older voices do match.

For the users sake, I thought it better to use the display name attribute and hide the possibilities of voice naming in the code - hence the extra looping. I’d rather waste cycles searching for a voice with an attribute of Fiona, than have to have the user pick fiona_premium from a list, rather than Fiona.

But, yes, probably a way to search just the name attribute, but I was lazy and threw it together quickly last night based on some googling.

Regards,

Alex

Thanks Alex, I didn’t know about that complication. So to still use the attributes your code could be simplified to:

set allVoices to current application's NSSpeechSynthesizer's availableVoices()
        repeat with thisVoice in allVoices
            set voiceName to current application's NSSpeechSynthesizer's attributesForVoice_(thisVoice)'s valueForKey_("VoiceName")
                if voiceName as text is chosen_voice then
                    log "Voice is usable: " & thisVoice
                    set found to "FOUND"
                    set usableVoice to thisVoice as text
                end if
        end repeat

BTW, where do you see voices like fiona_premium? I don’t see them when I log availableVoices (and I’m using Lion). Are there other voices that you can load up some how?

Ric