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