upstream version 1.2.2
[debian/freetts] / com / sun / speech / freetts / audio / JavaClipAudioPlayer.java
1 /**
2  * Copyright 2001 Sun Microsystems, Inc.
3  * 
4  * See the file "license.terms" for information on usage and
5  * redistribution of this file, and for a DISCLAIMER OF ALL 
6  * WARRANTIES.
7  */
8 package com.sun.speech.freetts.audio;
9
10 import java.io.IOException;
11 import java.io.PipedInputStream;
12 import java.io.PipedOutputStream;
13 import java.util.logging.Level;
14 import java.util.logging.Logger;
15
16 import javax.sound.sampled.AudioFormat;
17 import javax.sound.sampled.AudioInputStream;
18 import javax.sound.sampled.AudioSystem;
19 import javax.sound.sampled.Clip;
20 import javax.sound.sampled.DataLine;
21 import javax.sound.sampled.FloatControl;
22 import javax.sound.sampled.LineEvent;
23 import javax.sound.sampled.LineListener;
24 import javax.sound.sampled.LineUnavailableException;
25
26 import com.sun.speech.freetts.util.BulkTimer;
27 import com.sun.speech.freetts.util.Timer;
28 import com.sun.speech.freetts.util.Utilities;
29
30 /**
31  * Provides an implementation of <code>AudioPlayer</code> that creates
32  * javax.sound.sampled audio clips and outputs them via the
33  * javax.sound API.  The interface provides a highly reliable audio
34  * output package. Since audio is batched and not sent to the audio
35  * layer until an entire utterance has been processed, this player has
36  * higher latency (50 msecs for a typical 4 second utterance).
37  *
38  */
39 public class JavaClipAudioPlayer implements AudioPlayer {
40     /** Logger instance. */
41     private static final Logger LOGGER =
42         Logger.getLogger(JavaClipAudioPlayer.class.getName());
43     
44     private volatile boolean paused;
45     private volatile boolean cancelled = false;
46     private volatile Clip currentClip;
47
48     /** The current volume. */
49     private float volume = 1.0f; 
50     private boolean audioMetrics = false;
51     private final BulkTimer timer = new BulkTimer();
52     /** Default format is 8kHz. */
53     private AudioFormat defaultFormat = 
54         new AudioFormat(8000f, 16, 1, true, true);
55     private AudioFormat currentFormat = defaultFormat;
56     private boolean firstSample = true;
57     private boolean firstPlay = true;
58     private int curIndex = 0;
59     /** Data buffer to write the pure audio data to. */
60     private final PipedOutputStream outputData;
61     /** Audio input stream that is used to play back the audio. */
62     private AudioInputStream audioInput;
63     private final LineListener lineListener;
64
65     private long drainDelay;
66     private long openFailDelayMs;
67     private long totalOpenFailDelayMs;
68
69
70     /**
71      * Constructs a default JavaClipAudioPlayer 
72      */
73     public JavaClipAudioPlayer() {
74         drainDelay = Utilities.getLong
75             ("com.sun.speech.freetts.audio.AudioPlayer.drainDelay",
76              150L).longValue();
77         openFailDelayMs = Utilities.getLong
78             ("com.sun.speech.freetts.audio.AudioPlayer.openFailDelayMs",
79              0).longValue();
80         totalOpenFailDelayMs = Utilities.getLong
81             ("com.sun.speech.freetts.audio.AudioPlayer.totalOpenFailDelayMs",
82              0).longValue();
83         audioMetrics = Utilities.getBoolean(
84                 "com.sun.speech.freetts.audio.AudioPlayer.showAudioMetrics");
85         setPaused(false);
86         outputData = new PipedOutputStream();
87         lineListener = new JavaClipLineListener();
88     }
89
90     /**
91      * Sets the audio format for this player
92      *
93      * @param format the audio format
94      *
95      * @throws UnsupportedOperationException if the line cannot be opened with
96      *     the given format
97      */
98     public synchronized void setAudioFormat(AudioFormat format) {
99         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      *  Starts the output of a set of data. Audio data for a single
293      *  utterance should be grouped between begin/end pairs.
294      *
295      * @param size the size of data between now and the end
296      *
297      */
298     public synchronized void begin(int size) {
299         timer.start("utteranceOutput");
300         cancelled = false;
301         curIndex = 0;
302         PipedInputStream in;
303         try {
304             in = new PipedInputStream(outputData);
305             audioInput = new AudioInputStream(in, currentFormat, size);
306         } catch (IOException e) {
307             LOGGER.warning(e.getLocalizedMessage());
308         }
309         while (paused && !cancelled) {
310             try {
311                 wait();
312             } catch (InterruptedException ie) {
313                 return;
314             }
315         }
316
317         timer.start("clipGeneration");
318         
319         boolean opened = false;
320         long totalDelayMs = 0;
321         do {
322             // keep trying to open the clip until the specified
323             // delay is exceeded
324             try {
325                 currentClip = getClip();
326                 currentClip.open(audioInput);
327                 opened = true;
328             } catch (LineUnavailableException lue) {
329                 System.err.println("LINE UNAVAILABLE: " + 
330                                    "Format is " + currentFormat);
331                 try {
332                     Thread.sleep(openFailDelayMs);
333                     totalDelayMs += openFailDelayMs;
334                 } catch (InterruptedException ie) {
335                     ie.printStackTrace();
336                 }
337             } catch (IOException e) {
338                 LOGGER.warning(e.getLocalizedMessage());
339             }
340         } while (!opened && totalDelayMs < totalOpenFailDelayMs);
341         
342         if (!opened) {
343             close();
344         } else {
345             setVolume(currentClip, volume);
346             if (audioMetrics && firstPlay) {
347                 firstPlay = false;
348                 timer.stop("firstPlay");
349                 timer.getTimer("firstPlay");
350                 Timer.showTimesShortTitle("");
351                 timer.getTimer("firstPlay").showTimesShort(0);
352             }
353             currentClip.start();
354         }
355     }
356
357     /**
358      * Lazy instantiation of the clip.
359      * @return the clip to use.
360      * @throws LineUnavailableException
361      *         if the target line is not available.
362      */
363     private Clip getClip() throws LineUnavailableException {
364         if (currentClip == null) {
365             if (LOGGER.isLoggable(Level.FINE)) {
366                 LOGGER.fine("creating new clip");
367             }
368             DataLine.Info info = new DataLine.Info(Clip.class, currentFormat);
369             try {
370                 currentClip = (Clip) AudioSystem.getLine(info);
371                 currentClip.addLineListener(lineListener);
372             } catch (SecurityException e) {
373                 throw new LineUnavailableException(e.getLocalizedMessage());
374             } catch (IllegalArgumentException e) {
375                 throw new LineUnavailableException(e.getLocalizedMessage());
376             }
377         }
378         return currentClip;
379     }
380
381     /**
382      * Marks the end a set of data. Audio data for a single utterance should be
383      * grouped between begin/end pairs.
384      * 
385      * @return <code>true</code> if the audio was output properly,
386      *         <code>false </code> if the output was canceled or interrupted.
387      */
388     public synchronized boolean end() {
389         boolean ok = true;
390         
391         if (cancelled) {
392             return false;
393         }
394         
395         if ((currentClip == null) || !currentClip.isOpen()) {
396             close();
397             ok = false;
398         } else {
399             setVolume(currentClip, volume);
400             if (audioMetrics && firstPlay) {
401                 firstPlay = false;
402                 timer.stop("firstPlay");
403                 timer.getTimer("firstPlay");
404                 Timer.showTimesShortTitle("");
405                 timer.getTimer("firstPlay").showTimesShort(0);
406             }
407             try {
408                 // wait for audio to complete
409                 while (currentClip != null &&
410                        (currentClip.isRunning() || paused) && !cancelled) {
411                     wait();
412                 }
413             } catch (InterruptedException ie) {
414                 ok = false;
415             }
416             close();
417         }
418             
419         timer.stop("clipGeneration");
420         timer.stop("utteranceOutput");
421         ok &= !cancelled;
422         return ok;
423     }
424     
425     
426     /**
427      * Writes the given bytes to the audio stream
428      *
429      * @param audioData audio data to write to the device
430      *
431      * @return <code>true</code> if the write completed successfully, 
432      *          <code> false </code>if the write was cancelled.
433      */
434     public boolean write(byte[] audioData) {
435         return write(audioData, 0, audioData.length);
436     }
437     
438     /**
439      * Writes the given bytes to the audio stream
440      *
441      * @param bytes audio data to write to the device
442      * @param offset the offset into the buffer
443      * @param size the size into the buffer
444      *
445      * @return <code>true</code> if the write completed successfully, 
446      *          <code> false </code>if the write was canceled.
447      */
448     public boolean write(byte[] bytes, int offset, int size) {
449         if (firstSample) {
450             firstSample = false;
451             timer.stop("firstAudio");
452             if (audioMetrics) {
453                 Timer.showTimesShortTitle("");
454                 timer.getTimer("firstAudio").showTimesShort(0);
455             }
456         }
457         try {
458             outputData.write(bytes, offset, size);
459         } catch (IOException e) {
460             LOGGER.warning(e.getLocalizedMessage());
461             return false;
462         }
463         curIndex += size;
464         return true;
465     }
466
467
468     /**
469      * Returns the name of this audio player
470      *
471      * @return the name of the audio player
472      */
473     public String toString() {
474         return "JavaClipAudioPlayer";
475     }
476
477
478     /**
479      * Shows metrics for this audio player
480      */
481     public void showMetrics() {
482         timer.show(toString());
483     }
484
485     /**
486      * Starts the first sample timer
487      */
488     public void startFirstSampleTimer() {
489         timer.start("firstAudio");
490         firstSample = true;
491         if (audioMetrics) {
492             timer.start("firstPlay");
493             firstPlay = true;
494         }
495     }
496
497
498     /**
499      * Provides a LineListener for this clas.
500      */
501     private class JavaClipLineListener implements LineListener {
502         /**
503          * Implements update() method of LineListener interface. Responds to the
504          * line events as appropriate.
505          * 
506          * @param event
507          *            the LineEvent to handle
508          */
509         public void update(LineEvent event) {
510             if (event.getType().equals(LineEvent.Type.START)) {
511                 if (LOGGER.isLoggable(Level.FINE)) {
512                     LOGGER.fine(toString() + ": EVENT START");
513                 }
514             } else if (event.getType().equals(LineEvent.Type.STOP)) {
515                 if (LOGGER.isLoggable(Level.FINE)) {
516                     LOGGER.fine(toString() + ": EVENT STOP");
517                 }
518                 synchronized (JavaClipAudioPlayer.this) {
519                     JavaClipAudioPlayer.this.notifyAll();
520                 }
521             } else if (event.getType().equals(LineEvent.Type.OPEN)) {
522                 if (LOGGER.isLoggable(Level.FINE)) {
523                     LOGGER.fine(toString() + ": EVENT OPEN");
524                 }
525             } else if (event.getType().equals(LineEvent.Type.CLOSE)) {
526                 // When a clip is closed we no longer need it, so
527                 // set currentClip to null and notify anyone who may
528                 // be waiting on it.
529                 if (LOGGER.isLoggable(Level.FINE)) {
530                     LOGGER.fine(toString() + ": EVENT CLOSE");
531                 }
532                 synchronized (JavaClipAudioPlayer.this) {
533                     JavaClipAudioPlayer.this.notifyAll();
534                 }
535             }
536         }
537     }
538 }