2 * Copyright 2001 Sun Microsystems, Inc.
4 * See the file "license.terms" for information on usage and
5 * redistribution of this file, and for a DISCLAIMER OF ALL
8 package com.sun.speech.engine.synthesis.text;
11 import java.util.Vector;
12 import java.util.Enumeration;
14 import javax.speech.Engine;
15 import javax.speech.EngineStateError;
16 import javax.speech.synthesis.Speakable;
17 import javax.speech.synthesis.SpeakableEvent;
18 import javax.speech.synthesis.SynthesizerModeDesc;
20 import com.sun.speech.engine.synthesis.BaseSynthesizer;
21 import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem;
24 * Supports a simple text-output-only JSAPI 1.0 <code>Synthesizer</code>.
25 * Intended for demonstration purposes for those developing JSAPI
26 * implementations. It may also be useful to developers who want a
27 * JSAPI synthesizer that doesn't produce any noise.
29 public class TextSynthesizer extends BaseSynthesizer {
31 * Reference to output thread.
33 OutputHandler outputHandler = null;
36 * Creates a new Synthesizer in the DEALLOCATED state.
38 * @param desc the operating mode
40 public TextSynthesizer(SynthesizerModeDesc desc) {
42 outputHandler = new OutputHandler();
46 * Starts the output thread.
48 protected void handleAllocate() {
50 synchronized (engineStateLock) {
51 long newState = ALLOCATED | RESUMED;
52 newState |= (outputHandler.isQueueEmpty()
55 states = setEngineState(CLEAR_ALL_STATE, newState);
57 outputHandler.start();
58 postEngineAllocated(states[0], states[1]);
62 * Stops the output thread.
64 protected void handleDeallocate() {
65 long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED);
67 outputHandler.terminate();
68 postEngineDeallocated(states[0], states[1]);
72 * Creates a TextSynthesizerQueueItem.
74 * @return a TextSynthesizerQueueItem
76 protected BaseSynthesizerQueueItem createQueueItem() {
77 return new TextSynthesizerQueueItem();
81 * Returns an enumeration of the queue.
84 * an <code>Enumeration</code> of the speech output queue or
87 * @throws EngineStateError
88 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or
89 * <code>DEALLOCATING_RESOURCES</code> states
91 public Enumeration enumerateQueue() throws EngineStateError {
92 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
93 return outputHandler.enumerateQueue();
97 * Puts an item on the speaking queue and sends a queue updated
98 * event. Expects only <code>TextSynthesizerQueueItems</code>.
100 * @param item the item to add to the queue
103 protected void appendQueue(BaseSynthesizerQueueItem item) {
104 outputHandler.appendQueue((TextSynthesizerQueueItem) item);
108 * Cancels the item at the top of the queue.
110 * @throws EngineStateError
111 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or
112 * <code>DEALLOCATING_RESOURCES</code> states
114 public void cancel() throws EngineStateError {
115 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
116 outputHandler.cancelItem();
120 * Cancels a specific object on the queue.
123 * object to be removed from the speech output queue
125 * @throws IllegalArgumentException
126 * if the source object is not found in the speech output queue.
127 * @throws EngineStateError
128 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or
129 * <code>DEALLOCATING_RESOURCES</code> states
131 public void cancel(Object source)
132 throws IllegalArgumentException, EngineStateError {
133 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
134 outputHandler.cancelItem(source);
138 * Cancels all items on the output queue.
140 * @throws EngineStateError
141 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or
142 * <code>DEALLOCATING_RESOURCES</code> states
144 public void cancelAll() throws EngineStateError {
145 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
146 outputHandler.cancelAllItems();
152 protected void handlePause() {
153 outputHandler.pauseItem();
157 * Resumes the output.
159 protected void handleResume() {
160 outputHandler.resumeItem();
164 * The output device for a <code>TextSynthesizer</code>. Sends
165 * all text to standard out.
167 public class OutputHandler extends Thread {
168 protected boolean done = false;
171 * Internal speech output queue that will contain a set of
172 * TextSynthesizerQueueItems.
174 * @see BaseSynthesizerQueueItem
176 protected Vector queue;
179 * The current item to speak.
181 TextSynthesizerQueueItem currentItem;
184 * Object to lock on for setting the current item.
186 protected Object currentItemLock = new Object();
189 * Current output "speaking" rate.
190 * Updated as /rate[166.3]/ controls are detected in the output text.
195 * For the item at the top of the queue, the output command reflects
196 * whether item should be PAUSE, RESUME, CANCEL.
198 protected int command;
200 protected final static int PAUSE = 0;
201 protected final static int RESUME = 1;
202 protected final static int CANCEL = 2;
203 protected final static int CANCEL_ALL = 3;
204 protected final static int CANCEL_COMPLETE = 4;
207 * Object on which accesses to the command must synchronize.
209 protected Object commandLock = new Object();
214 public OutputHandler() {
215 queue = new Vector();
220 * Stops execution of the Thread.
222 public void terminate() {
227 * Returns the current queue.
229 * @return the current queue
231 public Enumeration enumerateQueue() {
232 synchronized(queue) {
233 return queue.elements();
238 * Determines if the queue is empty.
240 * @return <code>true</code> if the queue is empty
242 public boolean isQueueEmpty() {
243 synchronized(queue) {
244 return queue.size() == 0;
249 * Adds an item to be spoken to the output queue.
251 * @param item the item to be added
253 public void appendQueue(TextSynthesizerQueueItem item) {
254 boolean topOfQueueChanged;
255 synchronized(queue) {
256 topOfQueueChanged = (queue.size() == 0);
257 queue.addElement(item);
260 if (topOfQueueChanged) {
261 long[] states = setEngineState(QUEUE_EMPTY,
263 postQueueUpdated(topOfQueueChanged, states[0], states[1]);
268 * Cancels the current item.
270 protected void cancelItem() {
277 protected void cancelAllItems() {
278 cancelItem(CANCEL_ALL);
282 * Cancels all or just the current item.
284 * @param cancelType <code>CANCEL</code> or <code>CANCEL_ALL</code>
286 protected void cancelItem(int cancelType) {
287 synchronized(queue) {
288 if (queue.size() == 0) {
292 synchronized(commandLock) {
293 command = cancelType;
294 commandLock.notifyAll();
295 while (command != CANCEL_COMPLETE) {
298 } catch (InterruptedException e) {
299 // Ignore interrupts and we'll loop around
302 if (testEngineState(Engine.PAUSED)) {
307 commandLock.notifyAll();
312 * Cancels the given item.
314 * @param source the item to cancel
316 protected void cancelItem(Object source) {
317 // synchronized(currentItemLock) {
318 // if (currentItem.getSource() == source) {
321 // boolean queueEmptied;
322 // synchronized(queue) {
323 // for (int i = 0; i < queue.size(); i++) {
324 // BaseSynthesizerQueueItem item =
325 // (BaseSynthesizerQueueItem)(queue.elementAt(i));
326 // if (item.getSource() == source) {
327 // item.postSpeakableCancelled();
328 // queue.removeElementAt(i);
331 // queueEmptied = queue.size() == 0;
332 // queue.notifyAll();
334 // if (queueEmptied) {
335 // long[] states = setEngineState(QUEUE_NOT_EMPTY,
337 // postQueueEmptied(states[0], states[1]);
339 // long[] states = setEngineState(QUEUE_NOT_EMPTY,
341 // postQueueUpdated(false, states[0], states[1]);
350 protected void pauseItem() {
351 synchronized(commandLock) {
352 if (command != PAUSE) {
354 commandLock.notifyAll();
360 * Resumes the output.
362 protected void resumeItem() {
363 synchronized(commandLock) {
364 if (command != RESUME) {
366 commandLock.notifyAll();
372 * Controls output of text until terminate is called.
377 TextSynthesizerQueueItem item;
379 boolean queueEmptied;
381 if (testEngineState(Engine.PAUSED)) {
388 item = getQueueItem();
389 item.postTopOfQueue();
390 currentCommand = outputItem(item);
391 if (currentCommand == CANCEL_ALL) {
392 Vector itemList = new Vector();
394 synchronized(queue) {
396 while (queue.size() > 0) {
397 itemList.add(queue.remove(0));
400 synchronized(commandLock) {
401 command = CANCEL_COMPLETE;
402 commandLock.notifyAll();
404 while (itemList.size() > 0) {
405 item = (TextSynthesizerQueueItem)(itemList.remove(0));
406 item.postSpeakableCancelled();
408 long[] states = setEngineState(QUEUE_NOT_EMPTY,
410 postQueueEmptied(states[0], states[1]);
412 } else if (currentCommand == CANCEL) {
413 synchronized(commandLock) {
414 command = CANCEL_COMPLETE;
415 commandLock.notifyAll();
417 item.postSpeakableCancelled();
418 } else if ((currentCommand == PAUSE)
419 || (currentCommand == RESUME)) {
420 item.postSpeakableEnded();
423 synchronized(queue) {
425 queueEmptied = queue.size() == 0;
430 long[] states = setEngineState(QUEUE_NOT_EMPTY,
432 postQueueEmptied(states[0], states[1]);
434 long[] states = setEngineState(QUEUE_NOT_EMPTY,
436 postQueueUpdated(true, states[0], states[1]);
442 * Returns, but does not remove, the first item on the queue.
444 * @return the first item on the queue
446 protected TextSynthesizerQueueItem getQueueItem() {
447 synchronized(queue) {
448 while (queue.size() == 0) {
452 catch (InterruptedException e) {
453 // Ignore interrupts and we'll loop around
456 return (TextSynthesizerQueueItem) queue.elementAt(0);
461 * Starts outputting the item. Returns the current command.
463 * @param item to be output
465 * @return the current command
467 protected int outputItem(TextSynthesizerQueueItem item) {
471 boolean wasPaused = false;
473 System.out.println("----- BEGIN: "
474 + item.getTypeString()
477 engineText = item.getEngineText();
480 // [[[WDW - known danger with this loop -- the actual
481 // command could change between the times it is checked.
482 // For example, a call to pause followed by resume
483 // followed by a pause might go unnoticed.]]]
485 synchronized(commandLock) {
486 currentCommand = command;
488 while (engineTextIndex < engineText.length()) {
489 // On a pause, just hang out and wait. If the text
490 // index is not 0, it means we've already started some
491 // processing on the current item.
493 if (currentCommand == PAUSE) {
494 if (engineTextIndex > 0) {
495 item.postSpeakablePaused();
498 synchronized(commandLock) {
499 while (command == PAUSE) {
502 } catch (InterruptedException e) {
503 // Ignore interrupts and we'll loop around
506 currentCommand = command;
510 // On a resume, send out some text. If the text index
511 // is 0, it means we are just starting processing of
512 // this speakable and we need to post an event saying
515 if (currentCommand == RESUME) {
516 if (engineTextIndex == 0) {
517 item.postSpeakableStarted();
518 } else if (wasPaused) {
519 item.postSpeakableResumed();
523 // If we get here, then we're processing text
524 // Consider three options
525 // 1. Next char is the start of a synth directive
527 // 2. Next char is the start of white space
528 // 3. Next char is the start of plain text
530 if (isCommand(engineText, engineTextIndex)) {
531 engineTextIndex = processCommand(item,
534 } else if (isWhitespace(engineText, engineTextIndex)) {
535 engineTextIndex = processWhitespace(engineText,
538 engineTextIndex = processNormalText(item,
543 // Otherwise, the command is CANCEL or CANCEL_ALL
544 // and we should get out of this loop.
548 synchronized(commandLock) {
549 currentCommand = command;
553 System.out.println("\n----- END: "
554 + item.getTypeString()
557 return currentCommand;
561 * Determines if the next thing in line is a command.
563 * @param engineText the text containing embedded commands
564 * @param index the current index
566 * @return <code>true</code> if the next thing in line is a command
568 protected boolean isCommand(String engineText, int index) {
569 if (!engineText.substring(index,index + 1).equals(
570 TextSynthesizerQueueItem.COMMAND_PREFIX)) {
574 // Test for all known commands
577 i < TextSynthesizerQueueItem.ELEMENTS.length;
579 if (engineText.startsWith(
580 TextSynthesizerQueueItem.COMMAND_PREFIX
581 + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
589 * Attempts to process a command starting at the next character
590 * in the synthesizer text. Returns the new index.
592 * @param item the current queue item
593 * @param engineText the text containing embedded commands
594 * @param index the current index
596 * @return the new index
598 protected int processCommand(TextSynthesizerQueueItem item,
599 String engineText, int index) {
600 // Test for all known commands
603 i < TextSynthesizerQueueItem.ELEMENTS.length;
605 if (engineText.startsWith(
606 TextSynthesizerQueueItem.COMMAND_PREFIX
607 + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
608 int endIndex = engineText.indexOf(
609 TextSynthesizerQueueItem.COMMAND_SUFFIX, index+1)
611 String commandText = engineText.substring(index, endIndex);
612 System.out.print(commandText);
622 * Determines if there is whitespace at the current index.
624 * @param engineText the text containing embedded commands
625 * @param index the current index
627 * @return <code>true</code> if there is whitespace at the
630 protected boolean isWhitespace(String engineText, int index) {
631 return Character.isWhitespace(engineText.charAt(index));
635 * Processes whitespace at the current index in the synthesizer text.
636 * If next character is not whitespace, does nothing.
637 * If next character is whitespace, displays it and pauses
638 * briefly to simulate the speaking rate.
640 * @param engineText the text containing embedded commands
641 * @param index the current index
643 * @return the new index
645 protected int processWhitespace(String engineText, int index) {
646 // Identify full span of whitespace
648 int endIndex = index;
649 while (endIndex < engineText.length() &&
650 Character.isWhitespace(engineText.charAt(endIndex))) {
654 // Display the whitespace as plain text
656 System.out.print(engineText.substring(index, endIndex));
659 // Pause briefly with the delay determined by the current
660 // "speaking rate." Convert the word-per-minute rate to
664 sleep(1000 * 60 / rate);
665 } catch (InterruptedException e) {
666 // Ignore any interruption
673 * Processes next set of characters in output up to whitespace
674 * or next '/' that could indicate the start of a command.
676 * @param item the current queue item
677 * @param engineText the text containing embedded commands
678 * @param index the current index
680 * @return the new index
682 protected int processNormalText(TextSynthesizerQueueItem item,
687 // Find the end of the plain text
689 int endIndex = index+1;
690 while (endIndex < engineText.length() &&
691 engineText.charAt(endIndex) != '/' &&
692 !Character.isWhitespace(engineText.charAt(endIndex)))
695 // Display the text in a plain format
697 wordStr = engineText.substring(index, endIndex);
698 item.postWordStarted(wordStr, index, endIndex);
699 System.out.print(wordStr);