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