Introduction to the Java Speech API

Introduction to the Java Speech API

By Nathan Tippy, OCI Senior Software Engineer

March 2006


Speech Synthesis

Speech synthesis, also known as text-to-speech (TTS) conversion, is the process of converting text into human recognizable speech based on language and other vocal requirements. Speech synthesis can be used to enhance the user experience in many situations but care must be taken to ensure the user is comfortable with its use.

Speech synthesis has proven to be a great benefit in many ways. It is often used to assist the visually impaired as well as provide safety and efficiency in situations where the user needs to keep his eyes focused elsewhere. In the most successful applications of speech synthesis it is often central to the product requirements. If it is added on as an afterthought or a novelty it is rarely appreciated; people have high expectations when it comes to speech.

Natural sounding speech synthesis has been the goal of many development teams for a long time, yet it remains a significant challenge. People learn to speak at a very young age and continue to use their speaking and listening skills over the course of their lives, so it is very easy for people to recognize even the most minor flaws in speech synthesis.

As humans it is easy to take for granted our ability to speak but it is really a very complex process. There are a few different ways to implement a speech synthesis engine but in general they all complete the following steps:

 
This chart helps in understanding what goes on inside a speech synthesis engine but as a developer you will only need to concern yourself with the first step.

There are many voices available to developers today. Most of them are very good and a few are quite exceptional in how natural they sound. I put together a collection of both commercial and non-commercial voices so you can listen to them without having to setup or install anything.

Unfortunately the best voices (as of the time of this writing) are commercial so works produced using them can not be re-distributed without fees. Depending on how many voices you use and what you are using them for the annual costs for distribution rights can run from hundreds to thousands each year. Many vendors also provide different fee schedules for distributing applications that use a voice verses audio files and/or streams produced from the voices.

Java Speech API (JSAPI)

The goal of JSAPI is to enable cross-platform development of voice applications. The JSAPI enables developers to write applications that do not depend on the proprietary features of one platform or one speech engine.

Decoupling the engine from the application is important. As you can hear from the voice demo page; there is a wide variety of voices with different characteristics. Some users will be comfortable with a deep male voice while others may be more comfortable with a British female voice. The choice of speech engine and voice is subjective and may be expensive. In most cases, end users will use a single speech engine for multiple applications so they will expect any new speech enabled applications to integrate easily.

The Java Speech API 1.0 was first released by Sun in 1998 and defines packages for both speech recognition and speech synthesis. In order to remain brief the remainder of the article will focus on the speech synthesis package but if you would like to know more about speech recognition visit the CMU Sphinx sourceforge.net project.

All the JSAPI implementations available today are compliant with 1.0 or a subset of 1.0 but work is progressing on version 2.0 (JSR113) of the API. We will be using the open source implementation from FreeTTS for our demo app but there are other implementations such as the one from Cloudscape which provides support for the SAPI5 voices that Microsoft Windows uses.

Important Classes and Interfaces

Class: javax.speech.Central

This singleton class is the main interface for access to the speech engine facilities. It has a bad name (much too generic) but as part of the upgrade to version 2.0 they will be renaming it to EngineManager which is a much better name based on what it does.

For our example, we will only use the availableSynthesizers and createSynthesizer methods. Both of these methods need a mode description which is the next class we will use.

Class: javax.speech.synthesis.SynthesiserModeDesc

This simple bean holds all the required properties of the Synthesizer. When requesting a specific Synthesizer or a list of available Synthesizers this object can be passed in with specific properties to restrict the results to Synthesizers matching the defined properties only. The list of properties include the engine name, mode name, locale and running synthesizer.

The mode name property is not implemented with a type safe enumeration and it should only be set to the string value 'general' or 'time' when using the FreeTTS implementation. The mode name is specific to the engine, and in this case restricts the synthesizer to those that can speak any text or those that can only speak the time. If a time-only synthesizer is used for reading general text it will attempt to read it and print error messages when those phonemes it can't pronounce are encountered.

The locale property can be used to restrict international synthesizers which have support for many languages. See the MBROLA project for some international examples.

The running synthesizer property is used to limit the synthesizers returned to only those that are already loaded into memory. Because some synthesizers can take a long time to load into memory this feature may be helpful in limiting runtime delays.

Class: javax.speech.synthesis.Synthesiser

