]> git.gag.com Git - debian/freetts/blob - com/sun/speech/engine/synthesis/text/TextSynthesizer.java
upstream version 1.2.2
[debian/freetts] / com / sun / speech / engine / synthesis / text / TextSynthesizer.java
1 /**
2  * Copyright 2001 Sun Microsystems, Inc.
3  * 
4  * See the file "license.terms" for information on usage and
5  * redistribution of this file, and for a DISCLAIMER OF ALL 
6  * WARRANTIES.
7  */
8 package com.sun.speech.engine.synthesis.text;
9
10 import java.net.URL;
11 import java.util.Vector;
12 import java.util.Enumeration;
13
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;
19
20 import com.sun.speech.engine.synthesis.BaseSynthesizer;
21 import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem;
22
23 /**
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.
28  */
29 public class TextSynthesizer extends BaseSynthesizer {
30     /**
31      * Reference to output thread.
32      */
33     OutputHandler outputHandler = null;
34
35     /**
36      * Creates a new Synthesizer in the DEALLOCATED state.
37      *
38      * @param desc the operating mode
39      */
40     public TextSynthesizer(SynthesizerModeDesc desc) {
41         super(desc);
42         outputHandler = new OutputHandler();
43     }
44
45     /**
46      * Starts the output thread.
47      */
48     protected void handleAllocate() {
49         long states[];
50         synchronized (engineStateLock) {
51             long newState = ALLOCATED | RESUMED;
52             newState |= (outputHandler.isQueueEmpty()
53                          ? QUEUE_EMPTY
54                          : QUEUE_NOT_EMPTY);
55             states = setEngineState(CLEAR_ALL_STATE, newState);
56         }
57         outputHandler.start();
58         postEngineAllocated(states[0], states[1]);
59     }
60
61     /**
62      * Stops the output thread.
63      */
64     protected void handleDeallocate() {
65         long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED);
66         cancelAll();
67         outputHandler.terminate();
68         postEngineDeallocated(states[0], states[1]);
69     }
70     
71     /**
72      * Creates a TextSynthesizerQueueItem.
73      *
74      * @return a TextSynthesizerQueueItem
75      */
76     protected BaseSynthesizerQueueItem createQueueItem() {
77         return new TextSynthesizerQueueItem();
78     }
79
80     /**
81      * Returns an enumeration of the queue.
82      *
83      * @return
84      *   an <code>Enumeration</code> of the speech output queue or
85      *   <code>null</code>.
86      *
87      * @throws EngineStateError 
88      *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
89      *   <code>DEALLOCATING_RESOURCES</code> states
90      */
91     public Enumeration enumerateQueue() throws EngineStateError {
92         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
93         return outputHandler.enumerateQueue();
94     }
95
96     /**
97      * Puts an item on the speaking queue and sends a queue updated
98      * event.  Expects only <code>TextSynthesizerQueueItems</code>.
99      *
100      * @param item the item to add to the queue
101      *
102      */
103     protected void appendQueue(BaseSynthesizerQueueItem item) {
104         outputHandler.appendQueue((TextSynthesizerQueueItem) item);
105     }
106
107     /**
108      * Cancels the item at the top of the queue.
109      *
110      * @throws EngineStateError 
111      *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
112      *   <code>DEALLOCATING_RESOURCES</code> states
113      */
114     public void cancel() throws EngineStateError {
115         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
116         outputHandler.cancelItem();
117     }
118
119     /**
120      * Cancels a specific object on the queue.
121      *
122      * @param source
123      *    object to be removed from the speech output queue
124      *
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
130      */
131     public void cancel(Object source)
132         throws IllegalArgumentException, EngineStateError {
133         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
134         outputHandler.cancelItem(source);
135     }
136
137     /**
138      * Cancels all items on the output queue.
139      *
140      * @throws EngineStateError 
141      *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
142      *   <code>DEALLOCATING_RESOURCES</code> states
143      */
144     public void cancelAll() throws EngineStateError {
145         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
146         outputHandler.cancelAllItems();
147     }
148
149     /**
150      * Pauses the output.
151      */
152     protected void handlePause() {
153         outputHandler.pauseItem();
154     }    
155
156     /**
157      * Resumes the output.
158      */
159     protected void handleResume() {
160         outputHandler.resumeItem();
161     }
162
163     /**
164      * The output device for a <code>TextSynthesizer</code>.  Sends
165      * all text to standard out.
166      */
167     public class OutputHandler extends Thread {
168         protected boolean done = false;
169         
170         /**
171          * Internal speech output queue that will contain a set of 
172          * TextSynthesizerQueueItems.
173          *
174          * @see BaseSynthesizerQueueItem
175          */
176         protected Vector queue;
177
178         /**
179          * The current item to speak.
180          */
181         TextSynthesizerQueueItem currentItem;
182     
183         /**
184          * Object to lock on for setting the current item.
185          */
186         protected Object currentItemLock = new Object();
187         
188         /**
189          * Current output "speaking" rate.
190          * Updated as /rate[166.3]/ controls are detected in the output text.
191          */
192         int rate = 100;
193
194         /**
195          * For the item at the top of the queue, the output command reflects 
196          * whether item should be PAUSE, RESUME, CANCEL.
197          */
198         protected int command;
199
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;
205
206         /**
207          * Object on which accesses to the command must synchronize.
208          */
209         protected Object commandLock = new Object();
210
211         /**
212          * Class constructor.
213          */
214         public OutputHandler() {
215             queue = new Vector();
216             currentItem = null;
217         }
218
219         /**
220          * Stops execution of the Thread.
221          */
222         public void terminate() {
223             done = true;
224         }
225         
226         /**
227          * Returns the current queue.
228          *
229          * @return the current queue
230          */
231         public Enumeration enumerateQueue() {
232             synchronized(queue) {
233                 return queue.elements();
234             }
235         }
236
237         /**
238          * Determines if the queue is empty.
239          *
240          * @return <code>true</code> if the queue is empty
241          */
242         public boolean isQueueEmpty() {
243             synchronized(queue) {
244                 return queue.size() == 0;
245             }
246         }
247         
248         /**
249          * Adds an item to be spoken to the output queue.
250          *
251          * @param item the item to be added
252          */
253         public void appendQueue(TextSynthesizerQueueItem item) {
254             boolean topOfQueueChanged;
255             synchronized(queue) {
256                 topOfQueueChanged = (queue.size() == 0);
257                 queue.addElement(item);
258                 queue.notifyAll();
259             }            
260             if (topOfQueueChanged) {
261                 long[] states = setEngineState(QUEUE_EMPTY,
262                                                QUEUE_NOT_EMPTY);
263                 postQueueUpdated(topOfQueueChanged, states[0], states[1]);
264             }
265         }
266
267         /**
268          * Cancels the current item.
269          */
270         protected void cancelItem() {
271             cancelItem(CANCEL);
272         }
273         
274         /**
275          * Cancels all items.
276          */
277         protected void cancelAllItems() {
278             cancelItem(CANCEL_ALL);
279         }
280         
281         /**
282          * Cancels all or just the current item.
283          *
284          * @param cancelType <code>CANCEL</code> or <code>CANCEL_ALL</code>
285          */
286         protected void cancelItem(int cancelType) {
287             synchronized(queue) {
288                 if (queue.size() == 0) {
289                     return;
290                 }
291             }
292             synchronized(commandLock) {
293                 command = cancelType;
294                 commandLock.notifyAll();
295                 while (command != CANCEL_COMPLETE) {
296                     try {
297                         commandLock.wait();
298                     } catch (InterruptedException e) {
299                         // Ignore interrupts and we'll loop around
300                     }
301                 }
302                 if (testEngineState(Engine.PAUSED)) {
303                     command = PAUSE;
304                 } else {
305                     command = RESUME;
306                 }
307                 commandLock.notifyAll();
308             }
309         }
310             
311         /**
312          * Cancels the given item.
313          *
314          * @param source the item to cancel
315          */
316         protected void cancelItem(Object source) {
317 //              synchronized(currentItemLock) {
318 //                  if (currentItem.getSource() == source) {
319 //                      cancelItem();
320 //                  } else {
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);
329 //                              }
330 //                          }
331 //                          queueEmptied = queue.size() == 0;
332 //                          queue.notifyAll();
333 //                      }
334 //                      if (queueEmptied) {
335 //                          long[] states = setEngineState(QUEUE_NOT_EMPTY,
336 //                                                         QUEUE_EMPTY);
337 //                          postQueueEmptied(states[0], states[1]);
338 //                      } else { 
339 //                          long[] states = setEngineState(QUEUE_NOT_EMPTY,
340 //                                                         QUEUE_NOT_EMPTY);
341 //                          postQueueUpdated(false, states[0], states[1]);
342 //                      }
343 //                  }
344 //              }
345         }
346
347         /**
348          * Pauses the output.
349          */
350         protected void pauseItem() {
351             synchronized(commandLock) {
352                 if (command != PAUSE) {
353                     command = PAUSE;
354                     commandLock.notifyAll();
355                 }
356             }
357         }
358
359         /**
360          * Resumes the output.
361          */
362         protected void resumeItem() {
363             synchronized(commandLock) {
364                 if (command != RESUME) {
365                     command = RESUME;
366                     commandLock.notifyAll();
367                 }
368             }
369         }
370
371         /**
372          * Controls output of text until terminate is called.
373          *
374          * @see #terminate
375          */
376         public void run() {
377             TextSynthesizerQueueItem item;
378             int currentCommand;
379             boolean queueEmptied;
380             
381             if (testEngineState(Engine.PAUSED)) {
382                 command = PAUSE;
383             } else {
384                 command = RESUME;
385             }
386             
387             while (!done) {
388                 item = getQueueItem();
389                 item.postTopOfQueue();
390                 currentCommand = outputItem(item);
391                 if (currentCommand == CANCEL_ALL) {
392                     Vector itemList = new Vector();
393                     itemList.add(item);
394                     synchronized(queue) {
395                         queue.remove(0);
396                         while (queue.size() > 0) {
397                             itemList.add(queue.remove(0));
398                         }
399                     }
400                     synchronized(commandLock) {
401                         command = CANCEL_COMPLETE;
402                         commandLock.notifyAll();
403                     }
404                     while (itemList.size() > 0) {
405                         item = (TextSynthesizerQueueItem)(itemList.remove(0));
406                         item.postSpeakableCancelled();
407                     }
408                     long[] states = setEngineState(QUEUE_NOT_EMPTY,
409                                                    QUEUE_EMPTY);
410                     postQueueEmptied(states[0], states[1]);
411                     continue;
412                 } else if (currentCommand == CANCEL) {
413                     synchronized(commandLock) {
414                         command = CANCEL_COMPLETE;
415                         commandLock.notifyAll();
416                     }
417                     item.postSpeakableCancelled();
418                 } else if ((currentCommand == PAUSE)
419                     || (currentCommand == RESUME)) {
420                     item.postSpeakableEnded();
421                 }
422                 
423                 synchronized(queue) {
424                     queue.remove(0);
425                     queueEmptied = queue.size() == 0;
426                     queue.notifyAll();
427                 }                
428
429                 if (queueEmptied) {
430                     long[] states = setEngineState(QUEUE_NOT_EMPTY,
431                                                    QUEUE_EMPTY);
432                     postQueueEmptied(states[0], states[1]);
433                 } else { 
434                     long[] states = setEngineState(QUEUE_NOT_EMPTY,
435                                                    QUEUE_NOT_EMPTY);
436                     postQueueUpdated(true, states[0], states[1]);
437                 }
438             }
439         }
440
441         /**
442          * Returns, but does not remove, the first item on the queue.
443          *
444          * @return the first item on the queue
445          */
446         protected TextSynthesizerQueueItem getQueueItem() {
447             synchronized(queue) {
448                 while (queue.size() == 0) {
449                     try {
450                         queue.wait();
451                     }
452                     catch (InterruptedException e) {
453                         // Ignore interrupts and we'll loop around
454                     }
455                 }
456                 return (TextSynthesizerQueueItem) queue.elementAt(0);
457             }
458         }
459
460         /**
461          * Starts outputting the item.  Returns the current command.
462          *
463          * @param item to be output
464          *
465          * @return the current command
466          */
467         protected int outputItem(TextSynthesizerQueueItem item) {
468             int currentCommand;
469             String engineText;
470             int engineTextIndex;
471             boolean wasPaused = false;
472             
473             System.out.println("----- BEGIN: "
474                                + item.getTypeString()
475                                + "-----");
476
477             engineText = item.getEngineText();
478             engineTextIndex = 0;
479
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.]]]
484             //
485             synchronized(commandLock) {
486                 currentCommand = command;
487             }
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.
492                 //
493                 if (currentCommand == PAUSE) {
494                     if (engineTextIndex > 0) {
495                         item.postSpeakablePaused();
496                         wasPaused = true;
497                     }
498                     synchronized(commandLock) {
499                         while (command == PAUSE) {
500                             try {
501                                 commandLock.wait();
502                             } catch (InterruptedException e) {
503                                 // Ignore interrupts and we'll loop around
504                             }
505                         }
506                         currentCommand = command;
507                     }
508                 }
509
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
513                 // so.
514                 //
515                 if (currentCommand == RESUME) {
516                     if (engineTextIndex == 0) {
517                         item.postSpeakableStarted();
518                     } else if (wasPaused) {
519                         item.postSpeakableResumed();
520                         wasPaused = false;
521                     }
522
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
526                     //    such as /emp[1]/
527                     // 2. Next char is the start of white space
528                     // 3. Next char is the start of plain text
529                     //
530                     if (isCommand(engineText, engineTextIndex)) {
531                         engineTextIndex = processCommand(item,
532                                                          engineText,
533                                                          engineTextIndex);
534                     } else if (isWhitespace(engineText, engineTextIndex)) {
535                         engineTextIndex = processWhitespace(engineText,
536                                                             engineTextIndex);
537                     } else {
538                         engineTextIndex = processNormalText(item,
539                                                             engineText,
540                                                             engineTextIndex);
541                     }
542                 } else {
543                     // Otherwise, the command is CANCEL or CANCEL_ALL
544                     // and we should get out of this loop.
545                     //
546                     break;
547                 }
548                 synchronized(commandLock) {
549                     currentCommand = command;
550                 }
551             }
552             
553             System.out.println("\n----- END: "
554                                + item.getTypeString()
555                                + "-----\n");
556             
557             return currentCommand;
558         }        
559             
560         /**
561          * Determines if the next thing in line is a command.
562          *
563          * @param engineText the text containing embedded commands
564          * @param index the current index
565          *
566          * @return <code>true</code> if the next thing in line is a command
567          */
568         protected boolean isCommand(String engineText, int index) {
569             if (!engineText.substring(index,index + 1).equals(
570                 TextSynthesizerQueueItem.COMMAND_PREFIX)) {
571                 return false;
572             }
573             
574             // Test for all known commands
575             //
576             for (int i = 0;
577                  i < TextSynthesizerQueueItem.ELEMENTS.length;
578                  i++) {
579                 if (engineText.startsWith(
580                         TextSynthesizerQueueItem.COMMAND_PREFIX
581                         + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
582                     return true;
583                 }
584             }
585             return false;
586         }
587     
588         /**
589          * Attempts to process a command starting at the next character
590          * in the synthesizer text. Returns the new index.
591          *
592          * @param item the current queue item
593          * @param engineText the text containing embedded commands
594          * @param index the current index
595          *
596          * @return the new index
597          */
598         protected int processCommand(TextSynthesizerQueueItem item,
599                                      String engineText, int index) {
600             // Test for all known commands
601             //
602             for (int i = 0;
603                  i < TextSynthesizerQueueItem.ELEMENTS.length;
604                  i++) {
605                 if (engineText.startsWith(
606                         TextSynthesizerQueueItem.COMMAND_PREFIX
607                         + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
608                     int endIndex = engineText.indexOf(
609                         TextSynthesizerQueueItem.COMMAND_SUFFIX, index+1)
610                         + 1;
611                     String commandText = engineText.substring(index, endIndex);
612                     System.out.print(commandText);
613                     System.out.flush();
614                     return endIndex;
615                 }
616             }
617             return index;
618         }
619
620
621         /**
622          * Determines if there is whitespace at the current index.
623          *
624          * @param engineText the text containing embedded commands
625          * @param index the current index
626          *
627          * @return <code>true</code> if there is whitespace at the
628          *   current index
629          */
630         protected boolean isWhitespace(String engineText, int index) {
631             return Character.isWhitespace(engineText.charAt(index));
632         }
633
634         /**
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.
639          *
640          * @param engineText the text containing embedded commands
641          * @param index the current index
642          *
643          * @return the new index
644          */
645         protected int processWhitespace(String engineText, int index) {
646             // Identify full span of whitespace
647             //
648             int endIndex = index;
649             while (endIndex < engineText.length() && 
650                    Character.isWhitespace(engineText.charAt(endIndex))) {
651                 endIndex++;
652             }
653
654             // Display the whitespace as plain text
655             //
656             System.out.print(engineText.substring(index, endIndex));
657             System.out.flush();
658
659             // Pause briefly with the delay determined by the current
660             // "speaking rate."  Convert the word-per-minute rate to
661             // millseconds.
662             //
663             try {
664                 sleep(1000 * 60 / rate);
665             } catch (InterruptedException e) {
666                 // Ignore any interruption
667             }
668
669             return endIndex;
670         }
671
672         /**
673          * Processes next set of characters in output up to whitespace
674          * or next '/' that could indicate the start of a command.
675          *
676          * @param item the current queue item
677          * @param engineText the text containing embedded commands
678          * @param index the current index
679          *
680          * @return the new index
681          */
682         protected int processNormalText(TextSynthesizerQueueItem item,
683                                         String engineText,
684                                         int index) {
685             String wordStr;
686             
687             // Find the end of the plain text
688             //
689             int endIndex = index+1;
690             while (endIndex < engineText.length() && 
691                    engineText.charAt(endIndex) != '/' &&
692                    !Character.isWhitespace(engineText.charAt(endIndex)))
693                 endIndex++;
694
695             // Display the text in a plain format
696             //
697             wordStr = engineText.substring(index, endIndex);
698             item.postWordStarted(wordStr, index, endIndex);
699             System.out.print(wordStr);
700             System.out.flush();
701             return endIndex;
702         }
703     }
704 }