Customize voice to announce time

I have attempted to script the anchor “ClockPref” of pane “com.apple.preference.datetime” to toggle the system voice that announces the time. My goal was to toggle between “Samantha” and “Alex”. As GUI scripting has not been successful for me, I was wondering if a more direct method i.e. via command line, shell script, Applescript Objective C. or other method might be available to change the voice that announces the time.


tell application "System Preferences"
	reveal anchor "ClockPref" of pane "com.apple.preference.datetime"
end tell

set theProcess to "System Preferences"
tell application theProcess to activate

tell application "System Events"
	tell process theProcess
		tell window "Date & Time"
			tell tab group 1
				click button "Customize Voice."
				delay 1
			end tell
			repeat with i from 1 to 10
				if exists sheet 1 then
					exit repeat
				else
					delay 0.2
					set i to 1 + 1
					if i = 10 then display dialog "Error" giving up after 2
					return
				end if
			end repeat
			tell sheet 1
				click pop up button 1
				return
				tell pop up button 1
					if value is "Samantha" then
						set value to "Alex"
					else
						set value to "Samantha"
					end if
					
				end tell
				click button "OK"
			end tell
		end tell
		
		
		click (menu item "Close" of menu 1 of menu bar item "Window" of menu bar 1)
	end tell
end tell

Hi. I don’t know how to directly change the date preference, but you can call a script on a timed schedule using launchd and a custom plist. You can search this forum or Apple’s site for how to do that. The script could use standard additions to change the voice.

property index : 1
set voiceOpt to {"Alex", "Victoria"} --or whatever
say (current date)'s time string using voiceOpt's item index
if index = 1 then
	set index to 2
else
	set index to 1
end if

Marc, you are correct that the launchd agent method does work for scheduling a pre-scripted statement to announce the time. It, however, cannot take advantage of the System Preferences’ translation of its time announcement into other languages such as the System Preference currently allows. For example, I would like to customize the voice to Sin-Ji to announce the time in Chinese, on one occasion, and then to customize the voice to Yuna to announce the time in Korean, on another one.
My overall goal was to script announcement of times by the quarter hour but to change the language of the announced text to various language using various keystrokes or at least from a drop down menu, without fumbling through the System Preference Panels and without interrupting my other activities.
My current Applescript attempt failed to manage the various windows and sheets displayed by the underlying Announce Voice or work for scheduling a pre-scripted statement to announce the time. It, however, cannot take advantage of the System Preference application and by my limited ability to script its GUI elements.
The preference file, com.apple.speech.synthesis.general.prefs.plist, does not appear to control the underlying application but rather appears to register its current status.
I could find no dictionary in System Preferences for this function.
I would greatly appreciate any insights that might be more productive than my failed attempt.

Hi akim.

That’s the way it usually works nowadays. The ‘User Defaults’ system updates the plist files itself with any changes of which it’s aware and only reads them when it starts up. If you edit the files yourself, the edits will be ignored and will probably be overwritten later with the original settings.

You can script the Defaults system directly either with shell scripts (“defaults read .”, “defaults write .”, etc.) or with AppleScriptObjC. The latter’s probably easier here, given the depth of the setting within the plist’s hierarchy. The script below only works with voices which have already been downloaded.

use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"