This class is used for converting text into speech using the selected voice. Synthesizers must be allocated before they can be used and this may take some time if high quality voices are supported which make use of large data files. It is recommended that the allocate method is called upon startup from a background thread. Call deallocate only when the application is about to exit. Once you have an allocated synthesizer it can be kept for the life of the application. Please note, in the chart below, the allocating and deallocating states that the synthesizer will be in while completing the allocate and deallocate operations, respectively.

Class: javax.speech.synthesis.Voice

This simple bean holds the properties of the voice. The name, age and gender can be set along with a Boolean to indicate that only voices already loaded into memory should be used. The setVoice method uses these properties to select a voice matching the required properties. After a voice is selected the getVoice method can be called to get the properties of the voice currently being used.

Note that the age and gender parameters are integers and do not use a typesafe enumeration. If an invalid value is used a PropertyVetoException will be thrown. The valid constants for these fields are found on the Voice class and they are.

Voice.GENDER_DONT_CARE
Voice.GENDER_FEMALE
Voice.GENDER_MALE
Voice.GENDER_NUTRAL
 
Voice.AGE_DONT_CARE
Voice.AGE_CHILD
Voice.AGE_TEENAGER
Voice.AGE_YOUNGER_ADULT
Voice.AGE_MIDDLE_ADULT
Voice.AGE_OLDER_ADULT
Voice.AGE_NEUTRAL

Interface: javax.speech.synthesis.Speakable

This interface should be implemented by any object that will produce marked up text that is to be spoken. The specification for JSML can be found on line and is very similar to W3Cs Speech Synthesis Markup Language Specification (SSML) which will be used instead of JSML for the 2.0 release.

Interface: javax.speech.synthesis.SpeakableListener

This interface should be implemented by any object wishing to listen to speech events. Notifications for events such as starting, stopping, pausing, resuming and others can be used to keep the application in sync with what the speech engine is doing.

Hello World

To try the demo you will need to set up the following:

Download freetts-1.2.1-bin.zip from http://sourceforge.net/projects/freetts/ 
FreeTTS only supports a subset of 1.0 but it works well and has an easy-to-understand voice. Our JSML inflections will be ignored but the markup will be parsed correctly.

Unzip the freetts-1.2.1-bin.zip file to a local folder.
The D:\apps\ folder will be used for this example

Go to D:\apps\freetts-1.2.1\lib and run jsapi.exe
This will create the jsapi.jar from Sun Microsystems. This is done because it uses a different license than FreeTTS's BSD license.

Add this new jar and all the other jars found in the D:\apps\freetts-1.2.1\lib folder to your path. This will give us the engine, the JSAPI interfaces and three voices to use in our demo.

Copy the D:\apps\freetts-1.2.1\speech.properties file to your %user.home% or %java.home%/lib folders. This file is used by JSAPI to determine which speech engine will be used.

Compile the three demo files below and run BriefVoiceDemo from the command line.

