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