update standards version
[debian/freetts] / demo / util / EmacspeakProtocolHandler.java
1 /**
2  * Copyright 2001 Sun Microsystems, Inc.
3  * 
4  * See the file "license.terms" for information on usage and
5  * redistribution of this file, and for a DISCLAIMER OF ALL 
6  * WARRANTIES.
7  */
8 import com.sun.speech.freetts.util.Utilities;
9
10 import java.io.BufferedReader;
11 import java.io.DataOutputStream;
12 import java.io.IOException;
13 import java.io.InputStreamReader;
14 import java.io.OutputStream;
15
16 import java.net.Socket;
17 import java.net.SocketTimeoutException;
18
19
20 /**
21  * Implements a very simplified (and incomplete) version of the
22  * Emacspeak speech server.
23  *
24  * See the Emacspeak protocol document at
25  * http://emacspeak.sourceforge.net/info/html/TTS-Servers.html
26  * for more information.
27  */
28 public abstract class EmacspeakProtocolHandler implements Runnable {
29
30     // network related variables
31     private Socket socket;
32     private BufferedReader reader;
33     private OutputStream writer;
34
35     // synthesizer related variables
36     protected static final String PARENS_STAR_REGEX = "[.]*\\[\\*\\][.]*";
37     private static final int NOT_HANDLED_COMMAND = 1;
38     private static final int LETTER_COMMAND = 2;
39     private static final int QUEUE_COMMAND = 3;
40     private static final int TTS_SAY_COMMAND = 4;
41     private static final int STOP_COMMAND = 5;
42     private static final int EXIT_COMMAND = 6;
43     private static final int RATE_COMMAND = 7;
44     
45     private String lastQueuedCommand;
46     private String stopQuestionStart = "Active processes exist;";
47
48     private boolean debug = false;
49
50     /**
51      * Sometimes emacspeak will embed DECTalk escape sequences in the
52      * text.  These sequences are not meant to be spoken, and FreeTTS
53      * currently does not interpret them.  This simple flag provides
54      * a mechanism for FreeTTS to cut strings of the form "[...]"
55      * out of text to be spoken (DECTalk escape sequences are of the
56      * form "[...]").  Since this is a relatively heavy-handed thing
57      * to do, this feature is turned off by default.  To turn it on,
58      * add -DstripDECTalk=true to the command line.
59      */
60     private boolean stripDECTalk = false;
61
62     /**
63      * Sets the Socket to be used by this ProtocolHandler.
64      *
65      * @param socket the Socket to be used
66      */
67     public void setSocket(Socket socket) {
68         this.socket = socket;
69         if (socket != null) {
70             try {
71                 reader = new BufferedReader
72                     (new InputStreamReader(socket.getInputStream()));
73                 writer = new DataOutputStream(socket.getOutputStream());
74                 socket.setKeepAlive(true);
75                 // socket.setSoTimeout(5000);
76             } catch (IOException ioe) {
77                 ioe.printStackTrace();
78                 throw new Error();
79             }
80         }
81     }
82
83
84     /**
85      * Returns the socket used.
86      *
87      * @return the socket used
88      */
89     public Socket getSocket() {
90         return socket;
91     }
92
93
94     /**
95      * Set to debug mode, which will print out debug messages.
96      *
97      * @param true if set to debug mode, false if set to non-debug mode.
98      */
99     public void setDebug(boolean debug) {
100         this.debug = debug;
101     }
102
103
104     /**
105      * Returns true if the given input string starts with the given
106      * starting and ending sequence.
107      *
108      * @param start the starting character sequence
109      * @param end the ending character sequence
110      * 
111      * @return true if the input string matches the given Pattern;
112      *         false otherwise
113      */
114     private static boolean matches(String start, String end, String input) {
115         return (input.startsWith(start) && input.endsWith(end));
116     }
117
118
119     /**
120      * Returns the type of the given command.
121      *
122      * @param command the command from emacspeak
123      *
124      * @return the command type
125      */
126     private static int getCommandType(String command) {
127         int type = NOT_HANDLED_COMMAND;
128         if (command.startsWith("l ")) {
129             type = LETTER_COMMAND;
130         } else if (command.startsWith("q ")) {
131             type = QUEUE_COMMAND;
132         } else if (command.startsWith("tts_say ")) {
133             type = TTS_SAY_COMMAND;
134         } else if (command.startsWith("tts_set_speech_rate ")) {
135             type = RATE_COMMAND;
136         } else if (command.equals("s")) {
137             type = STOP_COMMAND;
138         } else if (command.equals("exit")) {
139             type = EXIT_COMMAND;
140         }
141         return type;
142     }
143
144     
145     /**
146      * Returns the text of the given input that is within curly
147      * brackets.  If there are no curly brackets (allowed in the
148      * Emacspeak protocol if the text has no spaces), then it
149      * just returns the text after the first space.
150      *
151      * @param input the input text
152      *
153      * @return text within curly brackets
154      */
155     public static String textInCurlyBrackets(String input) {
156         String result = "";
157         if (input.length() > 0) {
158             int first = input.indexOf('{');
159             if (first == -1) {
160                 first = input.indexOf(' ');
161             }
162             int last = input.lastIndexOf('}');
163             if (last == -1) {
164                 last = input.length();
165             }
166             if (first != -1 && last != -1 &&
167                 first < last) {
168                 result = input.substring(first+1, last);
169             }
170         }
171         return result.trim();
172     }
173
174
175     /**
176      * Strips DECTalk commands from the input text.  The DECTalk
177      * commands are anything inside "[" and "]".
178      */
179     public String stripDECTalkCommands(String content) {
180         int startPos = content.indexOf('[');
181         while (startPos != -1) {
182             int endPos = content.indexOf(']');
183             if (endPos != -1) {
184                 if (startPos == 0) {
185                     if (endPos == (content.length() - 1)) {
186                         content = "";
187                     } else {
188                         content = content.substring(endPos + 1);
189                     }
190                 } else {
191                     if (endPos == (content.length() - 1)) {
192                         content = content.substring(0, startPos);
193                     } else {
194                         String firstPart = content.substring(0, startPos);
195                         String secondPart = content.substring(endPos + 1);
196                         content = firstPart + " " + secondPart;
197                     }
198                 }
199                 startPos = content.indexOf('[');
200             } else {
201                 break;
202             }
203         }
204         return content.trim();
205     }
206
207     
208     /**
209      * Speaks the given input text.
210      *
211      * @param input the input text to speak.
212      */
213     public abstract void speak(String input);
214
215
216     /**
217      * Removes all the queued text.
218      */
219     public abstract void cancelAll();
220
221
222     /**
223      * Sets the speaking rate.
224      *
225      * @param wpm the new speaking rate (words per minute)
226      */
227     public abstract void setRate(float wpm);
228
229
230     /**
231      * Implements the run() method of Runnable
232      */
233     public synchronized void run() {
234         try {
235             String command = "";
236             stripDECTalk = Utilities.getBoolean("stripDECTalk");
237             while (isSocketLive()) {
238                 command = reader.readLine();
239                 if (command != null) {
240                     command = command.trim();
241                     debugPrintln("IN   : " + command);
242
243                     int commandType = getCommandType(command);
244
245                     if (commandType == EXIT_COMMAND) {
246                         socket.close();
247                         notifyAll();
248                     } else if (commandType == STOP_COMMAND) {
249                         cancelAll();
250                     } else if (commandType == RATE_COMMAND) {
251                         try {
252                             setRate(Float.parseFloat(
253                                         textInCurlyBrackets(command)));
254                         } catch (NumberFormatException e) {
255                             // ignore and do nothing
256                         }
257                     } else if (commandType != NOT_HANDLED_COMMAND) {
258                         String content = textInCurlyBrackets(command);
259                         if (stripDECTalk) {
260                             content = stripDECTalkCommands(content);
261                         }
262                         if (content.length() > 0) {
263                             speak(content);
264                         }
265                         // detect if emacspeak is trying to quit
266                         detectQuitting(commandType, content);
267                     } else {
268                         debugPrintln("SPEAK:");
269                     }
270                 }
271             }
272         } catch (IOException ioe) {
273             ioe.printStackTrace();
274         } finally {
275             debugPrintln("EmacspeakProtocolHandler: thread terminated");
276         }
277     }
278
279
280     /**
281      * Returns true if the Socket is still alive.
282      *
283      * @return true if the Socket is still alive
284      */
285     private boolean isSocketLive() {
286         return (socket.isBound() &&
287                 !socket.isClosed() && socket.isConnected() &&
288                 !socket.isInputShutdown() && !socket.isOutputShutdown());
289     }
290
291
292     /**
293      * Read a line of text. A line is considered to be terminated 
294      * by any one of a line feed ('\n'), a carriage return ('\r'),
295      * or a carriage return followed immediately by a linefeed. 
296      *
297      * @return A String containing the contents of the line, 
298      * not including any line-termination
299      * characters, or null if the end of the stream has been reached
300      *
301      * @throws IOException if an I/O error occurs
302      */
303     private String readLine() throws IOException {
304         String command = null;
305         boolean repeat = false;
306         do {
307             try {
308                 command = reader.readLine();
309                 repeat = false;
310             } catch (SocketTimeoutException ste) {
311                 System.out.println("timed out");
312                 /*
313                 writer.write(-1);
314                 writer.flush();
315                 */
316                 repeat = isSocketLive();
317             }
318         } while (repeat);
319      
320         return command;
321     }
322
323     
324     /**
325      * Detects and handles a possible emacspeak quitting sequence 
326      * of commands, by looking at the given command type and content.
327      * If a quitting sequence is detected, it will close the socket.
328      * Note that this is not the best way to trap a quitting sequence,
329      * but I can't find another way to trap it. This method will
330      * do a socket.notifyAll() to tell objects waiting on the socket
331      * that it has been closed. 
332      *
333      * @param commandType the command type
334      * @param content the contents of the command
335      */
336     private synchronized void detectQuitting(int commandType, String content)
337         throws IOException {
338         if (commandType == QUEUE_COMMAND) {
339             lastQueuedCommand = content;
340         } else if (commandType == TTS_SAY_COMMAND) {
341             if (content.equals("no")) {
342                 lastQueuedCommand = "";
343             } else if (content.equals("yes") &&
344                        lastQueuedCommand.startsWith(stopQuestionStart)) {
345                 socket.close();
346                 notifyAll();
347             }
348         }
349     }
350
351
352     /**
353      * Prints the given message if the <code>debug</code> System property
354      * is set to <code>true</code>.
355      *
356      * @param message the message to print
357      */ 
358     public void debugPrintln(String message) {
359         if (debug) {
360             System.out.println(message);
361         }
362     }
363 }