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.freetts.audio;
009
010import java.io.IOException;
011import java.io.PipedInputStream;
012import java.io.PipedOutputStream;
013import java.util.logging.Level;
014import java.util.logging.Logger;
015
016import javax.sound.sampled.AudioFormat;
017import javax.sound.sampled.AudioInputStream;
018import javax.sound.sampled.AudioSystem;
019import javax.sound.sampled.Clip;
020import javax.sound.sampled.DataLine;
021import javax.sound.sampled.FloatControl;
022import javax.sound.sampled.LineEvent;
023import javax.sound.sampled.LineListener;
024import javax.sound.sampled.LineUnavailableException;
025
026import com.sun.speech.freetts.util.BulkTimer;
027import com.sun.speech.freetts.util.Timer;
028import com.sun.speech.freetts.util.Utilities;
029
030/**
031 * Provides an implementation of <code>AudioPlayer</code> that creates
032 * javax.sound.sampled audio clips and outputs them via the
033 * javax.sound API.  The interface provides a highly reliable audio
034 * output package. Since audio is batched and not sent to the audio
035 * layer until an entire utterance has been processed, this player has
036 * higher latency (50 msecs for a typical 4 second utterance).
037 *
038 */
039public class JavaClipAudioPlayer implements AudioPlayer {
040    /** Logger instance. */
041    private static final Logger LOGGER =
042        Logger.getLogger(JavaClipAudioPlayer.class.getName());
043    
044    private volatile boolean paused;
045    private volatile boolean cancelled = false;
046    private volatile Clip currentClip;
047
048    /** The current volume. */
049    private float volume = 1.0f; 
050    private boolean audioMetrics = false;
051    private final BulkTimer timer = new BulkTimer();
052    /** Default format is 8kHz. */
053    private AudioFormat defaultFormat = 
054        new AudioFormat(8000f, 16, 1, true, true);
055    private AudioFormat currentFormat = defaultFormat;
056    private boolean firstSample = true;
057    private boolean firstPlay = true;
058    private int curIndex = 0;
059    /** Data buffer to write the pure audio data to. */
060    private final PipedOutputStream outputData;
061    /** Audio input stream that is used to play back the audio. */
062    private AudioInputStream audioInput;
063    private final LineListener lineListener;
064
065    private long drainDelay;
066    private long openFailDelayMs;
067    private long totalOpenFailDelayMs;
068
069
070    /**
071     * Constructs a default JavaClipAudioPlayer 
072     */
073    public JavaClipAudioPlayer() {
074        drainDelay = Utilities.getLong
075            ("com.sun.speech.freetts.audio.AudioPlayer.drainDelay",
076             150L).longValue();
077        openFailDelayMs = Utilities.getLong
078            ("com.sun.speech.freetts.audio.AudioPlayer.openFailDelayMs",
079             0).longValue();
080        totalOpenFailDelayMs = Utilities.getLong
081            ("com.sun.speech.freetts.audio.AudioPlayer.totalOpenFailDelayMs",
082             0).longValue();
083        audioMetrics = Utilities.getBoolean(
084                "com.sun.speech.freetts.audio.AudioPlayer.showAudioMetrics");
085        setPaused(false);
086        outputData = new PipedOutputStream();
087        lineListener = new JavaClipLineListener();
088    }
089
090    /**
091     * Sets the audio format for this player
092     *
093     * @param format the audio format
094     *
095     * @throws UnsupportedOperationException if the line cannot be opened with
096     *     the given format
097     */
098    public synchronized void setAudioFormat(AudioFormat format) {
099        if (currentFormat.matches(format)) {
100            return;
101        }
102        currentFormat = format;
103        // Force the clip to be recreated if the format changed.
104        if (currentClip != null) {
105            currentClip = null;
106        }
107    }
108
109    /**
110     * Retrieves the audio format for this player
111     *
112     * @return format the audio format
113     */
114    public AudioFormat getAudioFormat() {
115        return currentFormat;
116    }
117
118    /**
119     * Pauses audio output.   All audio output is 
120     * stopped. Output can be resumed at the
121     * current point by calling <code>resume</code>. Output can be
122     * aborted by calling <code> cancel </code>
123     */
124    public void pause() {
125        if (!paused) {
126            setPaused(true);
127            if (currentClip != null) {
128                currentClip.stop();
129            }
130            synchronized (this) {
131                notifyAll();
132            }
133        }
134    }
135
136    /**
137     * Resumes playing audio after a pause.
138     *
139     */
140    public synchronized void resume() {
141        if (paused) {
142            setPaused(false);
143            if (currentClip != null) {
144                currentClip.start();
145            }
146            notifyAll();
147        }
148    }
149        
150    /**
151     * Cancels all queued audio. Any 'write' in process will return
152     * immediately false.
153     */
154    public void cancel() {
155        if (audioMetrics) {
156            timer.start("audioCancel");
157        }
158        if (currentClip != null) {
159            currentClip.stop();
160            currentClip.close();
161        }
162        synchronized (this) {
163            cancelled = true;
164            paused = false;
165            notifyAll();
166        }
167        if (audioMetrics) {
168            timer.stop("audioCancel");
169            Timer.showTimesShortTitle("");
170            timer.getTimer("audioCancel").showTimesShort(0);
171        }
172    }
173
174    /**
175     * Prepares for another batch of output. Larger groups of output
176     * (such as all output associated with a single FreeTTSSpeakable)
177     * should be grouped between a reset/drain pair.
178     */
179    public synchronized void reset() {
180        timer.start("speakableOut");
181    }
182
183    /**
184     * Waits for all queued audio to be played
185     *
186     * @return <code>true</code> if the write completed successfully, 
187     *          <code> false </code>if the write was cancelled.
188     */
189    public boolean drain()  {
190        timer.stop("speakableOut");
191        return true;
192    }
193
194    /**
195     * Closes this audio player
196     *
197     *  [[[ WORKAROUND TODO
198     *   The javax.sound.sampled drain is almost working properly.  On
199     *   linux, there is still a little bit of sound that needs to go
200     *   out, even after drain is called. Thus, the drainDelay. We
201     *   wait for a few hundred milliseconds while the data is really
202     *   drained out of the system
203     * ]]]
204     */
205    public synchronized void close() {
206        if (currentClip != null) {
207            currentClip.drain();
208            if (drainDelay > 0L) {
209                try {
210                    Thread.sleep(drainDelay);
211                } catch (InterruptedException e) {
212                }
213            }
214            currentClip.close();
215        }
216        notifyAll();
217    }        
218
219    /**
220     * Returns the current volume.
221     * @return the current volume (between 0 and 1)
222     */
223    public float getVolume() {
224        return volume;
225    }         
226
227    /**
228     * Sets the current volume.
229     * @param volume  the current volume (between 0 and 1)
230     */
231    public void setVolume(float volume) {
232        if (volume > 1.0f) {
233            volume = 1.0f;
234        }
235        if (volume < 0.0f) {
236            volume = 0.0f;
237        }
238        this.volume = volume;
239        if (currentClip != null) {
240            setVolume(currentClip, volume);
241        }
242    }
243
244    /**
245     * Sets pause mode
246     * @param state true if we are paused
247     */
248    private void setPaused(boolean state) {
249        paused = state;
250    }
251
252
253    /**
254     * Sets the volume on the given clip
255     *
256     * @param line the line to set the volume on
257     * @param vol the volume (range 0 to 1)
258     */
259    private void setVolume(Clip clip, float vol) {
260        if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
261            FloatControl volumeControl = 
262                (FloatControl) clip.getControl (FloatControl.Type.MASTER_GAIN);
263            float range = volumeControl.getMaximum() -
264            volumeControl.getMinimum();
265            volumeControl.setValue(vol * range + volumeControl.getMinimum());
266        }
267    }
268
269
270    /**
271     * Returns the current position in the output stream since the
272     * last <code>resetTime</code> 
273     *
274     * Currently not supported.
275     *
276     * @return the position in the audio stream in milliseconds
277     *
278     */
279    public synchronized long getTime()  {
280        return -1L;
281    }
282
283
284    /**
285     * Resets the time for this audio stream to zero
286     */
287    public synchronized void resetTime() {
288    }
289    
290
291    /**
292     * {@inheritDoc}
293     *
294     */
295    public synchronized void begin(int size) throws IOException {
296        timer.start("utteranceOutput");
297        cancelled = false;
298        curIndex = 0;
299        PipedInputStream in;
300        try {
301            in = new PipedInputStream(outputData);
302            audioInput = new AudioInputStream(in, currentFormat, size);
303        } catch (IOException e) {
304            LOGGER.warning(e.getLocalizedMessage());
305        }
306        while (paused && !cancelled) {
307            try {
308                wait();
309            } catch (InterruptedException ie) {
310                return;
311            }
312        }
313
314        timer.start("clipGeneration");
315
316        boolean opened = false;
317        long totalDelayMs = 0;
318        do {
319            // keep trying to open the clip until the specified
320            // delay is exceeded
321            try {
322                currentClip = getClip();
323                currentClip.open(audioInput);
324                opened = true;
325            } catch (LineUnavailableException lue) {
326                System.err.println("LINE UNAVAILABLE: " + 
327                                   "Format is " + currentFormat);
328                try {
329                    Thread.sleep(openFailDelayMs);
330                    totalDelayMs += openFailDelayMs;
331                } catch (InterruptedException ie) {
332                    return;
333                }
334            }
335        } while (!opened && totalDelayMs < totalOpenFailDelayMs);
336        
337        if (!opened) {
338            close();
339        } else {
340            setVolume(currentClip, volume);
341            if (audioMetrics && firstPlay) {
342                firstPlay = false;
343                timer.stop("firstPlay");
344                timer.getTimer("firstPlay");
345                Timer.showTimesShortTitle("");
346                timer.getTimer("firstPlay").showTimesShort(0);
347            }
348            currentClip.start();
349        }
350    }
351
352    /**
353     * Lazy instantiation of the clip.
354     * @return the clip to use.
355     * @throws LineUnavailableException
356     *         if the target line is not available.
357     */
358    private Clip getClip() throws LineUnavailableException {
359        if (currentClip == null) {
360            if (LOGGER.isLoggable(Level.FINE)) {
361                LOGGER.fine("creating new clip");
362            }
363            DataLine.Info info = new DataLine.Info(Clip.class, currentFormat);
364            try {
365                currentClip = (Clip) AudioSystem.getLine(info);
366                currentClip.addLineListener(lineListener);
367            } catch (SecurityException e) {
368                throw new LineUnavailableException(e.getLocalizedMessage());
369            } catch (IllegalArgumentException e) {
370                throw new LineUnavailableException(e.getLocalizedMessage());
371            }
372        }
373        return currentClip;
374    }
375
376    /**
377     * Marks the end a set of data. Audio data for a single utterance should be
378     * grouped between begin/end pairs.
379     * 
380     * @return <code>true</code> if the audio was output properly,
381     *         <code>false </code> if the output was canceled or interrupted.
382     */
383    public synchronized boolean end() {
384        boolean ok = true;
385        
386        if (cancelled) {
387            return false;
388        }
389        
390        if ((currentClip == null) || !currentClip.isOpen()) {
391            close();
392            ok = false;
393        } else {
394            setVolume(currentClip, volume);
395            if (audioMetrics && firstPlay) {
396                firstPlay = false;
397                timer.stop("firstPlay");
398                timer.getTimer("firstPlay");
399                Timer.showTimesShortTitle("");
400                timer.getTimer("firstPlay").showTimesShort(0);
401            }
402            try {
403                // wait for audio to complete
404                while (currentClip != null &&
405                       (currentClip.isRunning() || paused) && !cancelled) {
406                    wait();
407                }
408            } catch (InterruptedException ie) {
409                ok = false;
410            }
411            close();
412        }
413            
414        timer.stop("clipGeneration");
415        timer.stop("utteranceOutput");
416        ok &= !cancelled;
417        return ok;
418    }
419
420    /**
421     * {@inheritDoc}
422     */
423    public boolean write(byte[] audioData) throws IOException {
424        return write(audioData, 0, audioData.length);
425    }
426
427    /**
428     * {@inheritDoc}
429     */
430    public boolean write(byte[] bytes, int offset, int size)
431        throws IOException {
432        if (firstSample) {
433            firstSample = false;
434            timer.stop("firstAudio");
435            if (audioMetrics) {
436                Timer.showTimesShortTitle("");
437                timer.getTimer("firstAudio").showTimesShort(0);
438            }
439        }
440        outputData.write(bytes, offset, size);
441        curIndex += size;
442        return true;
443    }
444
445
446    /**
447     * Returns the name of this audio player
448     *
449     * @return the name of the audio player
450     */
451    public String toString() {
452        return "JavaClipAudioPlayer";
453    }
454
455
456    /**
457     * Shows metrics for this audio player
458     */
459    public void showMetrics() {
460        timer.show(toString());
461    }
462
463    /**
464     * Starts the first sample timer
465     */
466    public void startFirstSampleTimer() {
467        timer.start("firstAudio");
468        firstSample = true;
469        if (audioMetrics) {
470            timer.start("firstPlay");
471            firstPlay = true;
472        }
473    }
474
475
476    /**
477     * Provides a LineListener for this clas.
478     */
479    private class JavaClipLineListener implements LineListener {
480        /**
481         * Implements update() method of LineListener interface. Responds to the
482         * line events as appropriate.
483         * 
484         * @param event
485         *            the LineEvent to handle
486         */
487        public void update(LineEvent event) {
488            if (event.getType().equals(LineEvent.Type.START)) {
489                if (LOGGER.isLoggable(Level.FINE)) {
490                    LOGGER.fine(toString() + ": EVENT START");
491                }
492            } else if (event.getType().equals(LineEvent.Type.STOP)) {
493                if (LOGGER.isLoggable(Level.FINE)) {
494                    LOGGER.fine(toString() + ": EVENT STOP");
495                }
496                synchronized (JavaClipAudioPlayer.this) {
497                    JavaClipAudioPlayer.this.notifyAll();
498                }
499            } else if (event.getType().equals(LineEvent.Type.OPEN)) {
500                if (LOGGER.isLoggable(Level.FINE)) {
501                    LOGGER.fine(toString() + ": EVENT OPEN");
502                }
503            } else if (event.getType().equals(LineEvent.Type.CLOSE)) {
504                // When a clip is closed we no longer need it, so
505                // set currentClip to null and notify anyone who may
506                // be waiting on it.
507                if (LOGGER.isLoggable(Level.FINE)) {
508                    LOGGER.fine(toString() + ": EVENT CLOSE");
509                }
510                synchronized (JavaClipAudioPlayer.this) {
511                    JavaClipAudioPlayer.this.notifyAll();
512                }
513            }
514        }
515    }
516}