BriefVoiceDemo.java

  1. package com.ociweb.jsapi;
  2.  
  3. import java.beans.PropertyVetoException;
  4. import java.io.File;
  5. import java.text.DateFormat;
  6. import java.text.SimpleDateFormat;
  7. import java.util.Date;
  8. import java.util.Locale;
  9.  
  10. import javax.speech.AudioException;
  11. import javax.speech.Central;
  12. import javax.speech.EngineException;
  13. import javax.speech.EngineList;
  14. import javax.speech.EngineModeDesc;
  15. import javax.speech.EngineStateError;
  16. import javax.speech.synthesis.JSMLException;
  17. import javax.speech.synthesis.Speakable;
  18. import javax.speech.synthesis.SpeakableListener;
  19. import javax.speech.synthesis.Synthesizer;
  20. import javax.speech.synthesis.SynthesizerModeDesc;
  21. import javax.speech.synthesis.Voice;
  22.  
  23. public class BriefVoiceDemo {
  24.  
  25. Synthesizer synthesizer;
  26.  
  27. public static void main(String[] args) {
  28.  
  29. //default synthesizer values
  30. SynthesizerModeDesc modeDesc = new SynthesizerModeDesc(
  31. null, // engine name
  32. "general", // mode name use 'general' or 'time'
  33. Locale.US, // locale, see MBROLA Project for i18n examples
  34. null, // prefer a running synthesizer (Boolean)
  35. null); // preload these voices (Voice[])
  36.  
  37. //default voice values
  38. Voice voice = new Voice(
  39. "kevin16", //name for this voice
  40. Voice.AGE_DONT_CARE, //age for this voice
  41. Voice.GENDER_DONT_CARE,//gender for this voice
  42. null); //prefer a running voice (Boolean)
  43.  
  44. boolean error=false;
  45. for (int r=0;r<args.length;r++) {
  46. String token= args[r];
  47. String value= token.substring(2);
  48.  
  49. //overide some of the default synthesizer values
  50. if (token.startsWith("-E")) {
  51. modeDesc.setEngineName(value);
  52. } else if (token.startsWith("-M")) {
  53. modeDesc.setModeName(value);
  54. } else
  55. //overide some of the default voice values
  56. if (token.startsWith("-V")) {
  57. voice.setName(value);
  58. } else if (token.startsWith("-GF")) {
  59. voice.setGender(Voice.GENDER_FEMALE);
  60. } else if (token.startsWith("-GM")) {
  61. voice.setGender(Voice.GENDER_MALE);
  62. } else
  63. //dont recognize this value so flag it and break out
  64. {
  65. System.out.println(token+
  66. " was not recognized as a supported parameter");
  67. error = true;
  68. break;
  69. }
  70. }
  71.  
  72. //The example starts here
  73. BriefVoiceDemo briefExample = new BriefVoiceDemo();
  74. if (error) {
  75. System.out.println("BriefVoiceDemo -E<ENGINENAME> " +
  76. "-M<time|general> -V<VOICENAME> -GF -GM");
  77. //list all the available voices for the user
  78. briefExample.listAllVoices();
  79. System.exit(1);
  80. }
  81.  
  82. //select synthesizer by the required parameters
  83. briefExample.createSynthesizer(modeDesc);
  84. //print the details of the selected synthesizer
  85. briefExample.printSelectedSynthesizerModeDesc();
  86.  
  87. //allocate all the resources needed by the synthesizer
  88. briefExample.allocateSynthesizer();
  89.  
  90. //change the synthesisers state from PAUSED to RESUME
  91. briefExample.resumeSynthesizer();
  92.  
  93. //set the voice
  94. briefExample.selectVoice(voice);
  95. //print the details of the selected voice
  96. briefExample.printSelectedVoice();
  97.  
  98. //create a listener to be notified of speech events.
  99. SpeakableListener optionalListener= new BriefListener();
  100.  
  101. //The Date and Time can be spoken by any of the selected voices
  102. SimpleDateFormat formatter = new SimpleDateFormat("h mm");
  103. String dateText = "The time is now " + formatter.format(new Date());
  104. briefExample.speakTextSynchronously(dateText, optionalListener);
  105.  
  106. //General text like this can only be spoken by general voices
  107. if (briefExample.isModeGeneral()) {
  108. //speak plain text
  109. String plainText =
  110. "Hello World, This is an example of plain text," +
  111. " any markup like <jsml></jsml> will be spoken as is";
  112. briefExample.speakTextSynchronously(plainText, optionalListener);
  113.  
  114. //speak marked-up text from Speakable object
  115. Speakable speakableExample = new BriefSpeakable();
  116. briefExample.speakSpeakableSynchronously(speakableExample,
  117. optionalListener);
  118. }
  119. //must deallocate the synthesizer before leaving
  120. briefExample.deallocateSynthesizer();
  121. }
  122.  
  123. /**
  124.   * Select voice supported by this synthesizer that matches the required
  125.   * properties found in the voice object. If no matching voice can be
  126.   * found the call is ignored and the previous or default voice will be used.
  127.   *
  128.   * @param voice required voice properties.
  129.   */
  130. private void selectVoice(Voice voice) {
  131. try {
  132. synthesizer.getSynthesizerProperties().setVoice(voice);
  133. } catch (PropertyVetoException e) {
  134. System.out.println("unsupported voice");
  135. exit(e);
  136. }
  137. }
  138.  
  139. /**
  140.   * This method prepares the synthesizer for speech by moving it from the
  141.   * PAUSED state to the RESUMED state. This is needed because all newly
  142.   * created synthesizers start in the PAUSED state.
  143.   * See Pause/Resume state diagram.
  144.   *
  145.   * The pauseSynthesizer method is not shown but looks like you would expect
  146.   * and can be used to pause any speech in process.
  147.   */
  148. private void resumeSynthesizer() {
  149. try {
  150. //leave the PAUSED state, see state diagram
  151. synthesizer.resume();
  152. } catch (AudioException e) {
  153. exit(e);
  154. }
  155. }
  156.  
  157. /**
  158.   * The allocate method may take significant time to return depending on the
  159.   * size and capabilities of the selected synthesizer. In a production
  160.   * application this would probably be done on startup with a background thread.
  161.   *
  162.   * This method moves the synthesizer from the DEALLOCATED state to the
  163.   * ALLOCATING RESOURCES state and returns only after entering the ALLOCATED
  164.   * state. See Allocate/Deallocate state diagram.
  165.   */
  166. private void allocateSynthesizer() {
  167. //ensure that we only do this when in the DEALLOCATED state
  168. if ((synthesizer.getEngineState()&Synthesizer.DEALLOCATED)!=0)
  169. {
  170. try {
  171. //this call may take significant time
  172.  
  173. synthesizer.getEngineState();
  174. synthesizer.allocate();
  175. } catch (EngineException e) {
  176. e.printStackTrace();
  177. System.exit(1);
  178. } catch (EngineStateError e) {
  179. e.printStackTrace();
  180. System.exit(1);
  181. }
  182. }
  183. }
  184.  
  185. /**
  186.   * deallocate the synthesizer. This must be done before exiting or
  187.   * you will run the risk of having a resource leak.
  188.   *
  189.   * This method moves the synthesizer from the ALLOCATED state to the
  190.   * DEALLOCATING RESOURCES state and returns only after entering the
  191.   * DEALLOCATED state. See Allocate/Deallocate state diagram.
  192.   */
  193. private void deallocateSynthesizer() {
  194. //ensure that we only do this when in the ALLOCATED state
  195. if ((synthesizer.getEngineState()&Synthesizer.ALLOCATED)!=0)
  196. {
  197. try {
  198. //free all the resources used by the synthesizer
  199. synthesizer.deallocate();
  200. } catch (EngineException e) {
  201. e.printStackTrace();
  202. System.exit(1);
  203. } catch (EngineStateError e) {
  204. e.printStackTrace();
  205. System.exit(1);
  206. }
  207. }
  208. }
  209.  
  210. /**
  211.   * Helper method to ensure the synthesizer is always deallocated before
  212.   * existing the VM. The synthesiser may be holding substantial native
  213.   * resources that must be explicitly released.
  214.   *
  215.   * @param e exception to print before exiting.
  216.   */
  217. private void exit(Exception e) {
  218. e.printStackTrace();
  219. deallocateSynthesizer();
  220. System.exit(1);
  221. }
  222.  
  223. /**
  224.   * create a synthesiser with the required properties. The Central class
  225.   * requires the speech.properties file to be in the user.home or the
  226.   * java.home/lib folders before it can create a synthesizer.
  227.   *
  228.   * @param modeDesc required properties for the created synthesizer
  229.   */
  230. private void createSynthesizer(SynthesizerModeDesc modeDesc) {
  231. try {
  232. //Create a Synthesizer with specified required properties.
  233. //if none can be found null is returned.
  234. synthesizer = Central.createSynthesizer(modeDesc);
  235. }
  236. catch (IllegalArgumentException e1) {
  237. e1.printStackTrace();
  238. System.exit(1);
  239. } catch (EngineException e1) {
  240. e1.printStackTrace();
  241. System.exit(1);
  242. }
  243.  
  244. if (synthesizer==null) {
  245. System.out.println("Unable to create synthesizer with " +
  246. "the required properties");
  247. System.out.println();
  248. System.out.println("Be sure to check that the \"speech.properties\"" +
  249. " file is in one of these locations:");
  250. System.out.println(" user.home : "+System.getProperty("user.home"));
  251. System.out.println(" java.home/lib : "+System.getProperty("java.home")
  252. +File.separator+"lib");
  253. System.out.println();
  254. System.exit(1);
  255. }
  256. }
  257.  
  258. /**
  259.   * is the selected synthesizer capable of speaking general text
  260.   * @return is Mode General
  261.   */
  262. private boolean isModeGeneral() {
  263. String mode=this.synthesizer.getEngineModeDesc().getModeName();
  264. return "general".equals(mode);
  265. }
  266.  
  267. /**
  268.   * Speak the marked-up text provided by the Speakable object and wait for
  269.   * synthesisers queue to empty. Support for specific markup tags is
  270.   * dependent upon the selected synthesizer. The text will be read as
  271.   * though the mark up was not present if unsuppored tags are encounterd by
  272.   * the selected synthesizer.
  273.   *
  274.   * @param speakable
  275.   * @param optionalListener
  276.   */
  277. private void speakSpeakableSynchronously(
  278. Speakable speakable,
  279. SpeakableListener optionalListener) {
  280.  
  281. try {
  282. this.synthesizer.speak(speakable, optionalListener);
  283. } catch (JSMLException e) {
  284. exit(e);
  285. }
  286.  
  287. try {
  288. //wait for the queue to empty
  289. this.synthesizer.waitEngineState(Synthesizer.QUEUE_EMPTY);
  290.  
  291. } catch (IllegalArgumentException e) {
  292. exit(e);
  293. } catch (InterruptedException e) {
  294. exit(e);
  295. }
  296. }
  297.  
  298.  
  299.  
  300. /**
  301.   * Speak plain text 'as is' and wait until the synthesizer queue is empty
  302.   *
  303.   * @param plainText that will be spoken ignoring any markup
  304.   * @param optionalListener will be notified of voice events
  305.   */
  306. private void speakTextSynchronously(String plainText,
  307. SpeakableListener optionalListener) {
  308. this.synthesizer.speakPlainText(plainText, optionalListener);
  309. try {
  310. //wait for the queue to empty
  311. this.synthesizer.waitEngineState(Synthesizer.QUEUE_EMPTY);
  312.  
  313. } catch (IllegalArgumentException e) {
  314. exit(e);
  315. } catch (InterruptedException e) {
  316. exit(e);
  317. }
  318. }
  319.  
  320. /**
  321.   * Print all the properties of the selected voice
  322.   */
  323. private void printSelectedVoice() {
  324.  
  325. Voice voice = this.synthesizer.getSynthesizerProperties().getVoice();
  326. System.out.println();
  327. System.out.println("Selected Voice:"+voice.getName());
  328. System.out.println(" Style:"+voice.getStyle());
  329. System.out.println(" Gender:"+genderToString(voice.getGender()));
  330. System.out.println(" Age:"+ageToString(voice.getAge()));
  331. System.out.println();
  332. }
  333.  
  334. /**
  335.   * Helper method to convert gender constants to strings
  336.   * @param gender as defined by the Voice constants
  337.   * @return gender description
  338.   */
  339. private String genderToString(int gender) {
  340. switch (gender) {
  341. case Voice.GENDER_FEMALE:
  342. return "Female";
  343. case Voice.GENDER_MALE:
  344. return "Male";
  345. case Voice.GENDER_NEUTRAL:
  346. return "Neutral";
  347. case Voice.GENDER_DONT_CARE:
  348. default:
  349. return "Unknown";
  350. }
  351. }
  352.  
  353. /**
  354.   * Helper method to convert age constants to strings
  355.   * @param age as defined by the Voice constants
  356.   * @return age description
  357.   */
  358. private String ageToString(int age) {
  359. switch (age) {
  360. case Voice.AGE_CHILD:
  361. return "Child";
  362. case Voice.AGE_MIDDLE_ADULT:
  363. return "Middle Adult";
  364. case Voice.AGE_NEUTRAL:
  365. return "Neutral";
  366. case Voice.AGE_OLDER_ADULT:
  367. return "OlderAdult";
  368. case Voice.AGE_TEENAGER:
  369. return "Teenager";
  370. case Voice.AGE_YOUNGER_ADULT:
  371. return "Younger Adult";
  372. case Voice.AGE_DONT_CARE:
  373. default:
  374. return "Unknown";
  375. }
  376. }
  377.  
  378. /**
  379.   * Print all the properties of the selected synthesizer
  380.   */
  381. private void printSelectedSynthesizerModeDesc() {
  382. EngineModeDesc description = this.synthesizer.getEngineModeDesc();
  383. System.out.println();
  384. System.out.println("Selected Synthesizer:"+description.getEngineName());
  385. System.out.println(" Mode:"+description.getModeName());
  386. System.out.println(" Locale:"+description.getLocale());
  387. System.out.println(" IsRunning:"+description.getRunning());
  388. System.out.println();
  389. }
  390.  
  391. /**
  392.   * List all the available synthesizers and voices.
  393.   */
  394. public void listAllVoices() {
  395. System.out.println();
  396. System.out.println("All available JSAPI Synthesizers and Voices:");
  397.  
  398. //Do not set any properties so all the synthesizers will be returned
  399. SynthesizerModeDesc emptyDesc = new SynthesizerModeDesc();
  400. EngineList engineList = Central.availableSynthesizers(emptyDesc);
  401. //loop over all the synthesizers
  402. for (int e = 0; e < engineList.size(); e++) {
  403. SynthesizerModeDesc desc = (SynthesizerModeDesc) engineList.get(e);
  404. //loop over all the voices for this synthesizer
  405. Voice[] voices = desc.getVoices();
  406. for (int v = 0; v < voices.length; v++) {
  407. System.out.println(
  408. desc.getEngineName()+
  409. " Voice:"+voices[v].getName()+
  410. " Gender:"+genderToString(voices[v].getGender()));
  411. }
  412. }
  413. System.out.println();
  414. }
  415. }
  416.  

