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 javax.sound.sampled.AudioFormat;
11 import javax.sound.sampled.AudioSystem;
12 import javax.sound.sampled.DataLine;
13 import javax.sound.sampled.FloatControl;
14 import javax.sound.sampled.LineEvent;
15 import javax.sound.sampled.LineListener;
16 import javax.sound.sampled.LineUnavailableException;
17 import javax.sound.sampled.SourceDataLine;
19 import com.sun.speech.freetts.util.BulkTimer;
20 import com.sun.speech.freetts.util.Timer;
21 import com.sun.speech.freetts.util.Utilities;
24 * Streams audio to java audio. This class provides a low latency
25 * method of sending audio output through the javax.sound audio API.
26 * Audio data is sent in small sets to the audio system allowing it to
27 * be played soon after it is generated.
29 * Unfortunately, the current release of the JDK (JDK 1.4 beta 2) has
31 * the implementation of 'SourceDataLine.drain'. A workaround solution that
32 * sleep/waits on SourceDataLine.isActive is used here instead. To
33 * disable the work around (i.e use the real 'drain') set the
37 * com.sun.speech.freetts.audio.AudioPlayer.drainWorksProperly;
39 * to <code>true</code>.
41 * If the workaround is enabled, the line.isActive method will be
42 * performed periodically. The period of the test can be controlled
47 * com.sun.speech.freetts.audio.AudioPlayer.drainDelay"
56 * com.sun.speech.freetts.audio.AudioPlayer.bufferSize"
60 * Controls the audio buffer size, it defaults to 8192
63 * Even with this drain work around, there are some issues with this
64 * class. The workaround drain is not completely reliable.
65 * A <code>resume</code> following a <code>pause</code> does not
66 * always continue at the proper position in the audio. On a rare
67 * occasion, sound output will be repeated a number of times. This may
68 * be related to bug 4421330 in the Bug Parade database.
72 public class JavaStreamingAudioPlayer implements AudioPlayer {
74 private volatile boolean paused;
75 private volatile boolean done = false;
76 private volatile boolean cancelled = false;
78 private SourceDataLine line;
79 private float volume = 1.0f; // the current volume
80 private long timeOffset = 0L;
81 private BulkTimer timer = new BulkTimer();
83 // default format is 8khz
84 private AudioFormat defaultFormat =
85 new AudioFormat(8000f, 16, 1, true, true);
86 private AudioFormat currentFormat = defaultFormat;
88 private boolean debug = false;
89 private boolean audioMetrics = false;
90 private boolean firstSample = true;
92 private long cancelDelay;
93 private long drainDelay;
94 private long openFailDelayMs;
95 private long totalOpenFailDelayMs;
97 private Object openLock = new Object();
98 private Object lineLock = new Object();
102 * controls the buffering to java audio
104 private final static int AUDIO_BUFFER_SIZE = Utilities.getInteger(
105 "com.sun.speech.freetts.audio.AudioPlayer.bufferSize", 8192).intValue();
108 * controls the number of bytes of audio to write to the buffer
109 * for each call to write()
111 private final static int BYTES_PER_WRITE = Utilities.getInteger
112 ("com.sun.speech.freetts.audio.AudioPlayer.bytesPerWrite", 160).intValue();
116 * Constructs a default JavaStreamingAudioPlayer
118 public JavaStreamingAudioPlayer() {
119 debug = Utilities.getBoolean
120 ("com.sun.speech.freetts.audio.AudioPlayer.debug");
121 cancelDelay = Utilities.getLong
122 ("com.sun.speech.freetts.audio.AudioPlayer.cancelDelay",
124 drainDelay = Utilities.getLong
125 ("com.sun.speech.freetts.audio.AudioPlayer.drainDelay",
127 openFailDelayMs = Utilities.getLong
128 ("com.sun.speech.freetts.audio.AudioPlayer.openFailDelayMs",
130 totalOpenFailDelayMs = Utilities.getLong
131 ("com.sun.speech.freetts.audio.AudioPlayer.totalOpenFailDelayMs",
133 audioMetrics = Utilities.getBoolean
134 ("com.sun.speech.freetts.audio.AudioPlayer.showAudioMetrics");
141 * Sets the audio format for this player
143 * @param format the audio format
145 * @throws UnsupportedOperationException if the line cannot be opened with
148 public synchronized void setAudioFormat(AudioFormat format) {
149 currentFormat = format;
150 debugPrint("AF changed to " + format);
155 * Gets the audio format for this player
157 * @return format the audio format
159 public AudioFormat getAudioFormat() {
160 return currentFormat;
164 * Starts the first sample timer
166 public void startFirstSampleTimer() {
167 timer.start("firstAudio");
175 * @param format the format for the audio
177 * @throws UnsupportedOperationException if the line cannot be opened with
180 private synchronized void openLine(AudioFormat format) {
181 synchronized (lineLock) {
187 DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
189 boolean opened = false;
190 long totalDelayMs = 0;
194 line = (SourceDataLine) AudioSystem.getLine(info);
195 line.addLineListener(new JavaStreamLineListener());
197 synchronized (openLock) {
198 line.open(format, AUDIO_BUFFER_SIZE);
201 } catch (InterruptedException ie) {
202 ie.printStackTrace();
206 } catch (LineUnavailableException lue) {
207 System.err.println("LINE UNAVAILABLE: " +
208 "Format is " + currentFormat);
210 Thread.sleep(openFailDelayMs);
211 totalDelayMs += openFailDelayMs;
212 } catch (InterruptedException ie) {
213 ie.printStackTrace();
216 } while (!opened && totalDelayMs < totalOpenFailDelayMs);
219 setVolume(line, volume);
221 if (isPaused() && line.isRunning()) {
236 * Pauses audio output
238 public synchronized void pause() {
248 * Resumes audio output
250 public synchronized void resume() {
253 if (!isCancelled() && line != null) {
262 * Cancels currently playing audio.
265 // [[[ WORKAROUND TODO
266 // The "Thread.sleep(cancelDelay)" is added to fix a problem in the
267 // FreeTTSEmacspeak demo. The problem was that the engine would
268 // stutter after using it for a while. Adding this sleep() fixed the
269 // problem. If we later find out that this problem no longer exists,
270 // we should remove the thread.sleep(). ]]]
271 public void cancel() {
272 debugPrint("cancelling...");
275 timer.start("audioCancel");
278 if (cancelDelay > 0) {
280 Thread.sleep(cancelDelay);
281 } catch (InterruptedException ie) {
282 ie.printStackTrace();
286 synchronized (lineLock) {
287 if (line != null && line.isRunning()) {
293 /* sets 'cancelled' to false, which breaks the write while loop */
294 synchronized (this) {
300 timer.stop("audioCancel");
301 Timer.showTimesShortTitle("");
302 timer.getTimer("audioCancel").showTimesShort(0);
305 debugPrint("...cancelled");
309 * Prepares for another batch of output. Larger groups of output
310 * (such as all output associated with a single FreeTTSSpeakable)
311 * should be grouped between a reset/drain pair.
313 public synchronized void reset() {
314 timer.start("audioOut");
317 if (isCancelled() && !isDone()) {
325 * Closes this audio player
327 public synchronized void close() {
329 if (line != null && line.isOpen()) {
338 * Returns the current volume.
340 * @return the current volume (between 0 and 1)
342 public float getVolume() {
347 * Sets the current volume.
349 * @param volume the current volume (between 0 and 1)
351 public void setVolume(float volume) {
358 this.volume = volume;
362 * Sets us in pause mode
364 * @param state true if we are paused
366 private void setPaused(boolean state) {
371 * Returns true if we are in pause mode
373 * @return true if paused
375 private boolean isPaused() {
380 * Sets the volume on the given clip
382 * @param line the line to set the volume on
383 * @param vol the volume (range 0 to 1)
385 private void setVolume(SourceDataLine line, float vol) {
387 line.isControlSupported (FloatControl.Type.MASTER_GAIN)) {
388 FloatControl volumeControl =
389 (FloatControl) line.getControl (FloatControl.Type.MASTER_GAIN);
390 float range = volumeControl.getMaximum() -
391 volumeControl.getMinimum();
392 volumeControl.setValue(vol * range + volumeControl.getMinimum());
397 * Starts the output of a set of data.
398 * For this JavaStreamingAudioPlayer, it actually opens the audio line.
399 * Since this is a streaming audio player, the <code>size</code>
400 * parameter has no meaning and effect at all, so any value can be used.
401 * Audio data for a single utterance should be grouped
402 * between begin/end pairs.
404 * @param size supposedly the size of data between now and the end,
405 * but since this is a streaming audio player, this parameter
406 * has no meaning and effect at all
408 public void begin(int size) {
409 debugPrint("opening Stream...");
410 openLine(currentFormat);
412 debugPrint("...Stream opened");
416 * Marks the end of a set of data. Audio data for a single
417 * utterance should be groupd between begin/end pairs.
419 * @return true if the audio was output properly, false if the
420 * output was cancelled or interrupted.
423 public synchronized boolean end() {
426 synchronized (lineLock) {
431 debugPrint("ended stream...");
437 * Waits for all queued audio to be played
439 * @return true if the audio played to completion, false if
440 * the audio was stopped
442 * [[[ WORKAROUND TODO
443 * The javax.sound.sampled drain is almost working properly. On
444 * linux, there is still a little bit of sound that needs to go
445 * out, even after drain is called. Thus, the drainDelay. We
446 * wait for a few hundred milliseconds while the data is really
447 * drained out of the system
450 public boolean drain() {
452 debugPrint("started draining...");
455 if (drainDelay > 0L) {
457 Thread.sleep(drainDelay);
458 } catch (InterruptedException ie) {
462 debugPrint("...finished draining");
464 timer.stop("audioOut");
466 return !isCancelled();
470 * Gets the amount of played since the last mark
472 * @return the amount of audio in milliseconds
474 public synchronized long getTime() {
475 return (line.getMicrosecondPosition() - timeOffset) / 1000L;
480 * Resets the audio clock
482 public synchronized void resetTime() {
483 timeOffset = line.getMicrosecondPosition();
489 * Writes the given bytes to the audio stream
491 * @param audioData audio data to write to the device
493 * @return <code>true</code> of the write completed successfully,
494 * <code> false </code>if the write was cancelled.
496 public boolean write(byte[] audioData) {
497 return write(audioData, 0, audioData.length);
501 * Writes the given bytes to the audio stream
503 * @param bytes audio data to write to the device
504 * @param offset the offset into the buffer
505 * @param size the size into the buffer
507 * @return <code>true</code> of the write completed successfully,
508 * <code> false </code>if the write was cancelled.
510 public boolean write(byte[] bytes, int offset, int size) {
515 int bytesRemaining = size;
516 int curIndex = offset;
520 timer.stop("firstAudio");
522 Timer.showTimesShortTitle("");
523 timer.getTimer("firstAudio").showTimesShort(0);
526 debugPrint(" au write " + bytesRemaining +
527 " pos " + line.getMicrosecondPosition()
528 + " avail " + line.available() + " bsz " +
529 line.getBufferSize());
531 while (bytesRemaining > 0 && !isCancelled()) {
537 debugPrint(" queueing cur " + curIndex + " br " + bytesRemaining);
540 synchronized (lineLock) {
541 bytesWritten = line.write
543 Math.min(BYTES_PER_WRITE, bytesRemaining));
545 if (bytesWritten != bytesWritten) {
547 ("RETRY! bw" +bytesWritten + " br " + bytesRemaining);
549 // System.out.println("BytesWritten: " + bytesWritten);
550 curIndex += bytesWritten;
551 bytesRemaining -= bytesWritten;
554 debugPrint(" wrote " + " cur " + curIndex
555 + " br " + bytesRemaining
556 + " bw " + bytesWritten);
559 return !isCancelled() && !isDone();
564 * Waits for resume. If this audio player
565 * is paused waits for the player to be resumed.
566 * Returns if resumed, cancelled or shutdown.
568 * @return true if the output has been resumed, false if the
569 * output has been cancelled or shutdown.
571 private synchronized boolean waitResume() {
572 while (isPaused() && !isCancelled() && !isDone()) {
574 debugPrint(" paused waiting ");
576 } catch (InterruptedException ie) {
580 return !isCancelled() && !isDone();
585 * Returns the name of this audioplayer
587 * @return the name of the audio player
589 public String toString() {
590 return "JavaStreamingAudioPlayer";
595 * Outputs a debug message if debugging is turned on
597 * @param msg the message to output
599 private void debugPrint(String msg) {
601 System.out.println(toString() + ": " + msg);
606 * Shows metrics for this audio player
608 public void showMetrics() {
609 timer.show("JavaStreamingAudioPlayer");
613 * Determines if the output has been cancelled. Access to the
614 * cancelled variable should be within a synchronized block such
615 * as this to ensure that access is coherent.
617 * @return true if output has been cancelled
619 private synchronized boolean isCancelled() {
624 * Determines if the output is done. Access to the
625 * done variable should be within a synchronized block such
626 * as this to ensure that access is coherent.
628 * @return true if output has completed
630 private synchronized boolean isDone() {
635 * Provides a LineListener for this clas.
637 private class JavaStreamLineListener implements LineListener {
640 * Implements update() method of LineListener interface. Responds
641 * to the line events as appropriate.
643 * @param event the LineEvent to handle
645 public void update(LineEvent event) {
646 if (event.getType().equals(LineEvent.Type.OPEN)) {
647 synchronized (openLock) {
648 openLock.notifyAll();