SynthesisPlaybackQueueItem.java revision 5d0ea0fe210db85a8a8a44c63d8fef195e206abb
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        // Ensure we set the first marker if there is one.
101        updateMarker();
102
103        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
123        if (mStatusCode == TextToSpeech.SUCCESS) {
124            dispatcher.dispatchOnSuccess();
125        } else if(mStatusCode == TextToSpeech.STOPPED) {
126            dispatcher.dispatchOnStop();
127        } else {
128            dispatcher.dispatchOnError(mStatusCode);
129        }
130
131        mLogger.onCompleted(mStatusCode);
132    }
133
134    @Override
135    void stop(int statusCode) {
136        try {
137            mListLock.lock();
138
139            // Update our internal state.
140            mStopped = true;
141            mStatusCode = statusCode;
142
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
186    /** 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.
227        getDispatcher().dispatchOnRangeStart(marker.start, marker.end, marker.frames);
228        // 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) {}
235
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}
307