blob: f52638b5a3fca5bb8b0928a7aae5e2df5fef5ade [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;
24import java.util.concurrent.locks.Condition;
25import java.util.concurrent.locks.Lock;
26import java.util.concurrent.locks.ReentrantLock;
Niels Egberts65c50782016-12-23 12:01:32 +000027import java.util.concurrent.ConcurrentLinkedQueue;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000028
29/**
Niels Egberts65c50782016-12-23 12:01:32 +000030 * Manages the playback of a list of byte arrays representing audio data that are queued by the
31 * engine to an audio track.
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000032 */
Niels Egberts65c50782016-12-23 12:01:32 +000033final class SynthesisPlaybackQueueItem extends PlaybackQueueItem
34 implements AudioTrack.OnPlaybackPositionUpdateListener {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000035 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;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010064 private volatile int mStatusCode;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000065
66 private final BlockingAudioTrack mAudioTrack;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010067 private final AbstractEventLogger mLogger;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000068
Niels Egberts65c50782016-12-23 12:01:32 +000069 // 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
Przemyslaw Szczepaniak5cbf17c2014-06-18 11:35:52 +010073 SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate,
74 int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher,
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +010075 Object callerIdentity, AbstractEventLogger logger) {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000076 super(dispatcher, callerIdentity);
77
78 mUnconsumedBytes = 0;
79
80 mStopped = false;
81 mDone = false;
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +010082 mStatusCode = TextToSpeech.SUCCESS;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000083
Przemyslaw Szczepaniak5cbf17c2014-06-18 11:35:52 +010084 mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000085 mLogger = logger;
86 }
87
88
89 @Override
90 public void run() {
91 final UtteranceProgressDispatcher dispatcher = getDispatcher();
92 dispatcher.dispatchOnStart();
93
Narayan Kamathed4e5412012-06-09 17:31:39 +010094 if (!mAudioTrack.init()) {
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +010095 dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);
Narayan Kamathed4e5412012-06-09 17:31:39 +010096 return;
97 }
Narayan Kamath67ae6bc2011-11-30 14:51:00 +000098
Niels Egberts65c50782016-12-23 12:01:32 +000099 mAudioTrack.setPlaybackPositionUpdateListener(this);
Sergio Sancho07d98a92017-02-14 12:35:13 +0000100 // Ensure we set the first marker if there is one.
101 updateMarker();
Niels Egberts65c50782016-12-23 12:01:32 +0000102
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000103 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
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +0100123 if (mStatusCode == TextToSpeech.SUCCESS) {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100124 dispatcher.dispatchOnSuccess();
Przemyslaw Szczepaniakfc4b2892014-06-26 11:52:20 +0100125 } else if(mStatusCode == TextToSpeech.STOPPED) {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100126 dispatcher.dispatchOnStop();
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000127 } else {
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100128 dispatcher.dispatchOnError(mStatusCode);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000129 }
130
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100131 mLogger.onCompleted(mStatusCode);
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000132 }
133
134 @Override
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100135 void stop(int statusCode) {
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000136 try {
137 mListLock.lock();
138
139 // Update our internal state.
140 mStopped = true;
Przemyslaw Szczepaniak90d15d22013-06-14 12:02:53 +0100141 mStatusCode = statusCode;
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000142
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
Niels Egberts65c50782016-12-23 12:01:32 +0000186 /** 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.
Niels Egberts5d0ea0f2017-03-21 15:29:24 +0000227 getDispatcher().dispatchOnRangeStart(marker.start, marker.end, marker.frames);
Niels Egberts65c50782016-12-23 12:01:32 +0000228 // 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) {}
Narayan Kamath67ae6bc2011-11-30 14:51:00 +0000235
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}