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}