001/**
002 * Copyright 2001 Sun Microsystems, Inc.
003 * 
004 * See the file "license.terms" for information on usage and
005 * redistribution of this file, and for a DISCLAIMER OF ALL 
006 * WARRANTIES.
007 */
008package com.sun.speech.engine.synthesis.text;
009
010import java.util.Enumeration;
011import java.util.Vector;
012
013import javax.speech.Engine;
014import javax.speech.EngineStateError;
015import javax.speech.synthesis.SynthesizerModeDesc;
016
017import com.sun.speech.engine.synthesis.BaseSynthesizer;
018import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem;
019
020/**
021 * Supports a simple text-output-only JSAPI 1.0 <code>Synthesizer</code>.
022 * Intended for demonstration purposes for those developing JSAPI
023 * implementations.  It may also be useful to developers who want a
024 * JSAPI synthesizer that doesn't produce any noise.
025 */
026public class TextSynthesizer extends BaseSynthesizer {
027    /**
028     * Reference to output thread.
029     */
030    OutputHandler outputHandler = null;
031
032    /**
033     * Creates a new Synthesizer in the DEALLOCATED state.
034     *
035     * @param desc the operating mode
036     */
037    public TextSynthesizer(SynthesizerModeDesc desc) {
038        super(desc);
039        outputHandler = new OutputHandler();
040    }
041
042    /**
043     * Starts the output thread.
044     */
045    protected void handleAllocate() {
046        long states[];
047        synchronized (engineStateLock) {
048            long newState = ALLOCATED | RESUMED;
049            newState |= (outputHandler.isQueueEmpty()
050                         ? QUEUE_EMPTY
051                         : QUEUE_NOT_EMPTY);
052            states = setEngineState(CLEAR_ALL_STATE, newState);
053        }
054        outputHandler.start();
055        postEngineAllocated(states[0], states[1]);
056    }
057
058    /**
059     * Stops the output thread.
060     */
061    protected void handleDeallocate() {
062        long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED);
063        cancelAll();
064        outputHandler.terminate();
065        postEngineDeallocated(states[0], states[1]);
066    }
067    
068    /**
069     * Creates a TextSynthesizerQueueItem.
070     *
071     * @return a TextSynthesizerQueueItem
072     */
073    protected BaseSynthesizerQueueItem createQueueItem() {
074        return new TextSynthesizerQueueItem();
075    }
076
077    /**
078     * Returns an enumeration of the queue.
079     *
080     * @return
081     *   an <code>Enumeration</code> of the speech output queue or
082     *   <code>null</code>.
083     *
084     * @throws EngineStateError 
085     *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
086     *   <code>DEALLOCATING_RESOURCES</code> states
087     */
088    public Enumeration enumerateQueue() throws EngineStateError {
089        checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
090        return outputHandler.enumerateQueue();
091    }
092
093    /**
094     * Puts an item on the speaking queue and sends a queue updated
095     * event.  Expects only <code>TextSynthesizerQueueItems</code>.
096     *
097     * @param item the item to add to the queue
098     *
099     */
100    protected void appendQueue(BaseSynthesizerQueueItem item) {
101        outputHandler.appendQueue((TextSynthesizerQueueItem) item);
102    }
103
104    /**
105     * Cancels the item at the top of the queue.
106     *
107     * @throws EngineStateError 
108     *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
109     *   <code>DEALLOCATING_RESOURCES</code> states
110     */
111    public void cancel() throws EngineStateError {
112        checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
113        outputHandler.cancelItem();
114    }
115
116    /**
117     * Cancels a specific object on the queue.
118     *
119     * @param source
120     *    object to be removed from the speech output queue
121     *
122     * @throws IllegalArgumentException
123     *  if the source object is not found in the speech output queue.
124     * @throws EngineStateError 
125     *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
126     *   <code>DEALLOCATING_RESOURCES</code> states
127     */
128    public void cancel(Object source)
129        throws IllegalArgumentException, EngineStateError {
130        checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
131        outputHandler.cancelItem(source);
132    }
133
134    /**
135     * Cancels all items on the output queue.
136     *
137     * @throws EngineStateError 
138     *   if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 
139     *   <code>DEALLOCATING_RESOURCES</code> states
140     */
141    public void cancelAll() throws EngineStateError {
142        checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
143        outputHandler.cancelAllItems();
144    }
145
146    /**
147     * Pauses the output.
148     */
149    protected void handlePause() {
150        outputHandler.pauseItem();
151    }    
152
153    /**
154     * Resumes the output.
155     */
156    protected void handleResume() {
157        outputHandler.resumeItem();
158    }
159
160    /**
161     * The output device for a <code>TextSynthesizer</code>.  Sends
162     * all text to standard out.
163     */
164    public class OutputHandler extends Thread {
165        protected boolean done = false;
166        
167        /**
168         * Internal speech output queue that will contain a set of 
169         * TextSynthesizerQueueItems.
170         *
171         * @see BaseSynthesizerQueueItem
172         */
173        protected Vector queue;
174
175        /**
176         * The current item to speak.
177         */
178        TextSynthesizerQueueItem currentItem;
179    
180        /**
181         * Object to lock on for setting the current item.
182         */
183        protected Object currentItemLock = new Object();
184        
185        /**
186         * Current output "speaking" rate.
187         * Updated as /rate[166.3]/ controls are detected in the output text.
188         */
189        int rate = 100;
190
191        /**
192         * For the item at the top of the queue, the output command reflects 
193         * whether item should be PAUSE, RESUME, CANCEL.
194         */
195        protected int command;
196
197        protected final static int PAUSE = 0;
198        protected final static int RESUME = 1;
199        protected final static int CANCEL = 2;
200        protected final static int CANCEL_ALL = 3;
201        protected final static int CANCEL_COMPLETE = 4;
202
203        /**
204         * Object on which accesses to the command must synchronize.
205         */
206        protected Object commandLock = new Object();
207
208        /**
209         * Class constructor.
210         */
211        public OutputHandler() {
212            queue = new Vector();
213            currentItem = null;
214        }
215
216        /**
217         * Stops execution of the Thread.
218         */
219        public void terminate() {
220            done = true;
221        }
222        
223        /**
224         * Returns the current queue.
225         *
226         * @return the current queue
227         */
228        public Enumeration enumerateQueue() {
229            synchronized(queue) {
230                return queue.elements();
231            }
232        }
233
234        /**
235         * Determines if the queue is empty.
236         *
237         * @return <code>true</code> if the queue is empty
238         */
239        public boolean isQueueEmpty() {
240            synchronized(queue) {
241                return queue.size() == 0;
242            }
243        }
244        
245        /**
246         * Adds an item to be spoken to the output queue.
247         *
248         * @param item the item to be added
249         */
250        public void appendQueue(TextSynthesizerQueueItem item) {
251            boolean topOfQueueChanged;
252            synchronized(queue) {
253                topOfQueueChanged = (queue.size() == 0);
254                queue.addElement(item);
255                queue.notifyAll();
256            }            
257            if (topOfQueueChanged) {
258                long[] states = setEngineState(QUEUE_EMPTY,
259                                               QUEUE_NOT_EMPTY);
260                postQueueUpdated(topOfQueueChanged, states[0], states[1]);
261            }
262        }
263
264        /**
265         * Cancels the current item.
266         */
267        protected void cancelItem() {
268            cancelItem(CANCEL);
269        }
270        
271        /**
272         * Cancels all items.
273         */
274        protected void cancelAllItems() {
275            cancelItem(CANCEL_ALL);
276        }
277        
278        /**
279         * Cancels all or just the current item.
280         *
281         * @param cancelType <code>CANCEL</code> or <code>CANCEL_ALL</code>
282         */
283        protected void cancelItem(int cancelType) {
284            synchronized(queue) {
285                if (queue.size() == 0) {
286                    return;
287                }
288            }
289            synchronized(commandLock) {
290                command = cancelType;
291                commandLock.notifyAll();
292                while (command != CANCEL_COMPLETE) {
293                    try {
294                        commandLock.wait();
295                    } catch (InterruptedException e) {
296                        // Ignore interrupts and we'll loop around
297                    }
298                }
299                if (testEngineState(Engine.PAUSED)) {
300                    command = PAUSE;
301                } else {
302                    command = RESUME;
303                }
304                commandLock.notifyAll();
305            }
306        }
307            
308        /**
309         * Cancels the given item.
310         *
311         * @param source the item to cancel
312         */
313        protected void cancelItem(Object source) {
314//              synchronized(currentItemLock) {
315//                  if (currentItem.getSource() == source) {
316//                      cancelItem();
317//                  } else {
318//                      boolean queueEmptied;
319//                      synchronized(queue) {
320//                          for (int i = 0; i < queue.size(); i++) {
321//                              BaseSynthesizerQueueItem item =
322//                                  (BaseSynthesizerQueueItem)(queue.elementAt(i));
323//                              if (item.getSource() == source) {
324//                                  item.postSpeakableCancelled();
325//                                  queue.removeElementAt(i);
326//                              }
327//                          }
328//                          queueEmptied = queue.size() == 0;
329//                          queue.notifyAll();
330//                      }
331//                      if (queueEmptied) {
332//                          long[] states = setEngineState(QUEUE_NOT_EMPTY,
333//                                                         QUEUE_EMPTY);
334//                          postQueueEmptied(states[0], states[1]);
335//                      } else { 
336//                          long[] states = setEngineState(QUEUE_NOT_EMPTY,
337//                                                         QUEUE_NOT_EMPTY);
338//                          postQueueUpdated(false, states[0], states[1]);
339//                      }
340//                  }
341//              }
342        }
343
344        /**
345         * Pauses the output.
346         */
347        protected void pauseItem() {
348            synchronized(commandLock) {
349                if (command != PAUSE) {
350                    command = PAUSE;
351                    commandLock.notifyAll();
352                }
353            }
354        }
355
356        /**
357         * Resumes the output.
358         */
359        protected void resumeItem() {
360            synchronized(commandLock) {
361                if (command != RESUME) {
362                    command = RESUME;
363                    commandLock.notifyAll();
364                }
365            }
366        }
367
368        /**
369         * Controls output of text until terminate is called.
370         *
371         * @see #terminate
372         */
373        public void run() {
374            TextSynthesizerQueueItem item;
375            int currentCommand;
376            boolean queueEmptied;
377            
378            if (testEngineState(Engine.PAUSED)) {
379                command = PAUSE;
380            } else {
381                command = RESUME;
382            }
383            
384            while (!done) {
385                item = getQueueItem();
386                item.postTopOfQueue();
387                currentCommand = outputItem(item);
388                if (currentCommand == CANCEL_ALL) {
389                    Vector itemList = new Vector();
390                    itemList.add(item);
391                    synchronized(queue) {
392                        queue.remove(0);
393                        while (queue.size() > 0) {
394                            itemList.add(queue.remove(0));
395                        }
396                    }
397                    synchronized(commandLock) {
398                        command = CANCEL_COMPLETE;
399                        commandLock.notifyAll();
400                    }
401                    while (itemList.size() > 0) {
402                        item = (TextSynthesizerQueueItem)(itemList.remove(0));
403                        item.postSpeakableCancelled();
404                    }
405                    long[] states = setEngineState(QUEUE_NOT_EMPTY,
406                                                   QUEUE_EMPTY);
407                    postQueueEmptied(states[0], states[1]);
408                    continue;
409                } else if (currentCommand == CANCEL) {
410                    synchronized(commandLock) {
411                        command = CANCEL_COMPLETE;
412                        commandLock.notifyAll();
413                    }
414                    item.postSpeakableCancelled();
415                } else if ((currentCommand == PAUSE)
416                    || (currentCommand == RESUME)) {
417                    item.postSpeakableEnded();
418                }
419                
420                synchronized(queue) {
421                    queue.remove(0);
422                    queueEmptied = queue.size() == 0;
423                    queue.notifyAll();
424                }                
425
426                if (queueEmptied) {
427                    long[] states = setEngineState(QUEUE_NOT_EMPTY,
428                                                   QUEUE_EMPTY);
429                    postQueueEmptied(states[0], states[1]);
430                } else { 
431                    long[] states = setEngineState(QUEUE_NOT_EMPTY,
432                                                   QUEUE_NOT_EMPTY);
433                    postQueueUpdated(true, states[0], states[1]);
434                }
435            }
436        }
437
438        /**
439         * Returns, but does not remove, the first item on the queue.
440         *
441         * @return the first item on the queue
442         */
443        protected TextSynthesizerQueueItem getQueueItem() {
444            synchronized(queue) {
445                while (queue.size() == 0) {
446                    try {
447                        queue.wait();
448                    }
449                    catch (InterruptedException e) {
450                        // Ignore interrupts and we'll loop around
451                    }
452                }
453                return (TextSynthesizerQueueItem) queue.elementAt(0);
454            }
455        }
456
457        /**
458         * Starts outputting the item.  Returns the current command.
459         *
460         * @param item to be output
461         *
462         * @return the current command
463         */
464        protected int outputItem(TextSynthesizerQueueItem item) {
465            int currentCommand;
466            String engineText;
467            int engineTextIndex;
468            boolean wasPaused = false;
469            
470            System.out.println("----- BEGIN: "
471                               + item.getTypeString()
472                               + "-----");
473
474            engineText = item.getEngineText();
475            engineTextIndex = 0;
476
477            // [[[WDW - known danger with this loop -- the actual
478            // command could change between the times it is checked.
479            // For example, a call to pause followed by resume
480            // followed by a pause might go unnoticed.]]]
481            //
482            synchronized(commandLock) {
483                currentCommand = command;
484            }
485            while (engineTextIndex < engineText.length()) {
486                // On a pause, just hang out and wait.  If the text
487                // index is not 0, it means we've already started some
488                // processing on the current item.
489                //
490                if (currentCommand == PAUSE) {
491                    if (engineTextIndex > 0) {
492                        item.postSpeakablePaused();
493                        wasPaused = true;
494                    }
495                    synchronized(commandLock) {
496                        while (command == PAUSE) {
497                            try {
498                                commandLock.wait();
499                            } catch (InterruptedException e) {
500                                // Ignore interrupts and we'll loop around
501                            }
502                        }
503                        currentCommand = command;
504                    }
505                }
506
507                // On a resume, send out some text.  If the text index
508                // is 0, it means we are just starting processing of
509                // this speakable and we need to post an event saying
510                // so.
511                //
512                if (currentCommand == RESUME) {
513                    if (engineTextIndex == 0) {
514                        item.postSpeakableStarted();
515                    } else if (wasPaused) {
516                        item.postSpeakableResumed();
517                        wasPaused = false;
518                    }
519
520                    // If we get here, then we're processing text
521                    // Consider three options
522                    // 1. Next char is the start of a synth directive
523                    //    such as /emp[1]/
524                    // 2. Next char is the start of white space
525                    // 3. Next char is the start of plain text
526                    //
527                    if (isCommand(engineText, engineTextIndex)) {
528                        engineTextIndex = processCommand(item,
529                                                         engineText,
530                                                         engineTextIndex);
531                    } else if (isWhitespace(engineText, engineTextIndex)) {
532                        engineTextIndex = processWhitespace(engineText,
533                                                            engineTextIndex);
534                    } else {
535                        engineTextIndex = processNormalText(item,
536                                                            engineText,
537                                                            engineTextIndex);
538                    }
539                } else {
540                    // Otherwise, the command is CANCEL or CANCEL_ALL
541                    // and we should get out of this loop.
542                    //
543                    break;
544                }
545                synchronized(commandLock) {
546                    currentCommand = command;
547                }
548            }
549            
550            System.out.println("\n----- END: "
551                               + item.getTypeString()
552                               + "-----\n");
553            
554            return currentCommand;
555        }        
556            
557        /**
558         * Determines if the next thing in line is a command.
559         *
560         * @param engineText the text containing embedded commands
561         * @param index the current index
562         *
563         * @return <code>true</code> if the next thing in line is a command
564         */
565        protected boolean isCommand(String engineText, int index) {
566            if (!engineText.substring(index,index + 1).equals(
567                TextSynthesizerQueueItem.COMMAND_PREFIX)) {
568                return false;
569            }
570            
571            // Test for all known commands
572            //
573            for (int i = 0;
574                 i < TextSynthesizerQueueItem.ELEMENTS.length;
575                 i++) {
576                if (engineText.startsWith(
577                        TextSynthesizerQueueItem.COMMAND_PREFIX
578                        + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
579                    return true;
580                }
581            }
582            return false;
583        }
584    
585        /**
586         * Attempts to process a command starting at the next character
587         * in the synthesizer text. Returns the new index.
588         *
589         * @param item the current queue item
590         * @param engineText the text containing embedded commands
591         * @param index the current index
592         *
593         * @return the new index
594         */
595        protected int processCommand(TextSynthesizerQueueItem item,
596                                     String engineText, int index) {
597            // Test for all known commands
598            //
599            for (int i = 0;
600                 i < TextSynthesizerQueueItem.ELEMENTS.length;
601                 i++) {
602                if (engineText.startsWith(
603                        TextSynthesizerQueueItem.COMMAND_PREFIX
604                        + TextSynthesizerQueueItem.ELEMENTS[i], index)) {
605                    int endIndex = engineText.indexOf(
606                        TextSynthesizerQueueItem.COMMAND_SUFFIX, index+1)
607                        + 1;
608                    String commandText = engineText.substring(index, endIndex);
609                    System.out.print(commandText);
610                    System.out.flush();
611                    return endIndex;
612                }
613            }
614            return index;
615        }
616
617
618        /**
619         * Determines if there is whitespace at the current index.
620         *
621         * @param engineText the text containing embedded commands
622         * @param index the current index
623         *
624         * @return <code>true</code> if there is whitespace at the
625         *   current index
626         */
627        protected boolean isWhitespace(String engineText, int index) {
628            return Character.isWhitespace(engineText.charAt(index));
629        }
630
631        /**
632         * Processes whitespace at the current index in the synthesizer text.
633         * If next character is not whitespace, does nothing.
634         * If next character is whitespace, displays it and pauses
635         * briefly to simulate the speaking rate.
636         *
637         * @param engineText the text containing embedded commands
638         * @param index the current index
639         *
640         * @return the new index
641         */
642        protected int processWhitespace(String engineText, int index) {
643            // Identify full span of whitespace
644            //
645            int endIndex = index;
646            while (endIndex < engineText.length() && 
647                   Character.isWhitespace(engineText.charAt(endIndex))) {
648                endIndex++;
649            }
650
651            // Display the whitespace as plain text
652            //
653            System.out.print(engineText.substring(index, endIndex));
654            System.out.flush();
655
656            // Pause briefly with the delay determined by the current
657            // "speaking rate."  Convert the word-per-minute rate to
658            // millseconds.
659            //
660            try {
661                sleep(1000 * 60 / rate);
662            } catch (InterruptedException e) {
663                // Ignore any interruption
664            }
665
666            return endIndex;
667        }
668
669        /**
670         * Processes next set of characters in output up to whitespace
671         * or next '/' that could indicate the start of a command.
672         *
673         * @param item the current queue item
674         * @param engineText the text containing embedded commands
675         * @param index the current index
676         *
677         * @return the new index
678         */
679        protected int processNormalText(TextSynthesizerQueueItem item,
680                                        String engineText,
681                                        int index) {
682            String wordStr;
683            
684            // Find the end of the plain text
685            //
686            int endIndex = index+1;
687            while (endIndex < engineText.length() && 
688                   engineText.charAt(endIndex) != '/' &&
689                   !Character.isWhitespace(engineText.charAt(endIndex)))
690                endIndex++;
691
692            // Display the text in a plain format
693            //
694            wordStr = engineText.substring(index, endIndex);
695            item.postWordStarted(wordStr, index, endIndex);
696            System.out.print(wordStr);
697            System.out.flush();
698            return endIndex;
699        }
700    }
701}