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.UtteranceProgressDispatcher;
19import android.util.Log;
20
21import java.util.LinkedList;
22import java.util.concurrent.locks.Condition;
23import java.util.concurrent.locks.Lock;
24import java.util.concurrent.locks.ReentrantLock;
25
26/**
27 * Manages the playback of a list of byte arrays representing audio data
28 * that are queued by the engine to an audio track.
29 */
30final class SynthesisPlaybackQueueItem extends PlaybackQueueItem {
31    private static final String TAG = "TTS.SynthQueueItem";
32    private static final boolean DBG = false;
33
34    /**
35     * Maximum length of audio we leave unconsumed by the audio track.
36     * Calls to {@link #put(byte[])} will block until we have less than
37     * this amount of audio left to play back.
38     */
39    private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
40
41    /**
42     * Guards accesses to mDataBufferList and mUnconsumedBytes.
43     */
44    private final Lock mListLock = new ReentrantLock();
45    private final Condition mReadReady = mListLock.newCondition();
46    private final Condition mNotFull = mListLock.newCondition();
47
48    // Guarded by mListLock.
49    private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
50    // Guarded by mListLock.
51    private int mUnconsumedBytes;
52
53    /*
54     * While mStopped and mIsError can be written from any thread, mDone is written
55     * only from the synthesis thread. All three variables are read from the
56     * audio playback thread.
57     */
58    private volatile boolean mStopped;
59    private volatile boolean mDone;
60    private volatile boolean mIsError;
61
62    private final BlockingAudioTrack mAudioTrack;
63    private final EventLogger mLogger;
64
65
66    SynthesisPlaybackQueueItem(int streamType, int sampleRate,
67            int audioFormat, int channelCount,
68            float volume, float pan, UtteranceProgressDispatcher dispatcher,
69            Object callerIdentity, EventLogger logger) {
70        super(dispatcher, callerIdentity);
71
72        mUnconsumedBytes = 0;
73
74        mStopped = false;
75        mDone = false;
76        mIsError = false;
77
78        mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat,
79                channelCount, volume, pan);
80        mLogger = logger;
81    }
82
83
84    @Override
85    public void run() {
86        final UtteranceProgressDispatcher dispatcher = getDispatcher();
87        dispatcher.dispatchOnStart();
88
89
90        if (!mAudioTrack.init()) {
91            dispatcher.dispatchOnError();
92            return;
93        }
94
95        try {
96            byte[] buffer = null;
97
98            // take() will block until:
99            //
100            // (a) there is a buffer available to tread. In which case
101            // a non null value is returned.
102            // OR (b) stop() is called in which case it will return null.
103            // OR (c) done() is called in which case it will return null.
104            while ((buffer = take()) != null) {
105                mAudioTrack.write(buffer);
106                mLogger.onAudioDataWritten();
107            }
108
109        } catch (InterruptedException ie) {
110            if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
111        }
112
113        mAudioTrack.waitAndRelease();
114
115        if (mIsError) {
116            dispatcher.dispatchOnError();
117        } else {
118            dispatcher.dispatchOnDone();
119        }
120
121        mLogger.onWriteData();
122    }
123
124    @Override
125    void stop(boolean isError) {
126        try {
127            mListLock.lock();
128
129            // Update our internal state.
130            mStopped = true;
131            mIsError = isError;
132
133            // Wake up the audio playback thread if it was waiting on take().
134            // take() will return null since mStopped was true, and will then
135            // break out of the data write loop.
136            mReadReady.signal();
137
138            // Wake up the synthesis thread if it was waiting on put(). Its
139            // buffers will no longer be copied since mStopped is true. The
140            // PlaybackSynthesisCallback that this synthesis corresponds to
141            // would also have been stopped, and so all calls to
142            // Callback.onDataAvailable( ) will return errors too.
143            mNotFull.signal();
144        } finally {
145            mListLock.unlock();
146        }
147
148        // Stop the underlying audio track. This will stop sending
149        // data to the mixer and discard any pending buffers that the
150        // track holds.
151        mAudioTrack.stop();
152    }
153
154    void done() {
155        try {
156            mListLock.lock();
157
158            // Update state.
159            mDone = true;
160
161            // Unblocks the audio playback thread if it was waiting on take()
162            // after having consumed all available buffers. It will then return
163            // null and leave the write loop.
164            mReadReady.signal();
165
166            // Just so that engines that try to queue buffers after
167            // calling done() don't block the synthesis thread forever. Ideally
168            // this should be called from the same thread as put() is, and hence
169            // this call should be pointless.
170            mNotFull.signal();
171        } finally {
172            mListLock.unlock();
173        }
174    }
175
176
177    void put(byte[] buffer) throws InterruptedException {
178        try {
179            mListLock.lock();
180            long unconsumedAudioMs = 0;
181
182            while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
183                    MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
184                mNotFull.await();
185            }
186
187            // Don't bother queueing the buffer if we've stopped. The playback thread
188            // would have woken up when stop() is called (if it was blocked) and will
189            // proceed to leave the write loop since take() will return null when
190            // stopped.
191            if (mStopped) {
192                return;
193            }
194
195            mDataBufferList.add(new ListEntry(buffer));
196            mUnconsumedBytes += buffer.length;
197            mReadReady.signal();
198        } finally {
199            mListLock.unlock();
200        }
201    }
202
203    private byte[] take() throws InterruptedException {
204        try {
205            mListLock.lock();
206
207            // Block if there are no available buffers, and stop() has not
208            // been called and done() has not been called.
209            while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
210                mReadReady.await();
211            }
212
213            // If stopped, return null so that we can exit the playback loop
214            // as soon as possible.
215            if (mStopped) {
216                return null;
217            }
218
219            // Remove the first entry from the queue.
220            ListEntry entry = mDataBufferList.poll();
221
222            // This is the normal playback loop exit case, when done() was
223            // called. (mDone will be true at this point).
224            if (entry == null) {
225                return null;
226            }
227
228            mUnconsumedBytes -= entry.mBytes.length;
229            // Unblock the waiting writer. We use signal() and not signalAll()
230            // because there will only be one thread waiting on this (the
231            // Synthesis thread).
232            mNotFull.signal();
233
234            return entry.mBytes;
235        } finally {
236            mListLock.unlock();
237        }
238    }
239
240    static final class ListEntry {
241        final byte[] mBytes;
242
243        ListEntry(byte[] bytes) {
244            mBytes = bytes;
245        }
246    }
247}
248