upstream version 1.2.2
[debian/freetts] / com / sun / speech / freetts / jsapi / FreeTTSSynthesizer.java
1 /**
2  * Copyright 2003 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.freetts.jsapi;
9
10 import java.beans.PropertyVetoException;
11 import java.util.Enumeration;
12 import java.util.Iterator;
13 import java.util.Vector;
14 import java.util.logging.Logger;
15
16 import javax.speech.EngineException;
17 import javax.speech.EngineStateError;
18 import javax.speech.synthesis.SynthesizerModeDesc;
19
20 import com.sun.speech.engine.BaseEngineProperties;
21 import com.sun.speech.engine.synthesis.BaseSynthesizer;
22 import com.sun.speech.engine.synthesis.BaseSynthesizerProperties;
23 import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem;
24 import com.sun.speech.engine.synthesis.BaseVoice;
25 import com.sun.speech.freetts.OutputQueue;
26 import com.sun.speech.freetts.audio.AudioPlayer;
27
28 /**
29  * Provides  partial support for a JSAPI 1.0 synthesizer for the 
30  * FreeTTS speech synthesis system.
31  */
32 public class FreeTTSSynthesizer extends BaseSynthesizer {
33     /** Logger instance. */
34     private static final Logger LOGGER =
35         Logger.getLogger(FreeTTSSynthesizer.class.getName());
36
37     /**
38      * Reference to output thread.
39      */
40     OutputHandler outputHandler;
41
42     /**
43      * The currently active voice for this synthesizer
44      */
45     private FreeTTSVoice curVoice;
46
47     private AudioPlayer audio;
48
49     /**
50      * All voice output for this synthesizer goes through
51      * this central utterance queue
52      */
53     private OutputQueue outputQueue;
54
55     /**
56      * Creates a new Synthesizer in the DEALLOCATED state.
57      *
58      * @param desc describes the allowed mode of operations for this
59      *          synthesizer.
60      */
61     public FreeTTSSynthesizer(FreeTTSSynthesizerModeDesc desc) {
62         super(desc);
63         outputHandler = new OutputHandler();
64
65     }
66
67     /**
68      * Starts the output thread. The output thread is responsible for
69      * taking items off of the queue and sending them to the audio
70      * player.
71      *
72      * @throws EngineException if an allocation error occurs
73      */
74     protected void handleAllocate() throws EngineException {
75         long states[];
76         boolean ok = false;
77         FreeTTSSynthesizerModeDesc desc = (FreeTTSSynthesizerModeDesc)
78             getEngineModeDesc();
79
80
81         outputQueue = com.sun.speech.freetts.Voice.createOutputThread();
82
83         if (desc.getVoices().length > 0) {
84             FreeTTSVoice freettsVoice = (FreeTTSVoice) desc.getVoices()[0];
85             ok = setCurrentVoice(freettsVoice);
86         }
87
88
89
90         if (ok) {
91             synchronized (engineStateLock) {
92                 long newState = ALLOCATED | RESUMED;
93                 newState |= (outputHandler.isQueueEmpty()
94                              ? QUEUE_EMPTY
95                              : QUEUE_NOT_EMPTY);
96                 states = setEngineState(CLEAR_ALL_STATE, newState);
97             }
98             outputHandler.start();
99             postEngineAllocated(states[0], states[1]);
100         } else {
101             throw new EngineException("Can't allocate FreeTTS synthesizer");
102         }
103     }
104
105
106
107     /**
108      * Sets the given voice to be the current voice. If
109      * the voice cannot be loaded, this call has no affect.
110      *
111      * @param voice the new voice.
112      */
113     private boolean setCurrentVoice(FreeTTSVoice voice) 
114             throws EngineException {
115
116         com.sun.speech.freetts.Voice freettsVoice = voice.getVoice();
117         boolean ok = false;
118
119
120         if (!freettsVoice.isLoaded()) {
121             freettsVoice.setOutputQueue(outputQueue);
122             freettsVoice.allocate();
123             audio = freettsVoice.getAudioPlayer();
124             if (audio == null) {
125                 audio = new com.sun.speech.freetts.audio.JavaClipAudioPlayer();
126             }
127             if (audio == null) {
128                 throw new EngineException("Can't get audio player");
129             }
130             freettsVoice.setAudioPlayer(audio);
131         }
132
133         if (freettsVoice.isLoaded()) {
134             curVoice = voice;
135             ok = true;
136             // notify the world of potential property changes
137             FreeTTSSynthesizerProperties props =
138                 (FreeTTSSynthesizerProperties) getSynthesizerProperties();
139             props.checkForPropertyChanges();
140         }
141         return ok;
142     }
143
144     /**
145      * Handles a deallocation request. Cancels all pending items,
146      * terminates the output handler, and posts the state changes.
147      *
148      * @throws EngineException if a deallocation error occurs
149      */
150     protected void handleDeallocate() throws EngineException {
151         long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED);
152         outputHandler.cancelAllItems();
153         outputHandler.terminate();
154
155         // Close the audio. This should flush out any queued audio data
156
157         if (audio != null) {
158             audio.close();
159         }
160
161         outputQueue.close();
162         
163         postEngineDeallocated(states[0], states[1]);
164     }
165     
166     /**
167      * Factory method to create a BaseSynthesizerQueueItem.
168      *
169      * @return a queue item appropriate for this synthesizer
170      */
171     protected BaseSynthesizerQueueItem createQueueItem() {
172         return new FreeTTSSynthesizerQueueItem();
173     }
174
175     /**
176      * Returns an enumeration of the queue.
177      *
178      * @return an enumeration of the contents of the queue. This
179      *          enumeration contains FreeTTSSynthesizerQueueItem objects
180      *
181      * @throws EngineStateError if the engine was not in the proper
182      *                          state
183      */
184     public Enumeration enumerateQueue() throws EngineStateError {
185         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
186         return outputHandler.enumerateQueue();
187     }
188
189     /**
190      * Places an item on the speaking queue and send the queue update event.
191      *
192      * @param item      the item to place  in the queue
193      */
194     protected void appendQueue(BaseSynthesizerQueueItem item) {
195         outputHandler.appendQueue((FreeTTSSynthesizerQueueItem) item);
196     }
197
198     /**
199      * Cancels the item at the top of the queue.
200      *
201      * @throws EngineStateError if the synthesizer is not in the
202      *                          proper state
203      */
204     public void cancel() throws EngineStateError {
205         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
206         outputHandler.cancelItem();
207     }
208
209     /**
210      * Cancels a specific object on the queue.
211      * 
212      * @param source the object to cancel
213      *
214      * @throws IllegalArgumentException if the source object is not
215      *                                  currently in the queue
216      * @throws EngineStateError         the synthesizer is not in the
217      *                                  proper state
218      */
219     public void cancel(Object source)
220         throws IllegalArgumentException, EngineStateError {
221         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
222         outputHandler.cancelItem(source);
223     }
224
225     /**
226      * Cancels all items on the output queue.
227      *
228      * @throws EngineStateError
229      */
230     public void cancelAll() throws EngineStateError {
231         checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES);
232         outputHandler.cancelAllItems();
233     }
234
235     /**
236      * Pauses the output
237      */
238     protected void handlePause() {
239         audio.pause();
240     }    
241
242     /**
243      * Resumes the output
244      */
245     protected void handleResume() {
246         audio.resume();
247     }
248
249     /**
250      * Factory constructor for EngineProperties object.
251      * Gets the default speaking voice from the SynthesizerModeDesc.
252      * Takes the default prosody values (pitch, range, volume, rate)
253      * from the default voice.
254      * Override to set engine-specific defaults.
255      */
256     protected BaseEngineProperties createEngineProperties() {
257         SynthesizerModeDesc desc = (SynthesizerModeDesc)engineModeDesc;
258         FreeTTSVoice defaultVoice = (FreeTTSVoice)(desc.getVoices()[0]);
259         return new FreeTTSSynthesizerProperties(defaultVoice,
260                          defaultVoice.getPitch(),
261                          defaultVoice.getPitchRange(),
262                          defaultVoice.getSpeakingRate(),
263                          defaultVoice.getVolume());
264     }
265
266     /**
267      * Manages the FreeTTS synthesizer properties
268      */
269      class FreeTTSSynthesizerProperties extends BaseSynthesizerProperties {
270
271          /**
272           * Constructor 
273           * 
274           * @param defaultVoice the voice to use as the default for
275           *                     this synthesizer
276           * @param defaultPitch the default pitch in hertz
277           * @param defaultPitchRange the default range of pitch in
278           *                     hertz
279           * @param defaultSpeakingRate the default speaking rate in
280           *                     words per minute
281           * @param defaultVolume the default speaking volume 
282           *                     (0.0 to 1.0)
283           */
284          FreeTTSSynthesizerProperties(
285                  BaseVoice defaultVoice,
286                  float defaultPitch,
287                  float defaultPitchRange,
288                  float defaultSpeakingRate,
289                  float defaultVolume) {
290
291              super(defaultVoice, defaultPitch, defaultPitchRange, 
292                      defaultSpeakingRate, defaultVolume);
293          }
294
295         /**
296          * Resets the properties to their default values
297          */
298         public void reset() {
299             super.reset();
300         }
301
302         /**
303          * Checks to see if any properties have changed and if so
304          * fires the proper events
305          */
306         void checkForPropertyChanges() {
307             try {
308                 float pitch = getPitch();
309                 if (pitch != currentPitch) {
310                     super.setPitch(pitch);
311                 }
312                 
313                 float pitchRange = getPitchRange();
314                 if (pitchRange != currentPitchRange) {
315                     super.setPitchRange(pitchRange);
316                 }
317
318                 float volume = getVolume();
319                 if (volume != currentVolume) {
320                     super.setVolume(volume);
321                 }
322
323                 float speakingRate = getSpeakingRate();
324                 if (speakingRate != currentSpeakingRate) {
325                     super.setSpeakingRate(speakingRate);
326                 }
327
328             } catch (PropertyVetoException pve) {
329                 // the actual properties in the voices have
330                 // already changed to these new values so 
331                 // we should not expect a PropertyVetoException
332             }
333         }
334
335         /**
336          * Get the baseline pitch for synthesis
337          *
338          * @return the current pitch (in hertz)
339          */
340         public float getPitch() {
341             com.sun.speech.freetts.Voice voice = curVoice.getVoice();
342             return voice.getPitch();
343         }
344
345         /**
346          * Sets the voice to a voice that matches the given voice
347          *
348          * @param voice the voice that matches it
349          */
350         public void setVoice(javax.speech.synthesis.Voice voice) {
351             if (!curVoice.match(voice)) {
352                 // chase through the voice list and find the first match
353                 // and use that.  If no match, just ignore it.
354                 FreeTTSSynthesizerModeDesc desc =
355                     (FreeTTSSynthesizerModeDesc) getEngineModeDesc();
356                 javax.speech.synthesis.Voice voices[]  = desc.getVoices();
357                 for (int i = 0; i < voices.length; i++) {
358                     if (voices[i].match(voice)) {
359                         try {
360                             if (setCurrentVoice((FreeTTSVoice) voices[i])) {
361                                 try {
362                                     super.setVoice(voice);
363                                     break;
364                                 } catch (PropertyVetoException pve) {
365                                     continue;
366                                 }
367                             }
368                         } catch (EngineException ee) {
369                             System.err.println("Engine Exception: " +
370                                     ee.getMessage());
371                         }
372                     }
373                 }
374             }
375         }
376
377         /**
378          * Set the baseline pitch for the current synthesis voice.
379          *
380          * @param hertz sets the current pitch
381          *
382          * @throws PropertyVetoException if the synthesizer rejects or
383          *      limits the new value
384          */
385         public void setPitch(float hertz) throws PropertyVetoException {
386             if (hertz != getPitch()) {
387                 com.sun.speech.freetts.Voice voice = curVoice.getVoice();
388                 voice.setPitch(hertz);
389                 super.setPitch(hertz);
390             }
391         }
392
393
394         /**
395          * Get the pitch range for synthesis.
396          *
397          * @return the current range of pitch in hertz
398          */
399         public float getPitchRange() {
400             com.sun.speech.freetts.Voice voice = curVoice.getVoice();
401             return voice.getPitchRange();
402         }
403
404         /**
405          * Set the pitch range for the current synthesis voice.
406          *
407          * @throws PropertyVetoException if the synthesizer rejects or
408          *      limits the new value
409          */
410         public void setPitchRange(float hertz) throws PropertyVetoException {
411             if (hertz != getPitchRange()) {
412                 com.sun.speech.freetts.Voice voice = curVoice.getVoice();
413                 voice.setPitchRange(hertz);
414                 super.setPitchRange(hertz);
415             }
416         }
417
418         /**
419          * Gets the current target speaking rate.  
420          *
421          * @return the current speaking rate in words per minute
422          */
423         public float getSpeakingRate() {
424             com.sun.speech.freetts.Voice voice = curVoice.getVoice();
425             return voice.getRate();
426         }
427
428         /**
429          * Set the target speaking rate.
430          *
431          * @param wpm sets the target speaking rate in 
432          *      words per minute
433          *
434          * @throws PropertyVetoException if the synthesizer rejects or
435          *                              limits the new value
436          */
437         public void setSpeakingRate(float wpm) throws PropertyVetoException {
438             if (wpm != getSpeakingRate()) {
439                 com.sun.speech.freetts.Voice voice = curVoice.getVoice();
440                 voice.setRate(wpm);
441                 super.setSpeakingRate(wpm);
442             }
443         }
444
445         /**
446          * Gets the current volume.  
447          *
448          * @return the current volume setting (between 0 and 1.0)
449          */
450         public float getVolume() {
451             com.sun.speech.freetts.Voice voice = curVoice.getVoice();
452             return voice.getVolume();
453         }
454
455         /**
456          * Sets the volume
457          *
458          * @param volume the new volume setting (between 0 and 1)
459          *
460          * @throws PropertyVetoException if the synthesizer rejects or
461          *      limits the new value
462          */
463         public void setVolume(float volume) throws PropertyVetoException {
464             if (volume > 1.0f)
465                 volume = 1.0f;
466             else if (volume < 0.0f)
467                 volume = 0.0f;
468         
469             if (volume != getVolume()) {
470                 com.sun.speech.freetts.Voice voice = curVoice.getVoice();
471                 voice.setVolume(volume);
472                 super.setVolume(volume);
473             }
474         }
475      }
476
477
478     /**
479      * The OutputHandler is responsible for taking items off of the
480      * input queue and sending them to the current voice.
481      */
482     class OutputHandler extends Thread {
483         protected boolean done = false;
484         
485         /**
486          * Internal speech output queue that will contain a set of 
487          * FreeTTSSynthesizerQueueItems.
488          *
489          * @see BaseSynthesizerQueueItem
490          */
491         protected Vector queue;
492
493         /**
494          * Create a new OutputHandler for the given Synthesizer.
495          */
496         public OutputHandler() {
497             queue = new Vector();
498         }
499
500         /**
501          * shuts down this output handler
502          */
503         public synchronized void terminate() {
504             synchronized (queue) {
505                 done = true;
506                 queue.notify();
507             }
508         }
509         
510         /**
511          * Returns an enumeration of the queue
512          *
513          * @return the enumeration queue
514          */
515         public Enumeration enumerateQueue() {
516             synchronized(queue) {
517                 return queue.elements();
518             }
519         }
520
521         /**
522          * Determines if the input queue is empty
523          *
524          * @return true if the queue is empty; otherwise false
525          */
526         public boolean isQueueEmpty() {
527             synchronized(queue) {
528                 return queue.size() == 0;
529             }
530         }
531         
532         /**
533          * Add an item to be spoken to the output queue. Fires the
534          * appropriate queue events
535          *
536          * @param item the item to add to the queue
537          */
538         public void appendQueue(FreeTTSSynthesizerQueueItem item) {
539             boolean topOfQueueChanged;
540             synchronized(queue) {
541                 topOfQueueChanged = (queue.size() == 0);
542                 queue.addElement(item);
543                 queue.notifyAll();
544             }            
545             if (topOfQueueChanged) {
546                 long[] states = setEngineState(QUEUE_EMPTY,
547                                                QUEUE_NOT_EMPTY);
548                 postQueueUpdated(topOfQueueChanged, states[0], states[1]);
549             }
550         }
551
552         /**
553          * Cancel the current item
554          */
555         protected void cancelItem() {
556             FreeTTSSynthesizerQueueItem item = null;
557
558             synchronized(queue) {
559                 audio.cancel();
560                 if (queue.size() != 0) {
561                     item = (FreeTTSSynthesizerQueueItem) queue.remove(0);
562                     if (item != null) {
563                         // item.postSpeakableCancelled();
564                         item.cancelled();
565                         queueDrained();
566                     }
567                 }
568             }
569         }
570         
571         /**
572          * Cancel all items in the queue
573          */
574         protected void cancelAllItems() {
575             FreeTTSSynthesizerQueueItem item = null;
576             Vector copy;
577
578             synchronized(queue) {
579                 audio.cancel();
580                 copy = (Vector) queue.clone();
581                 queue.clear();
582                 queueDrained();
583             }
584             for (Iterator i = copy.iterator(); i.hasNext(); ) {
585                 item = (FreeTTSSynthesizerQueueItem) i.next();
586                 // item.postSpeakableCancelled();
587                 item.cancelled();
588             }
589         }
590         
591             
592         /**
593          * Cancel the given item.
594          *
595          * @param source the item to cancel.
596          */
597         protected void cancelItem(Object source) {
598             FreeTTSSynthesizerQueueItem item = null;
599             synchronized(queue) {
600                 int index = queue.indexOf(source);
601                 if (index == 0) {
602                     cancelItem();
603                 } else {
604                     item = (FreeTTSSynthesizerQueueItem) queue.remove(index);
605                     if (item != null) {
606                         // item.postSpeakableCancelled();
607                         item.cancelled();
608                         queueDrained();
609                     }
610                 }
611             }
612         }
613
614         /**
615          * Gets the next item from the queue and outputs it
616          */
617         public void run() {
618             FreeTTSSynthesizerQueueItem item;
619             while (!done) {
620                 item = getQueueItem();
621                 if (item != null) {
622                     outputItem(item);
623                     removeQueueItem(item); 
624                 }
625             }
626         }
627
628         /**
629          * Return, but do not remove, the first item on the queue.
630          *
631          * @return a queue item
632          */
633         protected FreeTTSSynthesizerQueueItem getQueueItem() {
634             FreeTTSSynthesizerQueueItem item = null;
635             synchronized(queue) {
636                 while (queue.size() == 0 && !done) {
637                     try {
638                         queue.wait();
639                     }
640                     catch (InterruptedException e) {
641                         LOGGER.severe("Unexpected interrupt");
642                         // Ignore interrupts and we'll loop around
643                     }
644                 }
645
646                 if (done) {
647                     return null;
648                 }
649                 item = (FreeTTSSynthesizerQueueItem) queue.elementAt(0);
650             }
651             item.postTopOfQueue();
652             return item;
653         }
654
655         /**
656          * removes the given item, posting the appropriate
657          * events. The item may have already been removed (due to a
658          * cancel).
659          *
660          * @param item the item to remove 
661          */
662         protected void removeQueueItem(FreeTTSSynthesizerQueueItem item) {
663             boolean queueEmptied = false;
664             synchronized(queue) {
665                 boolean found = queue.remove(item);
666                 if (found) {
667                     queueDrained();
668                 }
669             }
670         }
671
672         /**
673          * Should be called iff one or more items have been removed
674          * from the queue. Generates the appropriate state changes and
675          * events.
676          */
677         private void queueDrained() {
678             if (queue.size() == 0) {
679                 long[] states = setEngineState(QUEUE_NOT_EMPTY, QUEUE_EMPTY);
680                 postQueueEmptied(states[0], states[1]);
681             } else { 
682                 long[] states = setEngineState(QUEUE_NOT_EMPTY,
683                                                QUEUE_NOT_EMPTY);
684                 postQueueUpdated(true, states[0], states[1]);
685             }
686         }
687
688         /**
689          * Outputs the given queue item to the current voice
690          *
691          * @param item the item to output
692          */
693         protected void outputItem(FreeTTSSynthesizerQueueItem item) {
694             com.sun.speech.freetts.Voice voice = curVoice.getVoice();
695             voice.speak(item);
696         }
697     }
698 }