Home | History | Annotate | Download | only in cts
      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