1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.speech.tts.cts; 17 18 import android.content.Context; 19 import android.media.MediaPlayer; 20 import android.speech.tts.TextToSpeech; 21 import android.speech.tts.TextToSpeech.OnInitListener; 22 import android.speech.tts.UtteranceProgressListener; 23 import android.util.Log; 24 25 import java.util.HashMap; 26 import java.util.HashSet; 27 import java.util.Map; 28 import java.util.Set; 29 import java.util.List; 30 import java.util.ArrayList; 31 import java.util.concurrent.TimeUnit; 32 import java.util.concurrent.locks.Condition; 33 import java.util.concurrent.locks.Lock; 34 import java.util.concurrent.locks.ReentrantLock; 35 import org.junit.Assert; 36 37 /** 38 * Wrapper for {@link TextToSpeech} with some handy test functionality. 39 */ 40 public class TextToSpeechWrapper { 41 private static final String LOG_TAG = "TextToSpeechServiceTest"; 42 43 public static final String MOCK_TTS_ENGINE = "android.speech.tts.cts"; 44 45 private final Context mContext; 46 private TextToSpeech mTts; 47 private final InitWaitListener mInitListener; 48 private final UtteranceWaitListener mUtteranceListener; 49 50 /** maximum time to wait for tts to be initialized */ 51 private static final int TTS_INIT_MAX_WAIT_TIME = 30 * 1000; 52 /** maximum time to wait for speech call to be complete */ 53 private static final int TTS_SPEECH_MAX_WAIT_TIME = 5 * 1000; 54 55 private TextToSpeechWrapper(Context context) { 56 mContext = context; 57 mInitListener = new InitWaitListener(); 58 mUtteranceListener = new UtteranceWaitListener(); 59 } 60 61 private boolean initTts() throws InterruptedException { 62 return initTts(new TextToSpeech(mContext, mInitListener)); 63 } 64 65 private boolean initTts(String engine) throws InterruptedException { 66 return initTts(new TextToSpeech(mContext, mInitListener, engine)); 67 } 68 69 private boolean initTts(TextToSpeech tts) throws InterruptedException { 70 mTts = tts; 71 if (!mInitListener.waitForInit()) { 72 return false; 73 } 74 mTts.setOnUtteranceProgressListener(mUtteranceListener); 75 return true; 76 } 77 78 public boolean waitForComplete(String utteranceId) throws InterruptedException { 79 return mUtteranceListener.waitForComplete(utteranceId); 80 } 81 82 public boolean waitForStop(String utteranceId) throws InterruptedException { 83 return mUtteranceListener.waitForStop(utteranceId); 84 } 85 86 public TextToSpeech getTts() { 87 return mTts; 88 } 89 90 public void shutdown() { 91 mTts.shutdown(); 92 } 93 94 public final Map<String, Integer> chunksReceived() { 95 return mUtteranceListener.chunksReceived(); 96 } 97 98 public final Map<String, List<Integer>> timePointsStart() { 99 return mUtteranceListener.timePointsStart(); 100 } 101 102 public final Map<String, List<Integer>> timePointsEnd() { 103 return mUtteranceListener.timePointsEnd(); 104 } 105 106 public final Map<String, List<Integer>> timePointsFrame() { 107 return mUtteranceListener.timePointsFrame(); 108 } 109 110 /** 111 * Sanity checks that the utteranceIds and only the utteranceIds completed and produced the 112 * correct callbacks. 113 * Can only be used when the test knows exactly which utterances should have been finished when 114 * this call is made. Else use waitForStop(String) or waitForComplete(String). 115 */ 116 public void verify(String... utteranceIds) { 117 mUtteranceListener.verify(utteranceIds); 118 } 119 120 public static TextToSpeechWrapper createTextToSpeechWrapper(Context context) 121 throws InterruptedException { 122 TextToSpeechWrapper wrapper = new TextToSpeechWrapper(context); 123 if (wrapper.initTts()) { 124 return wrapper; 125 } else { 126 return null; 127 } 128 } 129 130 public static TextToSpeechWrapper createTextToSpeechMockWrapper(Context context) 131 throws InterruptedException { 132 TextToSpeechWrapper wrapper = new TextToSpeechWrapper(context); 133 if (wrapper.initTts(MOCK_TTS_ENGINE)) { 134 return wrapper; 135 } else { 136 return null; 137 } 138 } 139 140 /** 141 * Listener for waiting for TTS engine initialization completion. 142 */ 143 private static class InitWaitListener implements OnInitListener { 144 private final Lock mLock = new ReentrantLock(); 145 private final Condition mDone = mLock.newCondition(); 146 private Integer mStatus = null; 147 148 public void onInit(int status) { 149 mLock.lock(); 150 try { 151 mStatus = new Integer(status); 152 mDone.signal(); 153 } finally { 154 mLock.unlock(); 155 } 156 } 157 158 public boolean waitForInit() throws InterruptedException { 159 long timeOutNanos = TimeUnit.MILLISECONDS.toNanos(TTS_INIT_MAX_WAIT_TIME); 160 mLock.lock(); 161 try { 162 while (mStatus == null) { 163 if (timeOutNanos <= 0) { 164 return false; 165 } 166 timeOutNanos = mDone.awaitNanos(timeOutNanos); 167 } 168 return mStatus == TextToSpeech.SUCCESS; 169 } finally { 170 mLock.unlock(); 171 } 172 } 173 } 174 175 /** 176 * Listener for waiting for utterance completion. 177 */ 178 private static class UtteranceWaitListener extends UtteranceProgressListener { 179 private final Lock mLock = new ReentrantLock(); 180 private final Condition mDone = mLock.newCondition(); 181 private final Set<String> mStartedUtterances = new HashSet<>(); 182 // Contains the list of utterances that are stopped. Entry is removed after waitForStop(). 183 private final Set<String> mStoppedUtterances = new HashSet<>(); 184 private final Map<String, Integer> mErredUtterances = new HashMap<>(); 185 // Contains the list of utterances that are completed. Entry is removed after 186 // waitForComplete(). 187 private final Set<String> mCompletedUtterances = new HashSet<>(); 188 private final Set<String> mBeginSynthesisUtterances = new HashSet<>(); 189 private final Map<String, Integer> mChunksReceived = new HashMap<>(); 190 private final Map<String, List<Integer>> mTimePointsStart = new HashMap<>(); 191 private final Map<String, List<Integer>> mTimePointsEnd = new HashMap<>(); 192 private final Map<String, List<Integer>> mTimePointsFrame = new HashMap<>(); 193 194 @Override 195 public void onDone(String utteranceId) { 196 mLock.lock(); 197 try { 198 Assert.assertTrue(mStartedUtterances.contains(utteranceId)); 199 mCompletedUtterances.add(utteranceId); 200 mDone.signal(); 201 } finally { 202 mLock.unlock(); 203 } 204 } 205 206 @Override 207 public void onError(String utteranceId) { 208 mLock.lock(); 209 try { 210 mErredUtterances.put(utteranceId, -1); 211 mDone.signal(); 212 } finally { 213 mLock.unlock(); 214 } 215 } 216 217 @Override 218 public void onError(String utteranceId, int errorCode) { 219 mLock.lock(); 220 try { 221 mErredUtterances.put(utteranceId, errorCode); 222 mDone.signal(); 223 } finally { 224 mLock.unlock(); 225 } 226 } 227 228 @Override 229 public void onStart(String utteranceId) { 230 mLock.lock(); 231 try { 232 // TODO: Due to a bug in the framework onStart() is called twice for 233 // synthesizeToFile requests. Once that is fixed we should assert here that we 234 // expect only one onStart() per utteranceId. 235 mStartedUtterances.add(utteranceId); 236 } finally { 237 mLock.unlock(); 238 } 239 } 240 241 @Override 242 public void onStop(String utteranceId, boolean isStarted) { 243 mLock.lock(); 244 try { 245 mStoppedUtterances.add(utteranceId); 246 mDone.signal(); 247 } finally { 248 mLock.unlock(); 249 } 250 } 251 252 @Override 253 public void onBeginSynthesis(String utteranceId, int sampleRateInHz, int audioFormat, int channelCount) { 254 Assert.assertNotNull(utteranceId); 255 Assert.assertTrue(sampleRateInHz > 0); 256 Assert.assertTrue(audioFormat == android.media.AudioFormat.ENCODING_PCM_8BIT 257 || audioFormat == android.media.AudioFormat.ENCODING_PCM_16BIT 258 || audioFormat == android.media.AudioFormat.ENCODING_PCM_FLOAT); 259 Assert.assertTrue(channelCount >= 1); 260 Assert.assertTrue(channelCount <= 2); 261 mLock.lock(); 262 try { 263 mBeginSynthesisUtterances.add(utteranceId); 264 } finally { 265 mLock.unlock(); 266 } 267 } 268 269 @Override 270 public void onAudioAvailable(String utteranceId, byte[] audio) { 271 Assert.assertNotNull(utteranceId); 272 Assert.assertTrue(audio.length > 0); 273 mLock.lock(); 274 try { 275 Assert.assertTrue(mBeginSynthesisUtterances.contains(utteranceId)); 276 if (mChunksReceived.get(utteranceId) != null) { 277 mChunksReceived.put(utteranceId, mChunksReceived.get(utteranceId) + 1); 278 } else { 279 mChunksReceived.put(utteranceId, 1); 280 } 281 } finally { 282 mLock.unlock(); 283 } 284 } 285 286 @Override 287 public void onRangeStart(String utteranceId, int start, int end, int frame) { 288 Assert.assertNotNull(utteranceId); 289 mLock.lock(); 290 try { 291 Assert.assertTrue(mBeginSynthesisUtterances.contains(utteranceId)); 292 if (mTimePointsStart.get(utteranceId) == null) { 293 mTimePointsStart.put(utteranceId, new ArrayList<Integer>()); 294 mTimePointsEnd.put(utteranceId, new ArrayList<Integer>()); 295 mTimePointsFrame.put(utteranceId, new ArrayList<Integer>()); 296 } 297 mTimePointsStart.get(utteranceId).add(start); 298 mTimePointsEnd.get(utteranceId).add(end); 299 mTimePointsFrame.get(utteranceId).add(frame); 300 } finally { 301 mLock.unlock(); 302 } 303 } 304 305 public boolean waitForComplete(String utteranceId) 306 throws InterruptedException { 307 long timeOutNanos = TimeUnit.MILLISECONDS.toNanos(TTS_INIT_MAX_WAIT_TIME); 308 mLock.lock(); 309 try { 310 while (!mCompletedUtterances.remove(utteranceId)) { 311 if (timeOutNanos <= 0) { 312 return false; 313 } 314 timeOutNanos = mDone.awaitNanos(timeOutNanos); 315 } 316 return true; 317 } finally { 318 mLock.unlock(); 319 } 320 } 321 322 public boolean waitForStop(String utteranceId) 323 throws InterruptedException { 324 long timeOutNanos = TimeUnit.MILLISECONDS.toNanos(TTS_INIT_MAX_WAIT_TIME); 325 mLock.lock(); 326 try { 327 while (!mStoppedUtterances.remove(utteranceId)) { 328 if (timeOutNanos <= 0) { 329 return false; 330 } 331 timeOutNanos = mDone.awaitNanos(timeOutNanos); 332 } 333 return true; 334 } finally { 335 mLock.unlock(); 336 } 337 } 338 339 public final Map<String, Integer> chunksReceived() { 340 return mChunksReceived; 341 } 342 343 public final Map<String, List<Integer>> timePointsStart() { 344 return mTimePointsStart; 345 } 346 347 public final Map<String, List<Integer>> timePointsEnd() { 348 return mTimePointsEnd; 349 } 350 351 public final Map<String, List<Integer>> timePointsFrame() { 352 return mTimePointsFrame; 353 } 354 355 public void verify(String... utteranceIds) { 356 Assert.assertTrue(utteranceIds.length == mStartedUtterances.size()); 357 for (String id : utteranceIds) { 358 Assert.assertTrue(mStartedUtterances.contains(id)); 359 Assert.assertTrue(mBeginSynthesisUtterances.contains(id)); 360 Assert.assertTrue(mChunksReceived.containsKey(id)); 361 } 362 } 363 } 364 365 /** 366 * Determines if given file path is a valid, playable music file. 367 */ 368 public static boolean isSoundFile(String filePath) { 369 // use media player to play the file. If it succeeds with no exceptions, assume file is 370 //valid 371 MediaPlayer mp = null; 372 try { 373 mp = new MediaPlayer(); 374 mp.setDataSource(filePath); 375 mp.prepare(); 376 mp.start(); 377 mp.stop(); 378 return true; 379 } catch (Exception e) { 380 Log.e(LOG_TAG, "Exception while attempting to play music file", e); 381 return false; 382 } finally { 383 if (mp != null) { 384 mp.release(); 385 } 386 } 387 } 388 389 } 390