BriefSpeakable.java

  1. package com.ociweb.jsapi;
  2.  
  3. import javax.speech.synthesis.Speakable;
  4.  
  5. /**
  6.  * Simple Speakable
  7.  * Returns marked-up text to be spoken
  8.  */
  9. public class BriefSpeakable implements Speakable {
  10.  
  11. /**
  12.   * Returns marked-up text. The markup is used to help the vice engine.
  13.   */
  14. public String getJSMLText() {
  15. return "<jsml><para>This Speech <sayas class='literal'>API</sayas> " +
  16. "can integrate with <emp> most </emp> " +
  17. "of the speech engines on the market today.</para>" +
  18. "<break msecs='300'/><para>Keep on top of the latest developments " +
  19. "by reading all you can about " +
  20. "<sayas class='literal'>JSR113</sayas></para></jsml>";
  21. }
  22.  
  23. /**
  24.   * Implemented so the listener can print out the source
  25.   */
  26. public String toString() {
  27. return getJSMLText();
  28. }
  29.  
  30. }

BriefListener.java

  1. package com.ociweb.jsapi;
  2.  
  3. import javax.speech.synthesis.SpeakableEvent;
  4. import javax.speech.synthesis.SpeakableListener;
  5.  
  6. /**
  7.  * Simple SpeakableListener
  8.  * Prints event type and the source object's toString()
  9.  */
  10. public class BriefListener implements SpeakableListener {
  11.  
  12. private String formatEvent(SpeakableEvent event) {
  13. return event.paramString()+": "+event.getSource();
  14. }
  15.  
  16. public void markerReached(SpeakableEvent event) {
  17. System.out.println(formatEvent(event));
  18. }
  19.  
  20. public void speakableCancelled(SpeakableEvent event) {
  21. System.out.println(formatEvent(event));
  22. }
  23.  
  24. public void speakableEnded(SpeakableEvent event) {
  25. System.out.println(formatEvent(event));
  26. }
  27.  
  28. public void speakablePaused(SpeakableEvent event) {
  29. System.out.println(formatEvent(event));
  30. }
  31.  
  32. public void speakableResumed(SpeakableEvent event) {
  33. System.out.println(formatEvent(event));
  34. }
  35.  
  36. public void speakableStarted(SpeakableEvent event) {
  37. System.out.println(formatEvent(event));
  38. }
  39.  
  40. public void topOfQueue(SpeakableEvent event) {
  41. System.out.println(formatEvent(event));
  42. }
  43.  
  44. public void wordStarted(SpeakableEvent event) {
  45. System.out.println(formatEvent(event));
  46. }
  47. }

Conclusion

Further work on version 2.0 continues under JSR 113. The primary goal of the upcoming 2.0 spec is to bring JSAPI to J2ME but a few other overdue changes like class renaming have been done as well.

My impression after using JSAPI is that it would be much easier to use if it implemented unchecked exceptions. This would help make the code much easier to read and implement. Overall I think the API is on the right track and adds a needed abstraction layer for any project using speech synthesis.

As computer performance continues to improve and Java becomes embedded in more devices, interfaces that make computers easier for non-technical people such as voice synthesis and recognition will become ubiquitous. I recommend that anyone who might be working with embedded Java in the future keep an eye on JSR113.

References

secret