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