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.media.AudioTrack; 19 import android.speech.tts.TextToSpeechService.AudioOutputParams; 20 import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; 21 import android.util.Log; 22 23 import java.util.LinkedList; 24 import java.util.concurrent.ConcurrentLinkedQueue; 25 import java.util.concurrent.atomic.AtomicInteger; 26 import java.util.concurrent.locks.Condition; 27 import java.util.concurrent.locks.Lock; 28 import java.util.concurrent.locks.ReentrantLock; 29 30 /** 31 * Manages the playback of a list of byte arrays representing audio data that are queued by the 32 * engine to an audio track. 33 */ 34 final class SynthesisPlaybackQueueItem extends PlaybackQueueItem 35 implements AudioTrack.OnPlaybackPositionUpdateListener { 36 private static final String TAG = "TTS.SynthQueueItem"; 37 private static final boolean DBG = false; 38 39 /** 40 * Maximum length of audio we leave unconsumed by the audio track. 41 * Calls to {@link #put(byte[])} will block until we have less than 42 * this amount of audio left to play back. 43 */ 44 private static final long MAX_UNCONSUMED_AUDIO_MS = 500; 45 46 /** 47 * Guards accesses to mDataBufferList and mUnconsumedBytes. 48 */ 49 private final Lock mListLock = new ReentrantLock(); 50 private final Condition mReadReady = mListLock.newCondition(); 51 private final Condition mNotFull = mListLock.newCondition(); 52 53 // Guarded by mListLock. 54 private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); 55 // Guarded by mListLock. 56 private int mUnconsumedBytes; 57 58 /* 59 * While mStopped and mIsError can be written from any thread, mDone is written 60 * only from the synthesis thread. All three variables are read from the 61 * audio playback thread. 62 */ 63 private volatile boolean mStopped; 64 private volatile boolean mDone; 65 private volatile int mStatusCode; 66 67 private final BlockingAudioTrack mAudioTrack; 68 private final AbstractEventLogger mLogger; 69 70 // Stores a queue of markers. When the marker in front is reached the client is informed and we 71 // wait for the next one. 72 private ConcurrentLinkedQueue<ProgressMarker> markerList = new ConcurrentLinkedQueue<>(); 73 74 private static final int NOT_RUN = 0; 75 private static final int RUN_CALLED = 1; 76 private static final int STOP_CALLED = 2; 77 private final AtomicInteger mRunState = new AtomicInteger(NOT_RUN); 78 SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate, int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher, Object callerIdentity, AbstractEventLogger logger)79 SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate, 80 int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher, 81 Object callerIdentity, AbstractEventLogger logger) { 82 super(dispatcher, callerIdentity); 83 84 mUnconsumedBytes = 0; 85 86 mStopped = false; 87 mDone = false; 88 mStatusCode = TextToSpeech.SUCCESS; 89 90 mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount); 91 mLogger = logger; 92 } 93 94 95 @Override run()96 public void run() { 97 if (!mRunState.compareAndSet(NOT_RUN, RUN_CALLED)) { 98 // stop() was already called before run(). Do nothing and just finish. 99 return; 100 } 101 102 final UtteranceProgressDispatcher dispatcher = getDispatcher(); 103 dispatcher.dispatchOnStart(); 104 105 if (!mAudioTrack.init()) { 106 dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT); 107 return; 108 } 109 110 mAudioTrack.setPlaybackPositionUpdateListener(this); 111 // Ensure we set the first marker if there is one. 112 updateMarker(); 113 114 try { 115 byte[] buffer = null; 116 117 // take() will block until: 118 // 119 // (a) there is a buffer available to tread. In which case 120 // a non null value is returned. 121 // OR (b) stop() is called in which case it will return null. 122 // OR (c) done() is called in which case it will return null. 123 while ((buffer = take()) != null) { 124 mAudioTrack.write(buffer); 125 mLogger.onAudioDataWritten(); 126 } 127 128 } catch (InterruptedException ie) { 129 if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up."); 130 } 131 132 mAudioTrack.waitAndRelease(); 133 134 dispatchEndStatus(); 135 } 136 dispatchEndStatus()137 private void dispatchEndStatus() { 138 final UtteranceProgressDispatcher dispatcher = getDispatcher(); 139 140 if (mStatusCode == TextToSpeech.SUCCESS) { 141 dispatcher.dispatchOnSuccess(); 142 } else if(mStatusCode == TextToSpeech.STOPPED) { 143 dispatcher.dispatchOnStop(); 144 } else { 145 dispatcher.dispatchOnError(mStatusCode); 146 } 147 148 mLogger.onCompleted(mStatusCode); 149 } 150 151 @Override stop(int statusCode)152 void stop(int statusCode) { 153 try { 154 mListLock.lock(); 155 156 // Update our internal state. 157 mStopped = true; 158 mStatusCode = statusCode; 159 160 // Wake up the synthesis thread if it was waiting on put(). Its 161 // buffers will no longer be copied since mStopped is true. The 162 // PlaybackSynthesisCallback that this synthesis corresponds to 163 // would also have been stopped, and so all calls to 164 // Callback.onDataAvailable( ) will return errors too. 165 mNotFull.signal(); 166 167 if (mRunState.getAndSet(STOP_CALLED) == NOT_RUN) { 168 // Dispatch the status code and just finish. Signaling audio 169 // playback is not necessary because run() hasn't started. 170 dispatchEndStatus(); 171 return; 172 } 173 174 // Wake up the audio playback thread if it was waiting on take(). 175 // take() will return null since mStopped was true, and will then 176 // break out of the data write loop. 177 mReadReady.signal(); 178 } finally { 179 mListLock.unlock(); 180 } 181 182 // Stop the underlying audio track. This will stop sending 183 // data to the mixer and discard any pending buffers that the 184 // track holds. 185 mAudioTrack.stop(); 186 } 187 done()188 void done() { 189 try { 190 mListLock.lock(); 191 192 // Update state. 193 mDone = true; 194 195 // Unblocks the audio playback thread if it was waiting on take() 196 // after having consumed all available buffers. It will then return 197 // null and leave the write loop. 198 mReadReady.signal(); 199 200 // Just so that engines that try to queue buffers after 201 // calling done() don't block the synthesis thread forever. Ideally 202 // this should be called from the same thread as put() is, and hence 203 // this call should be pointless. 204 mNotFull.signal(); 205 } finally { 206 mListLock.unlock(); 207 } 208 } 209 210 /** Convenience class for passing around TTS markers. */ 211 private class ProgressMarker { 212 // The index in frames of this marker. 213 public final int frames; 214 // The start index in the text of the utterance. 215 public final int start; 216 // The end index (exclusive) in the text of the utterance. 217 public final int end; 218 ProgressMarker(int frames, int start, int end)219 public ProgressMarker(int frames, int start, int end) { 220 this.frames = frames; 221 this.start = start; 222 this.end = end; 223 } 224 } 225 226 /** Set a callback for the first marker in the queue. */ updateMarker()227 void updateMarker() { 228 ProgressMarker marker = markerList.peek(); 229 if (marker != null) { 230 // Zero is used to disable the marker. The documentation recommends to use a non-zero 231 // position near zero such as 1. 232 int markerInFrames = marker.frames == 0 ? 1 : marker.frames; 233 mAudioTrack.setNotificationMarkerPosition(markerInFrames); 234 } 235 } 236 237 /** Informs us that at markerInFrames, the range between start and end is about to be spoken. */ rangeStart(int markerInFrames, int start, int end)238 void rangeStart(int markerInFrames, int start, int end) { 239 markerList.add(new ProgressMarker(markerInFrames, start, end)); 240 updateMarker(); 241 } 242 243 @Override onMarkerReached(AudioTrack track)244 public void onMarkerReached(AudioTrack track) { 245 ProgressMarker marker = markerList.poll(); 246 if (marker == null) { 247 Log.e(TAG, "onMarkerReached reached called but no marker in queue"); 248 return; 249 } 250 // Inform the client. 251 getDispatcher().dispatchOnRangeStart(marker.start, marker.end, marker.frames); 252 // Listen for the next marker. 253 // It's ok if this marker is in the past, in that case onMarkerReached will be called again. 254 updateMarker(); 255 } 256 257 @Override onPeriodicNotification(AudioTrack track)258 public void onPeriodicNotification(AudioTrack track) {} 259 put(byte[] buffer)260 void put(byte[] buffer) throws InterruptedException { 261 try { 262 mListLock.lock(); 263 long unconsumedAudioMs = 0; 264 265 while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) > 266 MAX_UNCONSUMED_AUDIO_MS && !mStopped) { 267 mNotFull.await(); 268 } 269 270 // Don't bother queueing the buffer if we've stopped. The playback thread 271 // would have woken up when stop() is called (if it was blocked) and will 272 // proceed to leave the write loop since take() will return null when 273 // stopped. 274 if (mStopped) { 275 return; 276 } 277 278 mDataBufferList.add(new ListEntry(buffer)); 279 mUnconsumedBytes += buffer.length; 280 mReadReady.signal(); 281 } finally { 282 mListLock.unlock(); 283 } 284 } 285 take()286 private byte[] take() throws InterruptedException { 287 try { 288 mListLock.lock(); 289 290 // Block if there are no available buffers, and stop() has not 291 // been called and done() has not been called. 292 while (mDataBufferList.size() == 0 && !mStopped && !mDone) { 293 mReadReady.await(); 294 } 295 296 // If stopped, return null so that we can exit the playback loop 297 // as soon as possible. 298 if (mStopped) { 299 return null; 300 } 301 302 // Remove the first entry from the queue. 303 ListEntry entry = mDataBufferList.poll(); 304 305 // This is the normal playback loop exit case, when done() was 306 // called. (mDone will be true at this point). 307 if (entry == null) { 308 return null; 309 } 310 311 mUnconsumedBytes -= entry.mBytes.length; 312 // Unblock the waiting writer. We use signal() and not signalAll() 313 // because there will only be one thread waiting on this (the 314 // Synthesis thread). 315 mNotFull.signal(); 316 317 return entry.mBytes; 318 } finally { 319 mListLock.unlock(); 320 } 321 } 322 323 static final class ListEntry { 324 final byte[] mBytes; 325 ListEntry(byte[] bytes)326 ListEntry(byte[] bytes) { 327 mBytes = bytes; 328 } 329 } 330 } 331