Deriving installed voices names and populating pop-up

I took me quite a while to work out how to populate a list of available voices with the proper names in Mavericks when there is a mix of legacy voices and the newer Nuance “premium” voices.
I had seen other methods that involved text delimiting the names identifiers to extract the name and testing for “premium” and then capitalization, etc. Instead I’ve come up with a simpler solution using NSSpeechSynthesizer and I’d like to share it so no one else has to go through the frustration I did trying to figure this out.

First make a couple global properties at the top of your script for the list of voices (this will hold a list of the actual voice identifier names as the system reads them) and the user selected voice, as well as an outlet for the popup.


property voiceList : {}
property currentVoice : ""
property voiceListPopup : missing value

Now in the “applicationWillFinishLaunching” handler insert the following code to query NSSpeechSynthesis during launch for the list of available voices and then also ask its voice attributes for the human-readable name of each voice to populate the pop-up. Whether legacy or “premium,” each voice already has a proper human-readable name in the “NSVoiceName” key value for attributesForVoice:(voiceIdentity) record (NSDictionary). It took me a while to realize that in ASObj-C I didn’t need the “NS” for the key name. Basically I put the system-provided list of the identifier names of all voices into the global list “voiceList” (for use in other handlers) and I made a new version of that list called voiceListReadable that will only be used to populate the popup during launch.


on applicationWillFinishLaunching:aNotification
	-- Insert code here to initialize your application before any files are opened
	
	
	-- Prepare the available voice popup on launch
	set voiceList to current application's NSSpeechSynthesizer's availableVoices() as list
	set voiceListReadable to {} as list
	
	repeat with i from 1 to (length of voiceList)
		
		set voiceAttributes to (current application's NSSpeechSynthesizer's attributesForVoice:(item i of voiceList as string))
		
		set readableVoiceName to (voiceAttributes's valueForKey:"VoiceName") as string
		
		set end of voiceListReadable to readableVoiceName
		
	end repeat
	
	-- Populate voice list popup with voice list
	voiceListPopup's addItemsWithTitles:voiceListReadable
	
end applicationWillFinishLaunching:

Now create an action handler to change the currentVoice variable every time the user selects a new one from the popup. I’ve added the “-1” because the index numbers we want don’t include the extra option at the top of the popup list for the default System Voice.


on setNewVoice:sender
	
	if voiceListPopup's indexOfSelectedItem = 1 then
		set currentVoice to theSynth's defaultVoice as string
		
	else
		set currentVoice to item ((voiceListPopup's indexOfSelectedItem as integer) - 1) of voiceList as string
		
	end if
	
end setNewVoice:

Wherever you create a NSSpeechSynthesizer instance you just have to tell it to use the currentVoice. This bit assumes you have a push button linked to a “sayHelloButton” action.


on sayHelloButton:sender
	
	set voiceText to "Hello World!" as string
	set theSynth to current application's NSSpeechSynthesizer's new()
	theSynth's setDelegate:me
	theSynth's setVoice:currentVoice
	theSynth's startSpeakingString:voiceText
	
end sayHelloButton:

Finally, add a popup to your nib, put “System Voice” and a divider at the top. The code executed at launch will automatically fill the rest of the list.

CTRL-click on your app delegate object (the blue cube) and connect both the voiceListPopup outlet and the setNewVoice action to the popup button.

Now build and run.

Notice that this works perfectly on “Samantha” (a.k.a. Siri) that I manually downloaded in the System Pref pane for Dictation and Speech, instead of having to decipher com.apple.speech.synthesis.voice.samantha.premium.

For my next project I plan to sort by language and gender, which are also keys in attributesForVoice:
Any help in this endeavour would be greatly appreciated.

Tim Dashwood

Hi,

you can omit all the coercions if you stay in terms of Cocoa


on applicationWillFinishLaunching:aNotification
	
	set voiceList to current application's NSSpeechSynthesizer's availableVoices()
	repeat with aVoice in voiceList
		set voiceAttributes to (current application's NSSpeechSynthesizer's attributesForVoice:aVoice)
		voiceListPopup's addItemWithTitle:(voiceAttributes's valueForKey:"VoiceName")
	end repeat
	
end applicationWillFinishLaunching:


Ah yes. Much cleaner.

Thank you.
TIm

This should get you started:

	set anNSMutableArray to current application's NSMutableArray's array()
	set voiceNSArray to current application's NSSpeechSynthesizer's availableVoices()
	set theCount to voiceNSArray's |count|()
	repeat with i from 0 to (theCount - 1)
		(anNSMutableArray's addObject:(current application's NSSpeechSynthesizer's attributesForVoice:(voiceNSArray's objectAtIndex:i)))
	end repeat
	set firstNSSortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:(current application's NSVoiceGender) ascending:true
	set secondNSSortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:(current application's NSVoiceLanguage) ascending:true
	anNSMutableArray's sortUsingDescriptors:{firstNSSortDescriptor, secondNSSortDescriptor}

Thank you Shane. That has definitely pointed me in the right direction.