The Mac has a really great text-so-speech (TTS) engine built right in, but at first glance it’s only available at Apple’s whim in specific contexts — e.g. via a menu command in TextEdit, or system-wide through the accessibility settings. Seems grim, but we’re in luck — Apple, in their infinite generosity, have given us a command line program called “say”, which lets us invoke the TTS engine through the terminal. It’s super simple to use, just type the command and then the text you want, e.g.

say cosmic manifold

So that’s great, now what if we wanted to make a Processing sketch talk to us? In Java, as in most languages, there are ways to send commands to the terminal programmatically. By calling Runtime.getRuntime().exec("some command"); we can run any code we want on the terminal from within Processing. So to invoke the TTS engine from a Processing sketch, we can just create the say ... command line instruction in a string object, pass that into the runtime execution thing, which in turn handles the TTS conversion.

I’ve put together a small Processing class that makes it easy to add speech to your Processing sketches. It only works on Mac OS, won’t work in a web applet, and has only been tested in Mac OS 10.6. (I think the list of voices has changed since 10.5.)

Note that the since the class is quite simple and really just wraps up a few functions. I’ve set it up for static access, which means that you should never need to instantiate the class by calling something like TextToSpeech tts = new TextToSpeech() — and in fact that would be a Bad Idea. Instead, you can access the methods any time without any prior instantiation using static style syntax, e.g. TextToSpeech.say("cosmic manifold");.

Here’s the class and a sample sketch:

// Processing Text to Speech
// Eric Mika, Winter 2010
// Tested on Mac OS 10.6 only, possibly compatible with 10.5 (with modification)
// Adapted from code by Denis Meyer (CallToPower)
// Thanks to Mark Triant for the inspiring sample text

String script = "cosmic manifold";
int voiceIndex;
int voiceSpeed;

void setup() {
size(500, 500);
}

void draw() {
background(0);

// set the voice based on mouse y
voiceIndex = round(map(mouseY, 0, height, 0, TextToSpeech.voices.length - 1));

//set the vooice speed based on mouse X
voiceSpeed = mouseX;

// help text
fill(255);
text("Click to hear " + TextToSpeech.voices[voiceIndex] + "\nsay \"" + script + "\"\nat speed " + mouseX, 10, 20);

fill(128);
text("Mouse X sets voice speed.\nMouse Y sets voice.", 10, 65);
}

void mousePressed() {
// say something
TextToSpeech.say(script, TextToSpeech.voices[voiceIndex], voiceSpeed);
}


// the text to speech class
import java.io.IOException;

static class TextToSpeech extends Object {

// Store the voices, makes for nice auto-complete in Eclipse

// male voices
static final String ALEX = "Alex";
static final String BRUCE = "Bruce";
static final String FRED = "Fred";
static final String JUNIOR = "Junior";
static final String RALPH = "Ralph";

// female voices
static final String AGNES = "Agnes";
static final String KATHY = "Kathy";
static final String PRINCESS = "Princess";
static final String VICKI = "Vicki";
static final String VICTORIA = "Victoria";

// novelty voices
static final String ALBERT = "Albert";
static final String BAD_NEWS = "Bad News";
static final String BAHH = "Bahh";
static final String BELLS = "Bells";
static final String BOING = "Boing";
static final String BUBBLES = "Bubbles";
static final String CELLOS = "Cellos";
static final String DERANGED = "Deranged";
static final String GOOD_NEWS = "Good News";
static final String HYSTERICAL = "Hysterical";
static final String PIPE_ORGAN = "Pipe Organ";
static final String TRINOIDS = "Trinoids";
static final String WHISPER = "Whisper";
static final String ZARVOX = "Zarvox";

// throw them in an array so we can iterate over them / pick at random
static String[] voices = {
ALEX, BRUCE, FRED, JUNIOR, RALPH, AGNES, KATHY,
PRINCESS, VICKI, VICTORIA, ALBERT, BAD_NEWS, BAHH,
BELLS, BOING, BUBBLES, CELLOS, DERANGED, GOOD_NEWS,
HYSTERICAL, PIPE_ORGAN, TRINOIDS, WHISPER, ZARVOX
};

// this sends the "say" command to the terminal with the appropriate args
static void say(String script, String voice, int speed) {
try {
Runtime.getRuntime().exec(new String[] {"say", "-v", voice, "[[rate " + speed + "]]" + script});
}
catch (IOException e) {
System.err.println("IOException");
}
}

// Overload the say method so we can call it with fewer arguments and basic defaults
static void say(String script) {
// 200 seems like a resonable default speed
say(script, ALEX, 200);
}

}