on setTimeAnnouncementVoice(voiceName)
	-- Get the "shared defaults object".
	set userDefaults to current application's class "NSUserDefaults"'s standardUserDefaults()
	
	-- Read the user's "com.apple.speech.synthesis.general.prefs" defaults, making an editable version of the resulting dictionary .
	set speechSynthPrefs to (userDefaults's persistentDomainForName:("com.apple.speech.synthesis.general.prefs"))'s mutableCopy()
	-- . so that its "TimeAnnouncementPrefs" dictionary can be made editable .
	set timeAnnouncementPrefs to (speechSynthPrefs's valueForKey:("TimeAnnouncementPrefs"))'s mutableCopy()
	tell speechSynthPrefs to setValue:(timeAnnouncementPrefs) forKey:("TimeAnnouncementPrefs")
	-- . so that that dictionary's "TimeAnnouncementsVoiceSettings" dictionary can be made editable .
	set timeAnnouncementVoiceSettings to (timeAnnouncementPrefs's valueForKey:("TimeAnnouncementsVoiceSettings"))'s mutableCopy()
	tell timeAnnouncementPrefs to setValue:(timeAnnouncementVoiceSettings) forKey:("TimeAnnouncementsVoiceSettings")
	
	-- . so that that dictionary's "VoiceIdentifier" value can be set to the ID of the required voice.
	set voiceID to (current application's class "NSString"'s stringWithString:("com.apple.speech.synthesis.voice." & voiceName))'s lowercaseString()
	tell timeAnnouncementVoiceSettings to setValue:(voiceID) forKey:("VoiceIdentifier")
	
	-- Make the edited hierarchy the new "com.apple.speech.synthesis.general.prefs" defaults setting.
	tell userDefaults to setPersistentDomain:(speechSynthPrefs) forName:("com.apple.speech.synthesis.general.prefs")
end setTimeAnnouncementVoice

setTimeAnnouncementVoice("Oliver")

Nigel,
Thank you for the wonderful illustration of using the “Foundation” framework through Applescript Objective C to write the preference file. Your script will come in handy especially when reading the plist. As you noted, this does not change the property values of the app prior to its execution. If another framework that controls the use of voice synthesizer could be run via Applescript, prior to the System writing the data to the plist, then an Applescript execution would flow without the cumbersome GUI involvement. That would be scripting goal for this project.
But thank you for your help.

If you you’re going to use launchd to launch the script periodically you could also use the command line util defaults since you’re already using a shell. And because we’re already using an shell there is also an command line utility say that can speak the time for you like this:

[format]say $(date)[/format]

Hi akim.

I didn’t understand your feedback at first because I’d been trying to make clear above that the idea was not to write directly to the file. But then I noticed that although my script does change the setting in the Defaults system (as confirmed by looking at the setting in System Preferences), the previous voice continues to be used. I was forgetting that the process responsible for actually speaking the time has to be quit or killed, and then relaunched, to make it take on new preferences from the Defaults system! Sorry about that. The process involved appears to be “SpeechSynthesisServer”, an application which responds to ‘quit’ and ‘launch’ commands from AppleScript.

In your original query, you wanted a script that would toggle specifically between “Alex” and “Samantha”. This would be another problem for the script above because “Alex”'s ID contains an upper-case “A”, whereas IDs for other voices are lower-case throughout. The script below has the relevant IDs written into it and does actually toggle the voices!

use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"

property firstVoiceID : "com.apple.speech.synthesis.voice.Alex" -- NB. capital "A" in "Alex".
property secondVoiceID : "com.apple.speech.synthesis.voice.samantha" -- Lower-case "s" in "samantha".

on toggleTimeAnnouncementVoices()
	-- Get the "shared defaults object".
	set userDefaults to current application's class "NSUserDefaults"'s standardUserDefaults()
	-- Read the user's "com.apple.speech.synthesis.general.prefs" defaults, getting an editable version of the resulting dictionary .
	set speechSynthPrefs to (userDefaults's persistentDomainForName:("com.apple.speech.synthesis.general.prefs"))'s mutableCopy()
	-- . so that its "TimeAnnouncementPrefs" subdictionary can be replaced with an editable one .
	set timeAnnouncementPrefs to (speechSynthPrefs's valueForKey:("TimeAnnouncementPrefs"))'s mutableCopy()
	tell speechSynthPrefs to setValue:(timeAnnouncementPrefs) forKey:("TimeAnnouncementPrefs")
	-- . so that that dictionary's "TimeAnnouncementsVoiceSettings" subdictionary can also be replaced with an editable one.
	set timeAnnouncementVoiceSettings to (timeAnnouncementPrefs's valueForKey:("TimeAnnouncementsVoiceSettings"))'s mutableCopy()
	tell timeAnnouncementPrefs to setValue:(timeAnnouncementVoiceSettings) forKey:("TimeAnnouncementsVoiceSettings")
	
	-- Get the ID of the current voice. If it's one of the two specified, get the id of the other.
	set currentVoiceID to timeAnnouncementVoiceSettings's valueForKey:("VoiceIdentifier")
	if ((currentVoiceID's isEqualToString:(firstVoiceID)) as boolean) then
		set otherVoiceID to secondVoiceID
	else if ((currentVoiceID's isEqualToString:(secondVoiceID)) as boolean) then
		set otherVoiceID to firstVoiceID
	else
		return -- Don't continue if the current voice isn't one of the two specified.
	end if
	
	-- Quit the "SpeechSynthesisServer" application if it's running.
	if (application "SpeechSynthesisServer"'s running) then
		tell application "SpeechSynthesisServer" to quit
		tell application "System Events"
			repeat while (process "SpeechSynthesisServer" exists)
				delay 0.2
			end repeat
		end tell
	end if
	
	-- Set the other voice's ID as the VoiceIdentifier value in the TimeAnnouncementsVoiceSettings dictionary.
	tell timeAnnouncementVoiceSettings to setValue:(otherVoiceID) forKey:("VoiceIdentifier")
	-- Make the entire edited hierarchy the new "com.apple.speech.synthesis.general.prefs" defaults setting.
	tell userDefaults to setPersistentDomain:(speechSynthPrefs) forName:("com.apple.speech.synthesis.general.prefs")
	
	-- (Re)launch SpeechSynthesisServer.
	tell application "SpeechSynthesisServer" to launch
end toggleTimeAnnouncementVoices

toggleTimeAnnouncementVoices()

OT: I’m glad this subject came up ” not just because it’s an interesting scripting problem but because it’s made me look at the system voices again. I set the voice on my machines to “Vicki” years ago because it was the least irritating and least unintelligible at the time and have never thought to look at the setting again. But while researching this, I’ve discovered “Kate”, who has a nice south-east-of-England accent reminding me of my youth orchestra days, excellent prosody, and a stunning ability to handle British place names! “Leicester”, “Gloucester”, “Worcester”, “Happisburgh”, “Birmingham”, and “Berkeley Square” are all correctly pronounced (or near enough) and “Shrewsbury”'s rendered in its “Shrowsbury” form. Even some Welsh place names are pretty well handled, vowelwise at least: “Bala”, “Pwllheli”, “Penrhyndeudraeth”, “Llandudno”, “Llanfair Caereinion”, “Waunfawr”. Her “Machynlleth”'s a bit weak though and her “Betws y Coed” is one of those renditions which makes you cringe and pretend not to be English when visiting the place!

Mind you, that’s not necessarily always the case – in fact, I’d say it’s the exception.

An application can read a defaults value once, store it, and keep using that value – in which case you will have to kill it – or it can simply consult defaults every time it needs a particular value. Although the latter might sound inefficient, the defaults system is designed to work that way, and it often results in cleaner code (the bits over here don’t need to get at the variable stored over there, sort of thing).

In the case of voices, where there’s presumably a bit of file loading and set-up involved each time the choice changes, it makes sense not to do it every time there’s a need to speak. But that’s probably in the minority of stored default values.

Thanks for the information, Shane. So when writing Defaults scripts, the thing to do is to try them both with and without quitting the app concerned ” although in the current case, where the effectiveness can only be ascertained four times an hour at most, it can take a while!

Here’s a shell script version of the script in post #7. With my limited shell scripting knowledge, I’ve had to choose between using two shell scripts or setting the defaults anyway to what they already are if the current voice isn’t one of those specified in the properties. On balance of probabilities, I’ve gone for the latter.

property firstVoiceID : "com.apple.speech.synthesis.voice.Alex" -- NB. capital "A" in "Alex".
property secondVoiceID : "com.apple.speech.synthesis.voice.samantha" -- Lower-case "s" in "samantha".

on toggleTimeAnnouncementVoices()
	-- Quit the "SpeechSynthesisServer" application if it's running.
	if (application "SpeechSynthesisServer"'s running) then
		tell application "SpeechSynthesisServer" to quit
		tell application "System Events"
			repeat while (process "SpeechSynthesisServer" exists)
				delay 0.2
			end repeat
		end tell
	end if
	
	do shell script "# Read and edit the time announcement preferences.
editedTAPrefs=$(defaults read com.apple.speech.synthesis.general.prefs TimeAnnouncementPrefs |
sed -E '/VoiceIdentifier = / { ; # In the VoiceIndentifier line .
	/\"" & firstVoiceID & "\"/ { ; # . if the line contains the first voice ID .
		s//\"" & secondVoiceID & "\"/ ; # . substitute the other ID .
		x ; # . and swap the edited line with the empty hold space to exclude it from the next test.
	}
	/\"" & secondVoiceID & "\"/ { ; # If instead the line contains the other ID .
		s//\"" & firstVoiceID & "\"/ ; # . substitute the first ID .
		x ; # . and swap the edited line with the empty hold space ditto.
	}
	/^$/ x ; # Unless the pattern space still holds an unmatched line, swap the edited line back into it.
}') ;
# Set the time announcement preferences to the edited (or not) version.
defaults write com.apple.speech.synthesis.general.prefs TimeAnnouncementPrefs \"$editedTAPrefs\""
	
	-- (Re)launch SpeechSynthesisServer.
	tell application "SpeechSynthesisServer" to launch
end toggleTimeAnnouncementVoices

toggleTimeAnnouncementVoices()

Nigel,
Thank you, and I enjoyed your approach of reading and writing a preference plist using the Foundation Framework via Applescript Objective C. I appreciated your great help as your script,via the foundation framework, re-wrote plist values for the desired preference keys.
The crux of your current script, however, depended on SpeechSynthesisServer’s limited AppleScript vocabulary.
In the interest of scripting a more direct method, I was wondering whether you were aware of any method to direct an Objective C framework that controlled SpeechSynthesisServer.
If so, then it would appear to me that the final step of converting a series of Objective C commands to Applescript Objective C commands would provide a much more straightforward script towards changing the Announce Time function.
Thanks for your help.

akim,

I think you’re missing part of Nigel’s point. it is not a way of reading and writing the preference plist – it’s a way of reading and writing the preference itself. The actual plist file is just a persistent backup of the preference.

You can only control how speechsynthesizer behaves in your own app, not in another.

Shane, please explain the difference between the writing a preference plist versus writing the preference itself. If I understand you correctly, the plist records the current status but does not alter the preferences to which the plist points to.

If my understanding is correct, then Nigel’s script re-writes the speechsynthesizer plist, which by itself does not change speechsynthesizer’s underlying preferences. The speechsynthesizer’s application only changes its preferences when it launches.

If my understanding has not gone awry, then the pivotal element of changing the application speechsynthesizer’s underlying preferences occurs invisible to the user and occurs only when the application itself launches.

If all of the above is correct, then any method of changing the preference plist should suffice, as long as the application speechsynthesizer re-launches.

Please correct my comprehension, or miscomprehension, where it has deviated from the actual mechanism.

Thank you for your help.

Imagine a system where there is no plist file: when an application is first launched it would use built-in settings, and as the user modified them they would be stored in memory. It would work fine – as long as the user never restarted. So imaging adding a mechanism to it where it would back the preferences up in a plist file, so they could be reloaded in the case of a restart. That’s roughly how it works – the plist files are effectively a sort of physical back-up of what is actually stored in a separate process.

No, it’s other way around – Nigel’s script changes the preferences themselves. These changes then get backed up to the plist file by the defaults daemon.

The actual defaults values are managed by a separate dedicated process – applications get them by talking to this process. They can get them at launch time, or any time after – there’s no fixed rule. Most applications don’t get values until they first need them. For a lot of settings it’s easier to get them every time, rather than store them internally. It’s up to whoever wrote the application, and what makes sense for the particular value.

That, or it happens the first time it needs to speak. Lazy loading is pretty common.

No, because the defaults process is still running, and you don’t know when it will clear its cached values. In some versions of the OS it can happen a long time after the relevant app has quit. It’s not going to load values if it already has them in memory – tThink of the plist file as a back-up.