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.engine.synthesis.text; 009 010import java.util.Enumeration; 011import java.util.Vector; 012 013import javax.speech.Engine; 014import javax.speech.EngineStateError; 015import javax.speech.synthesis.SynthesizerModeDesc; 016 017import com.sun.speech.engine.synthesis.BaseSynthesizer; 018import com.sun.speech.engine.synthesis.BaseSynthesizerQueueItem; 019 020/** 021 * Supports a simple text-output-only JSAPI 1.0 <code>Synthesizer</code>. 022 * Intended for demonstration purposes for those developing JSAPI 023 * implementations. It may also be useful to developers who want a 024 * JSAPI synthesizer that doesn't produce any noise. 025 */ 026public class TextSynthesizer extends BaseSynthesizer { 027 /** 028 * Reference to output thread. 029 */ 030 OutputHandler outputHandler = null; 031 032 /** 033 * Creates a new Synthesizer in the DEALLOCATED state. 034 * 035 * @param desc the operating mode 036 */ 037 public TextSynthesizer(SynthesizerModeDesc desc) { 038 super(desc); 039 outputHandler = new OutputHandler(); 040 } 041 042 /** 043 * Starts the output thread. 044 */ 045 protected void handleAllocate() { 046 long states[]; 047 synchronized (engineStateLock) { 048 long newState = ALLOCATED | RESUMED; 049 newState |= (outputHandler.isQueueEmpty() 050 ? QUEUE_EMPTY 051 : QUEUE_NOT_EMPTY); 052 states = setEngineState(CLEAR_ALL_STATE, newState); 053 } 054 outputHandler.start(); 055 postEngineAllocated(states[0], states[1]); 056 } 057 058 /** 059 * Stops the output thread. 060 */ 061 protected void handleDeallocate() { 062 long[] states = setEngineState(CLEAR_ALL_STATE, DEALLOCATED); 063 cancelAll(); 064 outputHandler.terminate(); 065 postEngineDeallocated(states[0], states[1]); 066 } 067 068 /** 069 * Creates a TextSynthesizerQueueItem. 070 * 071 * @return a TextSynthesizerQueueItem 072 */ 073 protected BaseSynthesizerQueueItem createQueueItem() { 074 return new TextSynthesizerQueueItem(); 075 } 076 077 /** 078 * Returns an enumeration of the queue. 079 * 080 * @return 081 * an <code>Enumeration</code> of the speech output queue or 082 * <code>null</code>. 083 * 084 * @throws EngineStateError 085 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 086 * <code>DEALLOCATING_RESOURCES</code> states 087 */ 088 public Enumeration enumerateQueue() throws EngineStateError { 089 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 090 return outputHandler.enumerateQueue(); 091 } 092 093 /** 094 * Puts an item on the speaking queue and sends a queue updated 095 * event. Expects only <code>TextSynthesizerQueueItems</code>. 096 * 097 * @param item the item to add to the queue 098 * 099 */ 100 protected void appendQueue(BaseSynthesizerQueueItem item) { 101 outputHandler.appendQueue((TextSynthesizerQueueItem) item); 102 } 103 104 /** 105 * Cancels the item at the top of the queue. 106 * 107 * @throws EngineStateError 108 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 109 * <code>DEALLOCATING_RESOURCES</code> states 110 */ 111 public void cancel() throws EngineStateError { 112 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 113 outputHandler.cancelItem(); 114 } 115 116 /** 117 * Cancels a specific object on the queue. 118 * 119 * @param source 120 * object to be removed from the speech output queue 121 * 122 * @throws IllegalArgumentException 123 * if the source object is not found in the speech output queue. 124 * @throws EngineStateError 125 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 126 * <code>DEALLOCATING_RESOURCES</code> states 127 */ 128 public void cancel(Object source) 129 throws IllegalArgumentException, EngineStateError { 130 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 131 outputHandler.cancelItem(source); 132 } 133 134 /** 135 * Cancels all items on the output queue. 136 * 137 * @throws EngineStateError 138 * if this <code>Synthesizer</code> in the <code>DEALLOCATED</code> or 139 * <code>DEALLOCATING_RESOURCES</code> states 140 */ 141 public void cancelAll() throws EngineStateError { 142 checkEngineState(DEALLOCATED | DEALLOCATING_RESOURCES); 143 outputHandler.cancelAllItems(); 144 } 145 146 /** 147 * Pauses the output. 148 */ 149 protected void handlePause() { 150 outputHandler.pauseItem(); 151 } 152 153 /** 154 * Resumes the output. 155 */ 156 protected void handleResume() { 157 outputHandler.resumeItem(); 158 } 159 160 /** 161 * The output device for a <code>TextSynthesizer</code>. Sends 162 * all text to standard out. 163 */ 164 public class OutputHandler extends Thread { 165 protected boolean done = false; 166 167 /** 168 * Internal speech output queue that will contain a set of 169 * TextSynthesizerQueueItems. 170 * 171 * @see BaseSynthesizerQueueItem 172 */ 173 protected Vector queue; 174 175 /** 176 * The current item to speak. 177 */ 178 TextSynthesizerQueueItem currentItem; 179 180 /** 181 * Object to lock on for setting the current item. 182 */ 183 protected Object currentItemLock = new Object(); 184 185 /** 186 * Current output "speaking" rate. 187 * Updated as /rate[166.3]/ controls are detected in the output text. 188 */ 189 int rate = 100; 190 191 /** 192 * For the item at the top of the queue, the output command reflects 193 * whether item should be PAUSE, RESUME, CANCEL. 194 */ 195 protected int command; 196 197 protected final static int PAUSE = 0; 198 protected final static int RESUME = 1; 199 protected final static int CANCEL = 2; 200 protected final static int CANCEL_ALL = 3; 201 protected final static int CANCEL_COMPLETE = 4; 202 203 /** 204 * Object on which accesses to the command must synchronize. 205 */ 206 protected Object commandLock = new Object(); 207 208 /** 209 * Class constructor. 210 */ 211 public OutputHandler() { 212 queue = new Vector(); 213 currentItem = null; 214 } 215 216 /** 217 * Stops execution of the Thread. 218 */ 219 public void terminate() { 220 done = true; 221 } 222 223 /** 224 * Returns the current queue. 225 * 226 * @return the current queue 227 */ 228 public Enumeration enumerateQueue() { 229 synchronized(queue) { 230 return queue.elements(); 231 } 232 } 233 234 /** 235 * Determines if the queue is empty. 236 * 237 * @return <code>true</code> if the queue is empty 238 */ 239 public boolean isQueueEmpty() { 240 synchronized(queue) { 241 return queue.size() == 0; 242 } 243 } 244 245 /** 246 * Adds an item to be spoken to the output queue. 247 * 248 * @param item the item to be added 249 */ 250 public void appendQueue(TextSynthesizerQueueItem item) { 251 boolean topOfQueueChanged; 252 synchronized(queue) { 253 topOfQueueChanged = (queue.size() == 0); 254 queue.addElement(item); 255 queue.notifyAll(); 256 } 257 if (topOfQueueChanged) { 258 long[] states = setEngineState(QUEUE_EMPTY, 259 QUEUE_NOT_EMPTY); 260 postQueueUpdated(topOfQueueChanged, states[0], states[1]); 261 } 262 } 263 264 /** 265 * Cancels the current item. 266 */ 267 protected void cancelItem() { 268 cancelItem(CANCEL); 269 } 270 271 /** 272 * Cancels all items. 273 */ 274 protected void cancelAllItems() { 275 cancelItem(CANCEL_ALL); 276 } 277 278 /** 279 * Cancels all or just the current item. 280 * 281 * @param cancelType <code>CANCEL</code> or <code>CANCEL_ALL</code> 282 */ 283 protected void cancelItem(int cancelType) { 284 synchronized(queue) { 285 if (queue.size() == 0) { 286 return; 287 } 288 } 289 synchronized(commandLock) { 290 command = cancelType; 291 commandLock.notifyAll(); 292 while (command != CANCEL_COMPLETE) { 293 try { 294 commandLock.wait(); 295 } catch (InterruptedException e) { 296 // Ignore interrupts and we'll loop around 297 } 298 } 299 if (testEngineState(Engine.PAUSED)) { 300 command = PAUSE; 301 } else { 302 command = RESUME; 303 } 304 commandLock.notifyAll(); 305 } 306 } 307 308 /** 309 * Cancels the given item. 310 * 311 * @param source the item to cancel 312 */ 313 protected void cancelItem(Object source) { 314// synchronized(currentItemLock) { 315// if (currentItem.getSource() == source) { 316// cancelItem(); 317// } else { 318// boolean queueEmptied; 319// synchronized(queue) { 320// for (int i = 0; i < queue.size(); i++) { 321// BaseSynthesizerQueueItem item = 322// (BaseSynthesizerQueueItem)(queue.elementAt(i)); 323// if (item.getSource() == source) { 324// item.postSpeakableCancelled(); 325// queue.removeElementAt(i); 326// } 327// } 328// queueEmptied = queue.size() == 0; 329// queue.notifyAll(); 330// } 331// if (queueEmptied) { 332// long[] states = setEngineState(QUEUE_NOT_EMPTY, 333// QUEUE_EMPTY); 334// postQueueEmptied(states[0], states[1]); 335// } else { 336// long[] states = setEngineState(QUEUE_NOT_EMPTY, 337// QUEUE_NOT_EMPTY); 338// postQueueUpdated(false, states[0], states[1]); 339// } 340// } 341// } 342 } 343 344 /** 345 * Pauses the output. 346 */ 347 protected void pauseItem() { 348 synchronized(commandLock) { 349 if (command != PAUSE) { 350 command = PAUSE; 351 commandLock.notifyAll(); 352 } 353 } 354 } 355 356 /** 357 * Resumes the output. 358 */ 359 protected void resumeItem() { 360 synchronized(commandLock) { 361 if (command != RESUME) { 362 command = RESUME; 363 commandLock.notifyAll(); 364 } 365 } 366 } 367 368 /** 369 * Controls output of text until terminate is called. 370 * 371 * @see #terminate 372 */ 373 public void run() { 374 TextSynthesizerQueueItem item; 375 int currentCommand; 376 boolean queueEmptied; 377 378 if (testEngineState(Engine.PAUSED)) { 379 command = PAUSE; 380 } else { 381 command = RESUME; 382 } 383 384 while (!done) { 385 item = getQueueItem(); 386 item.postTopOfQueue(); 387 currentCommand = outputItem(item); 388 if (currentCommand == CANCEL_ALL) { 389 Vector itemList = new Vector(); 390 itemList.add(item); 391 synchronized(queue) { 392 queue.remove(0); 393 while (queue.size() > 0) { 394 itemList.add(queue.remove(0)); 395 } 396 } 397 synchronized(commandLock) { 398 command = CANCEL_COMPLETE; 399 commandLock.notifyAll(); 400 } 401 while (itemList.size() > 0) { 402 item = (TextSynthesizerQueueItem)(itemList.remove(0)); 403 item.postSpeakableCancelled(); 404 } 405 long[] states = setEngineState(QUEUE_NOT_EMPTY, 406 QUEUE_EMPTY); 407 postQueueEmptied(states[0], states[1]); 408 continue; 409 } else if (currentCommand == CANCEL) { 410 synchronized(commandLock) { 411 command = CANCEL_COMPLETE; 412 commandLock.notifyAll(); 413 } 414 item.postSpeakableCancelled(); 415 } else if ((currentCommand == PAUSE) 416 || (currentCommand == RESUME)) { 417 item.postSpeakableEnded(); 418 } 419 420 synchronized(queue) { 421 queue.remove(0); 422 queueEmptied = queue.size() == 0; 423 queue.notifyAll(); 424 } 425 426 if (queueEmptied) { 427 long[] states = setEngineState(QUEUE_NOT_EMPTY, 428 QUEUE_EMPTY); 429 postQueueEmptied(states[0], states[1]); 430 } else { 431 long[] states = setEngineState(QUEUE_NOT_EMPTY, 432 QUEUE_NOT_EMPTY); 433 postQueueUpdated(true, states[0], states[1]); 434 } 435 } 436 } 437 438 /** 439 * Returns, but does not remove, the first item on the queue. 440 * 441 * @return the first item on the queue 442 */ 443 protected TextSynthesizerQueueItem getQueueItem() { 444 synchronized(queue) { 445 while (queue.size() == 0) { 446 try { 447 queue.wait(); 448 } 449 catch (InterruptedException e) { 450 // Ignore interrupts and we'll loop around 451 } 452 } 453 return (TextSynthesizerQueueItem) queue.elementAt(0); 454 } 455 } 456 457 /** 458 * Starts outputting the item. Returns the current command. 459 * 460 * @param item to be output 461 * 462 * @return the current command 463 */ 464 protected int outputItem(TextSynthesizerQueueItem item) { 465 int currentCommand; 466 String engineText; 467 int engineTextIndex; 468 boolean wasPaused = false; 469 470 System.out.println("----- BEGIN: " 471 + item.getTypeString() 472 + "-----"); 473 474 engineText = item.getEngineText(); 475 engineTextIndex = 0; 476 477 // [[[WDW - known danger with this loop -- the actual 478 // command could change between the times it is checked. 479 // For example, a call to pause followed by resume 480 // followed by a pause might go unnoticed.]]] 481 // 482 synchronized(commandLock) { 483 currentCommand = command; 484 } 485 while (engineTextIndex < engineText.length()) { 486 // On a pause, just hang out and wait. If the text 487 // index is not 0, it means we've already started some 488 // processing on the current item. 489 // 490 if (currentCommand == PAUSE) { 491 if (engineTextIndex > 0) { 492 item.postSpeakablePaused(); 493 wasPaused = true; 494 } 495 synchronized(commandLock) { 496 while (command == PAUSE) { 497 try { 498 commandLock.wait(); 499 } catch (InterruptedException e) { 500 // Ignore interrupts and we'll loop around 501 } 502 } 503 currentCommand = command; 504 } 505 } 506 507 // On a resume, send out some text. If the text index 508 // is 0, it means we are just starting processing of 509 // this speakable and we need to post an event saying 510 // so. 511 // 512 if (currentCommand == RESUME) { 513 if (engineTextIndex == 0) { 514 item.postSpeakableStarted(); 515 } else if (wasPaused) { 516 item.postSpeakableResumed(); 517 wasPaused = false; 518 } 519 520 // If we get here, then we're processing text 521 // Consider three options 522 // 1. Next char is the start of a synth directive 523 // such as /emp[1]/ 524 // 2. Next char is the start of white space 525 // 3. Next char is the start of plain text 526 // 527 if (isCommand(engineText, engineTextIndex)) { 528 engineTextIndex = processCommand(item, 529 engineText, 530 engineTextIndex); 531 } else if (isWhitespace(engineText, engineTextIndex)) { 532 engineTextIndex = processWhitespace(engineText, 533 engineTextIndex); 534 } else { 535 engineTextIndex = processNormalText(item, 536 engineText, 537 engineTextIndex); 538 } 539 } else { 540 // Otherwise, the command is CANCEL or CANCEL_ALL 541 // and we should get out of this loop. 542 // 543 break; 544 } 545 synchronized(commandLock) { 546 currentCommand = command; 547 } 548 } 549 550 System.out.println("\n----- END: " 551 + item.getTypeString() 552 + "-----\n"); 553 554 return currentCommand; 555 } 556 557 /** 558 * Determines if the next thing in line is a command. 559 * 560 * @param engineText the text containing embedded commands 561 * @param index the current index 562 * 563 * @return <code>true</code> if the next thing in line is a command 564 */ 565 protected boolean isCommand(String engineText, int index) { 566 if (!engineText.substring(index,index + 1).equals( 567 TextSynthesizerQueueItem.COMMAND_PREFIX)) { 568 return false; 569 } 570 571 // Test for all known commands 572 // 573 for (int i = 0; 574 i < TextSynthesizerQueueItem.ELEMENTS.length; 575 i++) { 576 if (engineText.startsWith( 577 TextSynthesizerQueueItem.COMMAND_PREFIX 578 + TextSynthesizerQueueItem.ELEMENTS[i], index)) { 579 return true; 580 } 581 } 582 return false; 583 } 584 585 /** 586 * Attempts to process a command starting at the next character 587 * in the synthesizer text. Returns the new index. 588 * 589 * @param item the current queue item 590 * @param engineText the text containing embedded commands 591 * @param index the current index 592 * 593 * @return the new index 594 */ 595 protected int processCommand(TextSynthesizerQueueItem item, 596 String engineText, int index) { 597 // Test for all known commands 598 // 599 for (int i = 0; 600 i < TextSynthesizerQueueItem.ELEMENTS.length; 601 i++) { 602 if (engineText.startsWith( 603 TextSynthesizerQueueItem.COMMAND_PREFIX 604 + TextSynthesizerQueueItem.ELEMENTS[i], index)) { 605 int endIndex = engineText.indexOf( 606 TextSynthesizerQueueItem.COMMAND_SUFFIX, index+1) 607 + 1; 608 String commandText = engineText.substring(index, endIndex); 609 System.out.print(commandText); 610 System.out.flush(); 611 return endIndex; 612 } 613 } 614 return index; 615 } 616 617 618 /** 619 * Determines if there is whitespace at the current index. 620 * 621 * @param engineText the text containing embedded commands 622 * @param index the current index 623 * 624 * @return <code>true</code> if there is whitespace at the 625 * current index 626 */ 627 protected boolean isWhitespace(String engineText, int index) { 628 return Character.isWhitespace(engineText.charAt(index)); 629 } 630 631 /** 632 * Processes whitespace at the current index in the synthesizer text. 633 * If next character is not whitespace, does nothing. 634 * If next character is whitespace, displays it and pauses 635 * briefly to simulate the speaking rate. 636 * 637 * @param engineText the text containing embedded commands 638 * @param index the current index 639 * 640 * @return the new index 641 */ 642 protected int processWhitespace(String engineText, int index) { 643 // Identify full span of whitespace 644 // 645 int endIndex = index; 646 while (endIndex < engineText.length() && 647 Character.isWhitespace(engineText.charAt(endIndex))) { 648 endIndex++; 649 } 650 651 // Display the whitespace as plain text 652 // 653 System.out.print(engineText.substring(index, endIndex)); 654 System.out.flush(); 655 656 // Pause briefly with the delay determined by the current 657 // "speaking rate." Convert the word-per-minute rate to 658 // millseconds. 659 // 660 try { 661 sleep(1000 * 60 / rate); 662 } catch (InterruptedException e) { 663 // Ignore any interruption 664 } 665 666 return endIndex; 667 } 668 669 /** 670 * Processes next set of characters in output up to whitespace 671 * or next '/' that could indicate the start of a command. 672 * 673 * @param item the current queue item 674 * @param engineText the text containing embedded commands 675 * @param index the current index 676 * 677 * @return the new index 678 */ 679 protected int processNormalText(TextSynthesizerQueueItem item, 680 String engineText, 681 int index) { 682 String wordStr; 683 684 // Find the end of the plain text 685 // 686 int endIndex = index+1; 687 while (endIndex < engineText.length() && 688 engineText.charAt(endIndex) != '/' && 689 !Character.isWhitespace(engineText.charAt(endIndex))) 690 endIndex++; 691 692 // Display the text in a plain format 693 // 694 wordStr = engineText.substring(index, endIndex); 695 item.postWordStarted(wordStr, index, endIndex); 696 System.out.print(wordStr); 697 System.out.flush(); 698 return endIndex; 699 } 700 } 701}