blob: 704a1daf3ec7baff8bee4773159f7f37ac3d11e0 [file] [log] [blame]
Narayan Kamath67ae6bc2011-11-30 14:51:00 +00001/*
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
Przemyslaw Szczepaniak5cbf17c2014-06-18 11:35:52 +010018import android.speech.tts.TextToSpeechService.AudioOutputParams;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000019import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
Niels Egberts65c50782016-12-23 12:01:32 +000020import android.media.AudioTrack;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000021import android.util.Log;
22
23import java.util.LinkedList;
Kazuhiro Inabaa81d17b2017-07-25 13:16:13 +090024import java.util.concurrent.atomic.AtomicInteger;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000025import java.util.concurrent.locks.Condition;
26import java.util.concurrent.locks.Lock;
27import java.util.concurrent.locks.ReentrantLock;
Niels Egberts65c50782016-12-23 12:01:32 +000028import java.util.concurrent.ConcurrentLinkedQueue;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000029
30/**
Niels Egberts65c50782016-12-23 12:01:32 +000031 * Manages the playback of a list of byte arrays representing audio data that are queued by the
32 * engine to an audio track.
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000033 */
Niels Egberts65c50782016-12-23 12:01:32 +000034final class SynthesisPlaybackQueueItem extends PlaybackQueueItem
35 implements AudioTrack.OnPlaybackPositionUpdateListener {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000036 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;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010065 private volatile int mStatusCode;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000066
67 private final BlockingAudioTrack mAudioTrack;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010068 private final AbstractEventLogger mLogger;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000069
Niels Egberts65c50782016-12-23 12:01:32 +000070 // 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
Kazuhiro Inabaa81d17b2017-07-25 13:16:13 +090074 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
Przemyslaw Szczepaniak5cbf17c2014-06-18 11:35:52 +010079 SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate,
80 int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher,
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010081 Object callerIdentity, AbstractEventLogger logger) {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000082 super(dispatcher, callerIdentity);
83
84 mUnconsumedBytes = 0;
85
86 mStopped = false;
87 mDone = false;
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +010088 mStatusCode = TextToSpeech.SUCCESS;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000089
Przemyslaw Szczepaniak5cbf17c2014-06-18 11:35:52 +010090 mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000091 mLogger = logger;
92 }
93
94
95 @Override
96 public void run() {
Kazuhiro Inabaa81d17b2017-07-25 13:16:13 +090097 if (!mRunState.compareAndSet(NOT_RUN, RUN_CALLED)) {
98 // stop() was already called before run(). Do nothing and just finish.
99 return;
100 }
101
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000102 final UtteranceProgressDispatcher dispatcher = getDispatcher();
103 dispatcher.dispatchOnStart();
104
Narayan Kamathed4e5412012-06-09 17:31:39 +0100105 if (!mAudioTrack.init()) {
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +0100106 dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);
Narayan Kamathed4e5412012-06-09 17:31:39 +0100107 return;
108 }
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000109
Niels Egberts65c50782016-12-23 12:01:32 +0000110 mAudioTrack.setPlaybackPositionUpdateListener(this);
Sergio Sancho07d98a92017-02-14 12:35:13 +0000111 // Ensure we set the first marker if there is one.
112 updateMarker();
Niels Egberts65c50782016-12-23 12:01:32 +0000113
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000114 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
Kazuhiro Inabaa81d17b2017-07-25 13:16:13 +0900134 dispatchEndStatus();
135 }
136
137 private void dispatchEndStatus() {
138 final UtteranceProgressDispatcher dispatcher = getDispatcher();
139
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +0100140 if (mStatusCode == TextToSpeech.SUCCESS) {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100141 dispatcher.dispatchOnSuccess();
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +0100142 } else if(mStatusCode == TextToSpeech.STOPPED) {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100143 dispatcher.dispatchOnStop();
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000144 } else {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100145 dispatcher.dispatchOnError(mStatusCode);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000146 }
147
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100148 mLogger.onCompleted(mStatusCode);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000149 }
150
151 @Override
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100152 void stop(int statusCode) {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000153 try {
154 mListLock.lock();
155
156 // Update our internal state.
157 mStopped = true;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100158 mStatusCode = statusCode;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000159
Kazuhiro Inabaa81d17b2017-07-25 13:16:13 +0900160 if (mRunState.getAndSet(STOP_CALLED) == NOT_RUN) {
161 // Dispatch the status code and just finish without signaling
162 // if run() has not even started.
163 dispatchEndStatus();
164 return;
165 }
166
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000167 // Wake up the audio playback thread if it was waiting on take().
168 // take() will return null since mStopped was true, and will then
169 // break out of the data write loop.
170 mReadReady.signal();
171
172 // Wake up the synthesis thread if it was waiting on put(). Its
173 // buffers will no longer be copied since mStopped is true. The
174 // PlaybackSynthesisCallback that this synthesis corresponds to
175 // would also have been stopped, and so all calls to
176 // Callback.onDataAvailable( ) will return errors too.
177 mNotFull.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
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
Niels Egberts65c50782016-12-23 12:01:32 +0000210 /** 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
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. */
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. */
238 void rangeStart(int markerInFrames, int start, int end) {
239 markerList.add(new ProgressMarker(markerInFrames, start, end));
240 updateMarker();
241 }
242
243 @Override
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.
Niels Egberts5d0ea0f2017-03-21 15:29:24 +0000251 getDispatcher().dispatchOnRangeStart(marker.start, marker.end, marker.frames);
Niels Egberts65c50782016-12-23 12:01:32 +0000252 // 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
258 public void onPeriodicNotification(AudioTrack track) {}
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000259
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
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
326 ListEntry(byte[] bytes) {
327 mBytes = bytes;
328 }
329 }
330}