SynthesisPlaybackQueueItem.java revision 32011ea4f144b46b5ab64577465983caf6303f2d
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.media.AudioTrack;
19import android.speech.tts.TextToSpeechService.AudioOutputParams;
20import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
21import android.util.Log;
22
23import java.util.LinkedList;
24import java.util.concurrent.ConcurrentLinkedQueue;
25import java.util.concurrent.atomic.AtomicInteger;
26import java.util.concurrent.locks.Condition;
27import java.util.concurrent.locks.Lock;
28import 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 */
34final 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
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
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
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
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
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
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.
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
258    public void onPeriodicNotification(AudioTrack track) {}
259
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}
331