2 * Copyright 2001 Sun Microsystems, Inc.
4 * See the file "license.terms" for information on usage and
5 * redistribution of this file, and for a DISCLAIMER OF ALL
8 package com.sun.speech.freetts.audio;
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;
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;
26 import com.sun.speech.freetts.util.BulkTimer;
27 import com.sun.speech.freetts.util.Timer;
28 import com.sun.speech.freetts.util.Utilities;
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).
39 public class JavaClipAudioPlayer implements AudioPlayer {
40 /** Logger instance. */
41 private static final Logger LOGGER =
42 Logger.getLogger(JavaClipAudioPlayer.class.getName());
44 private volatile boolean paused;
45 private volatile boolean cancelled = false;
46 private volatile Clip currentClip;
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;
65 private long drainDelay;
66 private long openFailDelayMs;
67 private long totalOpenFailDelayMs;
71 * Constructs a default JavaClipAudioPlayer
73 public JavaClipAudioPlayer() {
74 drainDelay = Utilities.getLong
75 ("com.sun.speech.freetts.audio.AudioPlayer.drainDelay",
77 openFailDelayMs = Utilities.getLong
78 ("com.sun.speech.freetts.audio.AudioPlayer.openFailDelayMs",
80 totalOpenFailDelayMs = Utilities.getLong
81 ("com.sun.speech.freetts.audio.AudioPlayer.totalOpenFailDelayMs",
83 audioMetrics = Utilities.getBoolean(
84 "com.sun.speech.freetts.audio.AudioPlayer.showAudioMetrics");
86 outputData = new PipedOutputStream();
87 lineListener = new JavaClipLineListener();
91 * Sets the audio format for this player
93 * @param format the audio format
95 * @throws UnsupportedOperationException if the line cannot be opened with
98 public synchronized void setAudioFormat(AudioFormat format) {
99 if (currentFormat.matches(format)) {
102 currentFormat = format;
103 // Force the clip to be recreated if the format changed.
104 if (currentClip != null) {
110 * Retrieves the audio format for this player
112 * @return format the audio format
114 public AudioFormat getAudioFormat() {
115 return currentFormat;
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>
124 public void pause() {
127 if (currentClip != null) {
130 synchronized (this) {
137 * Resumes playing audio after a pause.
140 public synchronized void resume() {
143 if (currentClip != null) {
151 * Cancels all queued audio. Any 'write' in process will return
154 public void cancel() {
156 timer.start("audioCancel");
158 if (currentClip != null) {
162 synchronized (this) {
168 timer.stop("audioCancel");
169 Timer.showTimesShortTitle("");
170 timer.getTimer("audioCancel").showTimesShort(0);
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.
179 public synchronized void reset() {
180 timer.start("speakableOut");
184 * Waits for all queued audio to be played
186 * @return <code>true</code> if the write completed successfully,
187 * <code> false </code>if the write was cancelled.
189 public boolean drain() {
190 timer.stop("speakableOut");
195 * Closes this audio player
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
205 public synchronized void close() {
206 if (currentClip != null) {
208 if (drainDelay > 0L) {
210 Thread.sleep(drainDelay);
211 } catch (InterruptedException e) {
220 * Returns the current volume.
221 * @return the current volume (between 0 and 1)
223 public float getVolume() {
228 * Sets the current volume.
229 * @param volume the current volume (between 0 and 1)
231 public void setVolume(float volume) {
238 this.volume = volume;
239 if (currentClip != null) {
240 setVolume(currentClip, volume);
246 * @param state true if we are paused
248 private void setPaused(boolean state) {
254 * Sets the volume on the given clip
256 * @param line the line to set the volume on
257 * @param vol the volume (range 0 to 1)
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());
271 * Returns the current position in the output stream since the
272 * last <code>resetTime</code>
274 * Currently not supported.
276 * @return the position in the audio stream in milliseconds
279 public synchronized long getTime() {
285 * Resets the time for this audio stream to zero
287 public synchronized void resetTime() {
292 * Starts the output of a set of data. Audio data for a single
293 * utterance should be grouped between begin/end pairs.
295 * @param size the size of data between now and the end
298 public synchronized void begin(int size) {
299 timer.start("utteranceOutput");
304 in = new PipedInputStream(outputData);
305 audioInput = new AudioInputStream(in, currentFormat, size);
306 } catch (IOException e) {
307 LOGGER.warning(e.getLocalizedMessage());
309 while (paused && !cancelled) {
312 } catch (InterruptedException ie) {
317 timer.start("clipGeneration");
319 boolean opened = false;
320 long totalDelayMs = 0;
322 // keep trying to open the clip until the specified
325 currentClip = getClip();
326 currentClip.open(audioInput);
328 } catch (LineUnavailableException lue) {
329 System.err.println("LINE UNAVAILABLE: " +
330 "Format is " + currentFormat);
332 Thread.sleep(openFailDelayMs);
333 totalDelayMs += openFailDelayMs;
334 } catch (InterruptedException ie) {
335 ie.printStackTrace();
337 } catch (IOException e) {
338 LOGGER.warning(e.getLocalizedMessage());
340 } while (!opened && totalDelayMs < totalOpenFailDelayMs);
345 setVolume(currentClip, volume);
346 if (audioMetrics && firstPlay) {
348 timer.stop("firstPlay");
349 timer.getTimer("firstPlay");
350 Timer.showTimesShortTitle("");
351 timer.getTimer("firstPlay").showTimesShort(0);
358 * Lazy instantiation of the clip.
359 * @return the clip to use.
360 * @throws LineUnavailableException
361 * if the target line is not available.
363 private Clip getClip() throws LineUnavailableException {
364 if (currentClip == null) {
365 if (LOGGER.isLoggable(Level.FINE)) {
366 LOGGER.fine("creating new clip");
368 DataLine.Info info = new DataLine.Info(Clip.class, currentFormat);
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());
382 * Marks the end a set of data. Audio data for a single utterance should be
383 * grouped between begin/end pairs.
385 * @return <code>true</code> if the audio was output properly,
386 * <code>false </code> if the output was canceled or interrupted.
388 public synchronized boolean end() {
395 if ((currentClip == null) || !currentClip.isOpen()) {
399 setVolume(currentClip, volume);
400 if (audioMetrics && firstPlay) {
402 timer.stop("firstPlay");
403 timer.getTimer("firstPlay");
404 Timer.showTimesShortTitle("");
405 timer.getTimer("firstPlay").showTimesShort(0);
408 // wait for audio to complete
409 while (currentClip != null &&
410 (currentClip.isRunning() || paused) && !cancelled) {
413 } catch (InterruptedException ie) {
419 timer.stop("clipGeneration");
420 timer.stop("utteranceOutput");
427 * Writes the given bytes to the audio stream
429 * @param audioData audio data to write to the device
431 * @return <code>true</code> if the write completed successfully,
432 * <code> false </code>if the write was cancelled.
434 public boolean write(byte[] audioData) {
435 return write(audioData, 0, audioData.length);
439 * Writes the given bytes to the audio stream
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
445 * @return <code>true</code> if the write completed successfully,
446 * <code> false </code>if the write was canceled.
448 public boolean write(byte[] bytes, int offset, int size) {
451 timer.stop("firstAudio");
453 Timer.showTimesShortTitle("");
454 timer.getTimer("firstAudio").showTimesShort(0);
458 outputData.write(bytes, offset, size);
459 } catch (IOException e) {
460 LOGGER.warning(e.getLocalizedMessage());
469 * Returns the name of this audio player
471 * @return the name of the audio player
473 public String toString() {
474 return "JavaClipAudioPlayer";
479 * Shows metrics for this audio player
481 public void showMetrics() {
482 timer.show(toString());
486 * Starts the first sample timer
488 public void startFirstSampleTimer() {
489 timer.start("firstAudio");
492 timer.start("firstPlay");
499 * Provides a LineListener for this clas.
501 private class JavaClipLineListener implements LineListener {
503 * Implements update() method of LineListener interface. Responds to the
504 * line events as appropriate.
507 * the LineEvent to handle
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");
514 } else if (event.getType().equals(LineEvent.Type.STOP)) {
515 if (LOGGER.isLoggable(Level.FINE)) {
516 LOGGER.fine(toString() + ": EVENT STOP");
518 synchronized (JavaClipAudioPlayer.this) {
519 JavaClipAudioPlayer.this.notifyAll();
521 } else if (event.getType().equals(LineEvent.Type.OPEN)) {
522 if (LOGGER.isLoggable(Level.FINE)) {
523 LOGGER.fine(toString() + ": EVENT OPEN");
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
529 if (LOGGER.isLoggable(Level.FINE)) {
530 LOGGER.fine(toString() + ": EVENT CLOSE");
532 synchronized (JavaClipAudioPlayer.this) {
533 JavaClipAudioPlayer.this.notifyAll();