SynthesisPlaybackQueueItem.java revision 65c50784564d0bae9276fde5472dd8898a781bcd
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.speech.tts.TextToSpeechService.AudioOutputParams;
19import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
20import android.media.AudioTrack;
21import 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;
27import java.util.concurrent.ConcurrentLinkedQueue;
28
29/**
30 * Manages the playback of a list of byte arrays representing audio data that are queued by the
31 * engine to an audio track.
32 */
33final class SynthesisPlaybackQueueItem extends PlaybackQueueItem
34        implements AudioTrack.OnPlaybackPositionUpdateListener {
35    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;
64    private volatile int mStatusCode;
65
66    private final BlockingAudioTrack mAudioTrack;
67    private final AbstractEventLogger mLogger;
68
69    // 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
73    SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate,
74            int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher,
75            Object callerIdentity, AbstractEventLogger logger) {
76        super(dispatcher, callerIdentity);
77
78        mUnconsumedBytes = 0;
79
80        mStopped = false;
81        mDone = false;
82        mStatusCode = TextToSpeech.SUCCESS;
83
84        mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount);
85        mLogger = logger;
86    }
87
88
89    @Override
90    public void run() {
91        final UtteranceProgressDispatcher dispatcher = getDispatcher();
92        dispatcher.dispatchOnStart();
93
94        if (!mAudioTrack.init()) {
95            dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);
96            return;
97        }
98
99        mAudioTrack.setPlaybackPositionUpdateListener(this);
100
101        try {
102            byte[] buffer = null;
103
104            // take() will block until:
105            //
106            // (a) there is a buffer available to tread. In which case
107            // a non null value is returned.
108            // OR (b) stop() is called in which case it will return null.
109            // OR (c) done() is called in which case it will return null.
110            while ((buffer = take()) != null) {
111                mAudioTrack.write(buffer);
112                mLogger.onAudioDataWritten();
113            }
114
115        } catch (InterruptedException ie) {
116            if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
117        }
118
119        mAudioTrack.waitAndRelease();
120
121        if (mStatusCode == TextToSpeech.SUCCESS) {
122            dispatcher.dispatchOnSuccess();
123        } else if(mStatusCode == TextToSpeech.STOPPED) {
124            dispatcher.dispatchOnStop();
125        } else {
126            dispatcher.dispatchOnError(mStatusCode);
127        }
128
129        mLogger.onCompleted(mStatusCode);
130    }
131
132    @Override
133    void stop(int statusCode) {
134        try {
135            mListLock.lock();
136
137            // Update our internal state.
138            mStopped = true;
139            mStatusCode = statusCode;
140
141            // Wake up the audio playback thread if it was waiting on take().
142            // take() will return null since mStopped was true, and will then
143            // break out of the data write loop.
144            mReadReady.signal();
145
146            // Wake up the synthesis thread if it was waiting on put(). Its
147            // buffers will no longer be copied since mStopped is true. The
148            // PlaybackSynthesisCallback that this synthesis corresponds to
149            // would also have been stopped, and so all calls to
150            // Callback.onDataAvailable( ) will return errors too.
151            mNotFull.signal();
152        } finally {
153            mListLock.unlock();
154        }
155
156        // Stop the underlying audio track. This will stop sending
157        // data to the mixer and discard any pending buffers that the
158        // track holds.
159        mAudioTrack.stop();
160    }
161
162    void done() {
163        try {
164            mListLock.lock();
165
166            // Update state.
167            mDone = true;
168
169            // Unblocks the audio playback thread if it was waiting on take()
170            // after having consumed all available buffers. It will then return
171            // null and leave the write loop.
172            mReadReady.signal();
173
174            // Just so that engines that try to queue buffers after
175            // calling done() don't block the synthesis thread forever. Ideally
176            // this should be called from the same thread as put() is, and hence
177            // this call should be pointless.
178            mNotFull.signal();
179        } finally {
180            mListLock.unlock();
181        }
182    }
183
184    /** Convenience class for passing around TTS markers. */
185    private class ProgressMarker {
186        // The index in frames of this marker.
187        public final int frames;
188        // The start index in the text of the utterance.
189        public final int start;
190        // The end index (exclusive) in the text of the utterance.
191        public final int end;
192
193        public ProgressMarker(int frames, int start, int end) {
194            this.frames = frames;
195            this.start = start;
196            this.end = end;
197        }
198    }
199
200    /** Set a callback for the first marker in the queue. */
201    void updateMarker() {
202        ProgressMarker marker = markerList.peek();
203        if (marker != null) {
204            // Zero is used to disable the marker. The documentation recommends to use a non-zero
205            // position near zero such as 1.
206            int markerInFrames = marker.frames == 0 ? 1 : marker.frames;
207            mAudioTrack.setNotificationMarkerPosition(markerInFrames);
208        }
209    }
210
211    /** Informs us that at markerInFrames, the range between start and end is about to be spoken. */
212    void rangeStart(int markerInFrames, int start, int end) {
213        markerList.add(new ProgressMarker(markerInFrames, start, end));
214        updateMarker();
215    }
216
217    @Override
218    public void onMarkerReached(AudioTrack track) {
219        ProgressMarker marker = markerList.poll();
220        if (marker == null) {
221            Log.e(TAG, "onMarkerReached reached called but no marker in queue");
222            return;
223        }
224        // Inform the client.
225        getDispatcher().dispatchOnUtteranceRangeStart(marker.start, marker.end);
226        // Listen for the next marker.
227        // It's ok if this marker is in the past, in that case onMarkerReached will be called again.
228        updateMarker();
229    }
230
231    @Override
232    public void onPeriodicNotification(AudioTrack track) {}
233
234    void put(byte[] buffer) throws InterruptedException {
235        try {
236            mListLock.lock();
237            long unconsumedAudioMs = 0;
238
239            while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
240                    MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
241                mNotFull.await();
242            }
243
244            // Don't bother queueing the buffer if we've stopped. The playback thread
245            // would have woken up when stop() is called (if it was blocked) and will
246            // proceed to leave the write loop since take() will return null when
247            // stopped.
248            if (mStopped) {
249                return;
250            }
251
252            mDataBufferList.add(new ListEntry(buffer));
253            mUnconsumedBytes += buffer.length;
254            mReadReady.signal();
255        } finally {
256            mListLock.unlock();
257        }
258    }
259
260    private byte[] take() throws InterruptedException {
261        try {
262            mListLock.lock();
263
264            // Block if there are no available buffers, and stop() has not
265            // been called and done() has not been called.
266            while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
267                mReadReady.await();
268            }
269
270            // If stopped, return null so that we can exit the playback loop
271            // as soon as possible.
272            if (mStopped) {
273                return null;
274            }
275
276            // Remove the first entry from the queue.
277            ListEntry entry = mDataBufferList.poll();
278
279            // This is the normal playback loop exit case, when done() was
280            // called. (mDone will be true at this point).
281            if (entry == null) {
282                return null;
283            }
284
285            mUnconsumedBytes -= entry.mBytes.length;
286            // Unblock the waiting writer. We use signal() and not signalAll()
287            // because there will only be one thread waiting on this (the
288            // Synthesis thread).
289            mNotFull.signal();
290
291            return entry.mBytes;
292        } finally {
293            mListLock.unlock();
294        }
295    }
296
297    static final class ListEntry {
298        final byte[] mBytes;
299
300        ListEntry(byte[] bytes) {
301            mBytes = bytes;
302        }
303    }
304}
305