1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package android.speech.tts; 17 18 import android.speech.tts.TextToSpeechService.AudioOutputParams; 19 import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; 20 import android.util.Log; 21 22 import java.util.LinkedList; 23 import java.util.concurrent.locks.Condition; 24 import java.util.concurrent.locks.Lock; 25 import java.util.concurrent.locks.ReentrantLock; 26 27 /** 28 * Manages the playback of a list of byte arrays representing audio data 29 * that are queued by the engine to an audio track. 30 */ 31 final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { 32 private static final String TAG = "TTS.SynthQueueItem"; 33 private static final boolean DBG = false; 34 35 /** 36 * Maximum length of audio we leave unconsumed by the audio track. 37 * Calls to {@link #put(byte[])} will block until we have less than 38 * this amount of audio left to play back. 39 */ 40 private static final long MAX_UNCONSUMED_AUDIO_MS = 500; 41 42 /** 43 * Guards accesses to mDataBufferList and mUnconsumedBytes. 44 */ 45 private final Lock mListLock = new ReentrantLock(); 46 private final Condition mReadReady = mListLock.newCondition(); 47 private final Condition mNotFull = mListLock.newCondition(); 48 49 // Guarded by mListLock. 50 private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); 51 // Guarded by mListLock. 52 private int mUnconsumedBytes; 53 54 /* 55 * While mStopped and mIsError can be written from any thread, mDone is written 56 * only from the synthesis thread. All three variables are read from the 57 * audio playback thread. 58 */ 59 private volatile boolean mStopped; 60 private volatile boolean mDone; 61 private volatile int mStatusCode; 62 63 private final BlockingAudioTrack mAudioTrack; 64 private final AbstractEventLogger mLogger; 65 66 SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate, 67 int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher, 68 Object callerIdentity, AbstractEventLogger logger) { 69 super(dispatcher, callerIdentity); 70 71 mUnconsumedBytes = 0; 72 73 mStopped = false; 74 mDone = false; 75 mStatusCode = TextToSpeech.SUCCESS; 76 77 mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount); 78 mLogger = logger; 79 } 80 81 82 @Override 83 public void run() { 84 final UtteranceProgressDispatcher dispatcher = getDispatcher(); 85 dispatcher.dispatchOnStart(); 86 87 if (!mAudioTrack.init()) { 88 dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT); 89 return; 90 } 91 92 try { 93 byte[] buffer = null; 94 95 // take() will block until: 96 // 97 // (a) there is a buffer available to tread. In which case 98 // a non null value is returned. 99 // OR (b) stop() is called in which case it will return null. 100 // OR (c) done() is called in which case it will return null. 101 while ((buffer = take()) != null) { 102 mAudioTrack.write(buffer); 103 mLogger.onAudioDataWritten(); 104 } 105 106 } catch (InterruptedException ie) { 107 if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up."); 108 } 109 110 mAudioTrack.waitAndRelease(); 111 112 if (mStatusCode == TextToSpeech.SUCCESS) { 113 dispatcher.dispatchOnSuccess(); 114 } else if(mStatusCode == TextToSpeech.STOPPED) { 115 dispatcher.dispatchOnStop(); 116 } else { 117 dispatcher.dispatchOnError(mStatusCode); 118 } 119 120 mLogger.onCompleted(mStatusCode); 121 } 122 123 @Override 124 void stop(int statusCode) { 125 try { 126 mListLock.lock(); 127 128 // Update our internal state. 129 mStopped = true; 130 mStatusCode = statusCode; 131 132 // Wake up the audio playback thread if it was waiting on take(). 133 // take() will return null since mStopped was true, and will then 134 // break out of the data write loop. 135 mReadReady.signal(); 136 137 // Wake up the synthesis thread if it was waiting on put(). Its 138 // buffers will no longer be copied since mStopped is true. The 139 // PlaybackSynthesisCallback that this synthesis corresponds to 140 // would also have been stopped, and so all calls to 141 // Callback.onDataAvailable( ) will return errors too. 142 mNotFull.signal(); 143 } finally { 144 mListLock.unlock(); 145 } 146 147 // Stop the underlying audio track. This will stop sending 148 // data to the mixer and discard any pending buffers that the 149 // track holds. 150 mAudioTrack.stop(); 151 } 152 153 void done() { 154 try { 155 mListLock.lock(); 156 157 // Update state. 158 mDone = true; 159 160 // Unblocks the audio playback thread if it was waiting on take() 161 // after having consumed all available buffers. It will then return 162 // null and leave the write loop. 163 mReadReady.signal(); 164 165 // Just so that engines that try to queue buffers after 166 // calling done() don't block the synthesis thread forever. Ideally 167 // this should be called from the same thread as put() is, and hence 168 // this call should be pointless. 169 mNotFull.signal(); 170 } finally { 171 mListLock.unlock(); 172 } 173 } 174 175 176 void put(byte[] buffer) throws InterruptedException { 177 try { 178 mListLock.lock(); 179 long unconsumedAudioMs = 0; 180 181 while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) > 182 MAX_UNCONSUMED_AUDIO_MS && !mStopped) { 183 mNotFull.await(); 184 } 185 186 // Don't bother queueing the buffer if we've stopped. The playback thread 187 // would have woken up when stop() is called (if it was blocked) and will 188 // proceed to leave the write loop since take() will return null when 189 // stopped. 190 if (mStopped) { 191 return; 192 } 193 194 mDataBufferList.add(new ListEntry(buffer)); 195 mUnconsumedBytes += buffer.length; 196 mReadReady.signal(); 197 } finally { 198 mListLock.unlock(); 199 } 200 } 201 202 private byte[] take() throws InterruptedException { 203 try { 204 mListLock.lock(); 205 206 // Block if there are no available buffers, and stop() has not 207 // been called and done() has not been called. 208 while (mDataBufferList.size() == 0 && !mStopped && !mDone) { 209 mReadReady.await(); 210 } 211 212 // If stopped, return null so that we can exit the playback loop 213 // as soon as possible. 214 if (mStopped) { 215 return null; 216 } 217 218 // Remove the first entry from the queue. 219 ListEntry entry = mDataBufferList.poll(); 220 221 // This is the normal playback loop exit case, when done() was 222 // called. (mDone will be true at this point). 223 if (entry == null) { 224 return null; 225 } 226 227 mUnconsumedBytes -= entry.mBytes.length; 228 // Unblock the waiting writer. We use signal() and not signalAll() 229 // because there will only be one thread waiting on this (the 230 // Synthesis thread). 231 mNotFull.signal(); 232 233 return entry.mBytes; 234 } finally { 235 mListLock.unlock(); 236 } 237 } 238 239 static final class ListEntry { 240 final byte[] mBytes; 241 242 ListEntry(byte[] bytes) { 243 mBytes = bytes; 244 } 245 } 246 } 247