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.UtteranceProgressDispatcher; 19 import android.util.Log; 20 21 import java.util.LinkedList; 22 import java.util.concurrent.locks.Condition; 23 import java.util.concurrent.locks.Lock; 24 import 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 */ 30 final 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 boolean mIsError; 61 62 private final BlockingAudioTrack mAudioTrack; 63 private final EventLogger mLogger; 64 65 66 SynthesisPlaybackQueueItem(int streamType, int sampleRate, 67 int audioFormat, int channelCount, 68 float volume, float pan, UtteranceProgressDispatcher dispatcher, 69 Object callerIdentity, EventLogger logger) { 70 super(dispatcher, callerIdentity); 71 72 mUnconsumedBytes = 0; 73 74 mStopped = false; 75 mDone = false; 76 mIsError = false; 77 78 mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat, 79 channelCount, volume, pan); 80 mLogger = logger; 81 } 82 83 84 @Override 85 public void run() { 86 final UtteranceProgressDispatcher dispatcher = getDispatcher(); 87 dispatcher.dispatchOnStart(); 88 89 90 mAudioTrack.init(); 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 (mIsError) { 113 dispatcher.dispatchOnError(); 114 } else { 115 dispatcher.dispatchOnDone(); 116 } 117 118 mLogger.onWriteData(); 119 } 120 121 @Override 122 void stop(boolean isError) { 123 try { 124 mListLock.lock(); 125 126 // Update our internal state. 127 mStopped = true; 128 mIsError = isError; 129 130 // Wake up the audio playback thread if it was waiting on take(). 131 // take() will return null since mStopped was true, and will then 132 // break out of the data write loop. 133 mReadReady.signal(); 134 135 // Wake up the synthesis thread if it was waiting on put(). Its 136 // buffers will no longer be copied since mStopped is true. The 137 // PlaybackSynthesisCallback that this synthesis corresponds to 138 // would also have been stopped, and so all calls to 139 // Callback.onDataAvailable( ) will return errors too. 140 mNotFull.signal(); 141 } finally { 142 mListLock.unlock(); 143 } 144 145 // Stop the underlying audio track. This will stop sending 146 // data to the mixer and discard any pending buffers that the 147 // track holds. 148 mAudioTrack.stop(); 149 } 150 151 void done() { 152 try { 153 mListLock.lock(); 154 155 // Update state. 156 mDone = true; 157 158 // Unblocks the audio playback thread if it was waiting on take() 159 // after having consumed all available buffers. It will then return 160 // null and leave the write loop. 161 mReadReady.signal(); 162 163 // Just so that engines that try to queue buffers after 164 // calling done() don't block the synthesis thread forever. Ideally 165 // this should be called from the same thread as put() is, and hence 166 // this call should be pointless. 167 mNotFull.signal(); 168 } finally { 169 mListLock.unlock(); 170 } 171 } 172 173 174 void put(byte[] buffer) throws InterruptedException { 175 try { 176 mListLock.lock(); 177 long unconsumedAudioMs = 0; 178 179 while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) > 180 MAX_UNCONSUMED_AUDIO_MS && !mStopped) { 181 mNotFull.await(); 182 } 183 184 // Don't bother queueing the buffer if we've stopped. The playback thread 185 // would have woken up when stop() is called (if it was blocked) and will 186 // proceed to leave the write loop since take() will return null when 187 // stopped. 188 if (mStopped) { 189 return; 190 } 191 192 mDataBufferList.add(new ListEntry(buffer)); 193 mUnconsumedBytes += buffer.length; 194 mReadReady.signal(); 195 } finally { 196 mListLock.unlock(); 197 } 198 } 199 200 private byte[] take() throws InterruptedException { 201 try { 202 mListLock.lock(); 203 204 // Block if there are no available buffers, and stop() has not 205 // been called and done() has not been called. 206 while (mDataBufferList.size() == 0 && !mStopped && !mDone) { 207 mReadReady.await(); 208 } 209 210 // If stopped, return null so that we can exit the playback loop 211 // as soon as possible. 212 if (mStopped) { 213 return null; 214 } 215 216 // Remove the first entry from the queue. 217 ListEntry entry = mDataBufferList.poll(); 218 219 // This is the normal playback loop exit case, when done() was 220 // called. (mDone will be true at this point). 221 if (entry == null) { 222 return null; 223 } 224 225 mUnconsumedBytes -= entry.mBytes.length; 226 // Unblock the waiting writer. We use signal() and not signalAll() 227 // because there will only be one thread waiting on this (the 228 // Synthesis thread). 229 mNotFull.signal(); 230 231 return entry.mBytes; 232 } finally { 233 mListLock.unlock(); 234 } 235 } 236 237 static final class ListEntry { 238 final byte[] mBytes; 239 240 ListEntry(byte[] bytes) { 241 mBytes = bytes; 242 } 243 } 244 } 245 246