001/** 002 * Copyright 2003 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.jsapi; 009 010import java.beans.PropertyVetoException; 011import java.io.IOException; 012import java.util.Enumeration; 013import java.util.Iterator; 014import java.util.Vector; 015import java.util.logging.Logger; 016 017import javax.speech.EngineException; 018import javax.speech.EngineStateError; 019import javax.speech.synthesis.SynthesizerModeDesc; 020 021import com.sun.speech.engine.BaseEngineProperties; 022import com.sun.speech.engine.synthesis.BaseSynthesizer; 023import com.sun.speech.engine.synthesis.BaseSynthesizerProperties; 024import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem; 025import com.sun.speech.engine.synthesis.BaseVoice; 026import com.sun.speech.freetts.OutputQueue; 027import com.sun.speech.freetts.audio.AudioPlayer; 028 029/** 030 * Provides partial support for a JSAPI 1.0 synthesizer for the 031 * FreeTTS speech synthesis system. 032 */ 033public class FreeTTSSynthesizer extends BaseSynthesizer { 034 /** Logger instance. */ 035 private static final Logger LOGGER = 036 Logger.getLogger(FreeTTSSynthesizer.class.getName()); 037 038 /** 039 * Reference to output thread. 040 */ 041 OutputHandler outputHandler; 042 043 /** 044 * The currently active voice for this synthesizer 045 */ 046 private FreeTTSVoice curVoice; 047 048 private AudioPlayer audio; 049 050 /** 051 * All voice output for this synthesizer goes through 052 * this central utterance queue 053 */ 054 private OutputQueue outputQueue; 055 056 /** 057 * Creates a new Synthesizer in the DEALLOCATED state. 058 * 059 * @param desc describes the allowed mode of operations for this 060 * synthesizer. 061 */ 062 public FreeTTSSynthesizer(FreeTTSSynthesizerModeDesc desc) { 063 super(desc); 064 outputHandler = new OutputHandler(); 065 066 } 067 068 /** 069 * Starts the output thread. The output thread is responsible for 070 * taking items off of the queue and sending them to the audio 071 * player. 072 * 073 * @throws EngineException if an allocation error occurs 074 */ 075 protected void handleAllocate() throws EngineException { 076 long states[]; 077 boolean ok = false; 078 FreeTTSSynthesizerModeDesc desc = (FreeTTSSynthesizerModeDesc) 079 getEngineModeDesc(); 080 081 082 outputQueue = com.sun.speech.freetts.Voice.createOutputThread(); 083 084 if (desc.getVoices().length > 0) { 085 FreeTTSVoice freettsVoice = (FreeTTSVoice) desc.getVoices()[0]; 086 ok = setCurrentVoice(freettsVoice); 087 } 088 089 090 091 if (ok) { 092 synchronized (engineStateLock) { 093 long newState = ALLOCATED | RESUMED; 094 newState |= (outputHandler.isQueueEmpty() 095 ? QUEUE_EMPTY 096 : QUEUE_NOT_EMPTY); 097 states = setEngineState(CLEAR_ALL_STATE, newState); 098 } 099 outputHandler.start(); 100 postEngineAllocated(states[0], states[1]); 101 } else { 102 throw new EngineException("Can't allocate FreeTTS synthesizer"); 103 } 104 } 105 106 107 108 /** 109 * Sets the given voice to be the current voice. If 110 * the voice cannot be loaded, this call has no affect. 111 * 112 * @param voice the new voice. 113 */ 114 private boolean setCurrentVoice(FreeTTSVoice voice) 115 throws EngineException { 116 117 com.sun.speech.freetts.Voice freettsVoice = voice.getVoice(); 118 boolean ok = false; 119 120 121 if (!freettsVoice.isLoaded()) { 122 freettsVoice.setOutputQueue(outputQueue); 123 freettsVoice.allocate(); 124 audio = freettsVoice.getAudioPlayer(); 125 if (audio == null) { 126 audio = new com.sun.speech.freetts.audio.JavaClipAudioPlayer(); 127 } 128 if (audio == null) { 129 throw new EngineException("Can't get audio player"); 130 } 131 freettsVoice.setAudioPlayer(audio); 132 } 133 134 if (freettsVoice.isLoaded()) { 135 curVoice = voice; 136 ok = true; 137 // notify the world of potential property changes 138 FreeTTSSynthesizerProperties props = 139 (FreeTTSSynthesizerProperties) getSynthesizerProperties(); 140 props.checkForPropertyChanges(); 141 } 142 return ok; 143 } 144 145 /** 146 * Handles a deallocation request. Cancels all pending items, 147 * terminates the output handler, and posts the state changes. 148 * 149 * @throws EngineException if a deallocation error occurs 150 */ 151 protected void handleDeallocate() throws EngineException { 152 long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED); 153 outputHandler.cancelAllItems(); 154 outputHandler.terminate(); 155 156 // Close the audio. This should flush out any queued audio data 157 if (audio != null) { 158 try { 159 audio.close(); 160 } catch (IOException e) { 161 throw new EngineException(e.getMessage()); 162 } 163 } 164 165 outputQueue.close(); 166 167 postEngineDeallocated(states[0], states[1]); 168 } 169 170 /** 171 * Factory method to create a BaseSynthesizerQueueItem. 172 * 173 * @return a queue item appropriate for this synthesizer 174 */ 175 protected BaseSynthesizerQueueItem createQueueItem() { 176 return new FreeTTSSynthesizerQueueItem(); 177 } 178 179 /** 180 * Returns an enumeration of the queue. 181 * 182 * @return an enumeration of the contents of the queue. This 183 * enumeration contains FreeTTSSynthesizerQueueItem objects 184 * 185 * @throws EngineStateError if the engine was not in the proper 186 * state 187 */ 188 public Enumeration enumerateQueue() throws EngineStateError { 189 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 190 return outputHandler.enumerateQueue(); 191 } 192 193 /** 194 * Places an item on the speaking queue and send the queue update event. 195 * 196 * @param item the item to place in the queue 197 */ 198 protected void appendQueue(BaseSynthesizerQueueItem item) { 199 outputHandler.appendQueue((FreeTTSSynthesizerQueueItem) item); 200 } 201 202 /** 203 * Cancels the item at the top of the queue. 204 * 205 * @throws EngineStateError if the synthesizer is not in the 206 * proper state 207 */ 208 public void cancel() throws EngineStateError { 209 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 210 outputHandler.cancelItem(); 211 } 212 213 /** 214 * Cancels a specific object on the queue. 215 * 216 * @param source the object to cancel 217 * 218 * @throws IllegalArgumentException if the source object is not 219 * currently in the queue 220 * @throws EngineStateError the synthesizer is not in the 221 * proper state 222 */ 223 public void cancel(Object source) 224 throws IllegalArgumentException, EngineStateError { 225 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 226 outputHandler.cancelItem(source); 227 } 228 229 /** 230 * Cancels all items on the output queue. 231 * 232 * @throws EngineStateError 233 */ 234 public void cancelAll() throws EngineStateError { 235 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 236 outputHandler.cancelAllItems(); 237 } 238 239 /** 240 * Pauses the output 241 */ 242 protected void handlePause() { 243 audio.pause(); 244 } 245 246 /** 247 * Resumes the output 248 */ 249 protected void handleResume() { 250 audio.resume(); 251 } 252 253 /** 254 * Factory constructor for EngineProperties object. 255 * Gets the default speaking voice from the SynthesizerModeDesc. 256 * Takes the default prosody values (pitch, range, volume, rate) 257 * from the default voice. 258 * Override to set engine-specific defaults. 259 */ 260 protected BaseEngineProperties createEngineProperties() { 261 SynthesizerModeDesc desc = (SynthesizerModeDesc)engineModeDesc; 262 FreeTTSVoice defaultVoice = (FreeTTSVoice)(desc.getVoices()[0]); 263 return new FreeTTSSynthesizerProperties(defaultVoice, 264 defaultVoice.getPitch(), 265 defaultVoice.getPitchRange(), 266 defaultVoice.getSpeakingRate(), 267 defaultVoice.getVolume()); 268 } 269 270 /** 271 * Manages the FreeTTS synthesizer properties 272 */ 273 class FreeTTSSynthesizerProperties extends BaseSynthesizerProperties { 274 275 /** 276 * Constructor 277 * 278 * @param defaultVoice the voice to use as the default for 279 * this synthesizer 280 * @param defaultPitch the default pitch in hertz 281 * @param defaultPitchRange the default range of pitch in 282 * hertz 283 * @param defaultSpeakingRate the default speaking rate in 284 * words per minute 285 * @param defaultVolume the default speaking volume 286 * (0.0 to 1.0) 287 */ 288 FreeTTSSynthesizerProperties( 289 BaseVoice defaultVoice, 290 float defaultPitch, 291 float defaultPitchRange, 292 float defaultSpeakingRate, 293 float defaultVolume) { 294 295 super(defaultVoice, defaultPitch, defaultPitchRange, 296 defaultSpeakingRate, defaultVolume); 297 } 298 299 /** 300 * Resets the properties to their default values 301 */ 302 public void reset() { 303 super.reset(); 304 } 305 306 /** 307 * Checks to see if any properties have changed and if so 308 * fires the proper events 309 */ 310 void checkForPropertyChanges() { 311 try { 312 float pitch = getPitch(); 313 if (pitch != currentPitch) { 314 super.setPitch(pitch); 315 } 316 317 float pitchRange = getPitchRange(); 318 if (pitchRange != currentPitchRange) { 319 super.setPitchRange(pitchRange); 320 } 321 322 float volume = getVolume(); 323 if (volume != currentVolume) { 324 super.setVolume(volume); 325 } 326 327 float speakingRate = getSpeakingRate(); 328 if (speakingRate != currentSpeakingRate) { 329 super.setSpeakingRate(speakingRate); 330 } 331 332 } catch (PropertyVetoException pve) { 333 // the actual properties in the voices have 334 // already changed to these new values so 335 // we should not expect a PropertyVetoException 336 } 337 } 338 339 /** 340 * Get the baseline pitch for synthesis 341 * 342 * @return the current pitch (in hertz) 343 */ 344 public float getPitch() { 345 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 346 return voice.getPitch(); 347 } 348 349 /** 350 * Sets the voice to a voice that matches the given voice 351 * 352 * @param voice the voice that matches it 353 */ 354 public void setVoice(javax.speech.synthesis.Voice voice) { 355 if (!curVoice.match(voice)) { 356 // chase through the voice list and find the first match 357 // and use that. If no match, just ignore it. 358 FreeTTSSynthesizerModeDesc desc = 359 (FreeTTSSynthesizerModeDesc) getEngineModeDesc(); 360 javax.speech.synthesis.Voice voices[] = desc.getVoices(); 361 for (int i = 0; i < voices.length; i++) { 362 if (voices[i].match(voice)) { 363 try { 364 if (setCurrentVoice((FreeTTSVoice) voices[i])) { 365 try { 366 super.setVoice(voice); 367 break; 368 } catch (PropertyVetoException pve) { 369 continue; 370 } 371 } 372 } catch (EngineException ee) { 373 System.err.println("Engine Exception: " + 374 ee.getMessage()); 375 } 376 } 377 } 378 } 379 } 380 381 /** 382 * Set the baseline pitch for the current synthesis voice. 383 * 384 * @param hertz sets the current pitch 385 * 386 * @throws PropertyVetoException if the synthesizer rejects or 387 * limits the new value 388 */ 389 public void setPitch(float hertz) throws PropertyVetoException { 390 if (hertz != getPitch()) { 391 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 392 voice.setPitch(hertz); 393 super.setPitch(hertz); 394 } 395 } 396 397 398 /** 399 * Get the pitch range for synthesis. 400 * 401 * @return the current range of pitch in hertz 402 */ 403 public float getPitchRange() { 404 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 405 return voice.getPitchRange(); 406 } 407 408 /** 409 * Set the pitch range for the current synthesis voice. 410 * 411 * @throws PropertyVetoException if the synthesizer rejects or 412 * limits the new value 413 */ 414 public void setPitchRange(float hertz) throws PropertyVetoException { 415 if (hertz != getPitchRange()) { 416 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 417 voice.setPitchRange(hertz); 418 super.setPitchRange(hertz); 419 } 420 } 421 422 /** 423 * Gets the current target speaking rate. 424 * 425 * @return the current speaking rate in words per minute 426 */ 427 public float getSpeakingRate() { 428 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 429 return voice.getRate(); 430 } 431 432 /** 433 * Set the target speaking rate. 434 * 435 * @param wpm sets the target speaking rate in 436 * words per minute 437 * 438 * @throws PropertyVetoException if the synthesizer rejects or 439 * limits the new value 440 */ 441 public void setSpeakingRate(float wpm) throws PropertyVetoException { 442 if (wpm != getSpeakingRate()) { 443 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 444 voice.setRate(wpm); 445 super.setSpeakingRate(wpm); 446 } 447 } 448 449 /** 450 * Gets the current volume. 451 * 452 * @return the current volume setting (between 0 and 1.0) 453 */ 454 public float getVolume() { 455 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 456 return voice.getVolume(); 457 } 458 459 /** 460 * Sets the volume 461 * 462 * @param volume the new volume setting (between 0 and 1) 463 * 464 * @throws PropertyVetoException if the synthesizer rejects or 465 * limits the new value 466 */ 467 public void setVolume(float volume) throws PropertyVetoException { 468 if (volume > 1.0f) 469 volume = 1.0f; 470 else if (volume < 0.0f) 471 volume = 0.0f; 472 473 if (volume != getVolume()) { 474 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 475 voice.setVolume(volume); 476 super.setVolume(volume); 477 } 478 } 479 } 480 481 482 /** 483 * The OutputHandler is responsible for taking items off of the 484 * input queue and sending them to the current voice. 485 */ 486 class OutputHandler extends Thread { 487 protected boolean done = false; 488 489 /** 490 * Internal speech output queue that will contain a set of 491 * FreeTTSSynthesizerQueueItems. 492 * 493 * @see BaseSynthesizerQueueItem 494 */ 495 protected Vector queue; 496 497 /** 498 * Create a new OutputHandler for the given Synthesizer. 499 */ 500 public OutputHandler() { 501 queue = new Vector(); 502 } 503 504 /** 505 * shuts down this output handler 506 */ 507 public synchronized void terminate() { 508 synchronized (queue) { 509 done = true; 510 queue.notify(); 511 } 512 } 513 514 /** 515 * Returns an enumeration of the queue 516 * 517 * @return the enumeration queue 518 */ 519 public Enumeration enumerateQueue() { 520 synchronized(queue) { 521 return queue.elements(); 522 } 523 } 524 525 /** 526 * Determines if the input queue is empty 527 * 528 * @return true if the queue is empty; otherwise false 529 */ 530 public boolean isQueueEmpty() { 531 synchronized(queue) { 532 return queue.size() == 0; 533 } 534 } 535 536 /** 537 * Add an item to be spoken to the output queue. Fires the 538 * appropriate queue events 539 * 540 * @param item the item to add to the queue 541 */ 542 public void appendQueue(FreeTTSSynthesizerQueueItem item) { 543 boolean topOfQueueChanged; 544 synchronized(queue) { 545 topOfQueueChanged = (queue.size() == 0); 546 queue.addElement(item); 547 queue.notifyAll(); 548 } 549 if (topOfQueueChanged) { 550 long[] states = setEngineState(QUEUE_EMPTY, 551 QUEUE_NOT_EMPTY); 552 postQueueUpdated(topOfQueueChanged, states[0], states[1]); 553 } 554 } 555 556 /** 557 * Cancel the current item 558 */ 559 protected void cancelItem() { 560 FreeTTSSynthesizerQueueItem item = null; 561 562 synchronized(queue) { 563 audio.cancel(); 564 if (queue.size() != 0) { 565 item = (FreeTTSSynthesizerQueueItem) queue.remove(0); 566 if (item != null) { 567 // item.postSpeakableCancelled(); 568 item.cancelled(); 569 queueDrained(); 570 } 571 } 572 } 573 } 574 575 /** 576 * Cancel all items in the queue 577 */ 578 protected void cancelAllItems() { 579 FreeTTSSynthesizerQueueItem item = null; 580 Vector copy; 581 582 synchronized(queue) { 583 audio.cancel(); 584 copy = (Vector) queue.clone(); 585 queue.clear(); 586 queueDrained(); 587 } 588 for (Iterator i = copy.iterator(); i.hasNext(); ) { 589 item = (FreeTTSSynthesizerQueueItem) i.next(); 590 // item.postSpeakableCancelled(); 591 item.cancelled(); 592 } 593 } 594 595 596 /** 597 * Cancel the given item. 598 * 599 * @param source the item to cancel. 600 */ 601 protected void cancelItem(Object source) { 602 FreeTTSSynthesizerQueueItem item = null; 603 synchronized(queue) { 604 int index = queue.indexOf(source); 605 if (index == 0) { 606 cancelItem(); 607 } else { 608 item = (FreeTTSSynthesizerQueueItem) queue.remove(index); 609 if (item != null) { 610 // item.postSpeakableCancelled(); 611 item.cancelled(); 612 queueDrained(); 613 } 614 } 615 } 616 } 617 618 /** 619 * Gets the next item from the queue and outputs it 620 */ 621 public void run() { 622 FreeTTSSynthesizerQueueItem item; 623 while (!done) { 624 item = getQueueItem(); 625 if (item != null) { 626 outputItem(item); 627 removeQueueItem(item); 628 } 629 } 630 } 631 632 /** 633 * Return, but do not remove, the first item on the queue. 634 * 635 * @return a queue item 636 */ 637 protected FreeTTSSynthesizerQueueItem getQueueItem() { 638 FreeTTSSynthesizerQueueItem item = null; 639 synchronized(queue) { 640 while (queue.size() == 0 && !done) { 641 try { 642 queue.wait(); 643 } 644 catch (InterruptedException e) { 645 LOGGER.severe("Unexpected interrupt"); 646 // Ignore interrupts and we'll loop around 647 } 648 } 649 650 if (done) { 651 return null; 652 } 653 item = (FreeTTSSynthesizerQueueItem) queue.elementAt(0); 654 } 655 item.postTopOfQueue(); 656 return item; 657 } 658 659 /** 660 * removes the given item, posting the appropriate 661 * events. The item may have already been removed (due to a 662 * cancel). 663 * 664 * @param item the item to remove 665 */ 666 protected void removeQueueItem(FreeTTSSynthesizerQueueItem item) { 667 boolean queueEmptied = false; 668 synchronized(queue) { 669 boolean found = queue.remove(item); 670 if (found) { 671 queueDrained(); 672 } 673 } 674 } 675 676 /** 677 * Should be called iff one or more items have been removed 678 * from the queue. Generates the appropriate state changes and 679 * events. 680 */ 681 private void queueDrained() { 682 if (queue.size() == 0) { 683 long[] states = setEngineState(QUEUE_NOT_EMPTY, QUEUE_EMPTY); 684 postQueueEmptied(states[0], states[1]); 685 } else { 686 long[] states = setEngineState(QUEUE_NOT_EMPTY, 687 QUEUE_NOT_EMPTY); 688 postQueueUpdated(true, states[0], states[1]); 689 } 690 } 691 692 /** 693 * Outputs the given queue item to the current voice 694 * 695 * @param item the item to output 696 */ 697 protected void outputItem(FreeTTSSynthesizerQueueItem item) { 698 com.sun.speech.freetts.Voice voice = curVoice.getVoice(); 699 voice.speak(item); 700 } 701 } 702}