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