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
21/**
22 * Speech synthesis request that plays the audio as it is received.
23 */
24class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
25
26    private static final String TAG = "PlaybackSynthesisRequest";
27    private static final boolean DBG = false;
28
29    private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
30
31    /**
32     * Audio stream type. Must be one of the STREAM_ contants defined in
33     * {@link android.media.AudioManager}.
34     */
35    private final int mStreamType;
36
37    /**
38     * Volume, in the range [0.0f, 1.0f]. The default value is
39     * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
40     */
41    private final float mVolume;
42
43    /**
44     * Left/right position of the audio, in the range [-1.0f, 1.0f].
45     * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
46     */
47    private final float mPan;
48
49    /**
50     * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}.
51     */
52    private final Object mStateLock = new Object();
53
54    // Handler associated with a thread that plays back audio requests.
55    private final AudioPlaybackHandler mAudioTrackHandler;
56    // A request "token", which will be non null after start() has been called.
57    private SynthesisPlaybackQueueItem mItem = null;
58    // Whether this request has been stopped. This is useful for keeping
59    // track whether stop() has been called before start(). In all other cases,
60    // a non-null value of mItem will provide the same information.
61    private boolean mStopped = false;
62
63    private volatile boolean mDone = false;
64
65    private final UtteranceProgressDispatcher mDispatcher;
66    private final Object mCallerIdentity;
67    private final EventLogger mLogger;
68
69    PlaybackSynthesisCallback(int streamType, float volume, float pan,
70            AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher,
71            Object callerIdentity, EventLogger logger) {
72        mStreamType = streamType;
73        mVolume = volume;
74        mPan = pan;
75        mAudioTrackHandler = audioTrackHandler;
76        mDispatcher = dispatcher;
77        mCallerIdentity = callerIdentity;
78        mLogger = logger;
79    }
80
81    @Override
82    void stop() {
83        stopImpl(false);
84    }
85
86    void stopImpl(boolean wasError) {
87        if (DBG) Log.d(TAG, "stop()");
88
89        // Note that mLogger.mError might be true too at this point.
90        mLogger.onStopped();
91
92        SynthesisPlaybackQueueItem item;
93        synchronized (mStateLock) {
94            if (mStopped) {
95                Log.w(TAG, "stop() called twice");
96                return;
97            }
98
99            item = mItem;
100            mStopped = true;
101        }
102
103        if (item != null) {
104            // This might result in the synthesis thread being woken up, at which
105            // point it will write an additional buffer to the item - but we
106            // won't worry about that because the audio playback queue will be cleared
107            // soon after (see SynthHandler#stop(String).
108            item.stop(wasError);
109        } else {
110            // This happens when stop() or error() were called before start() was.
111
112            // In all other cases, mAudioTrackHandler.stop() will
113            // result in onSynthesisDone being called, and we will
114            // write data there.
115            mLogger.onWriteData();
116
117            if (wasError) {
118                // We have to dispatch the error ourselves.
119                mDispatcher.dispatchOnError();
120            }
121        }
122    }
123
124    @Override
125    public int getMaxBufferSize() {
126        // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be
127        // a safe buffer size to pass in.
128        return MIN_AUDIO_BUFFER_SIZE;
129    }
130
131    @Override
132    boolean isDone() {
133        return mDone;
134    }
135
136    @Override
137    public int start(int sampleRateInHz, int audioFormat, int channelCount) {
138        if (DBG) {
139            Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat
140                    + "," + channelCount + ")");
141        }
142
143        int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
144        if (channelConfig == 0) {
145            Log.e(TAG, "Unsupported number of channels :" + channelCount);
146            return TextToSpeech.ERROR;
147        }
148
149        synchronized (mStateLock) {
150            if (mStopped) {
151                if (DBG) Log.d(TAG, "stop() called before start(), returning.");
152                return TextToSpeech.ERROR;
153            }
154            SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
155                    mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
156                    mDispatcher, mCallerIdentity, mLogger);
157            mAudioTrackHandler.enqueue(item);
158            mItem = item;
159        }
160
161        return TextToSpeech.SUCCESS;
162    }
163
164
165    @Override
166    public int audioAvailable(byte[] buffer, int offset, int length) {
167        if (DBG) {
168            Log.d(TAG, "audioAvailable(byte[" + buffer.length + "],"
169                    + offset + "," + length + ")");
170        }
171        if (length > getMaxBufferSize() || length <= 0) {
172            throw new IllegalArgumentException("buffer is too large or of zero length (" +
173                    + length + " bytes)");
174        }
175
176        SynthesisPlaybackQueueItem item = null;
177        synchronized (mStateLock) {
178            if (mItem == null || mStopped) {
179                return TextToSpeech.ERROR;
180            }
181            item = mItem;
182        }
183
184        // Sigh, another copy.
185        final byte[] bufferCopy = new byte[length];
186        System.arraycopy(buffer, offset, bufferCopy, 0, length);
187
188        // Might block on mItem.this, if there are too many buffers waiting to
189        // be consumed.
190        try {
191            item.put(bufferCopy);
192        } catch (InterruptedException ie) {
193            return TextToSpeech.ERROR;
194        }
195
196        mLogger.onEngineDataReceived();
197
198        return TextToSpeech.SUCCESS;
199    }
200
201    @Override
202    public int done() {
203        if (DBG) Log.d(TAG, "done()");
204
205        SynthesisPlaybackQueueItem item = null;
206        synchronized (mStateLock) {
207            if (mDone) {
208                Log.w(TAG, "Duplicate call to done()");
209                return TextToSpeech.ERROR;
210            }
211
212            mDone = true;
213
214            if (mItem == null) {
215                return TextToSpeech.ERROR;
216            }
217
218            item = mItem;
219        }
220
221        item.done();
222        mLogger.onEngineComplete();
223
224        return TextToSpeech.SUCCESS;
225    }
226
227    @Override
228    public void error() {
229        if (DBG) Log.d(TAG, "error() [will call stop]");
230        // Currently, this call will not be logged if error( ) is called
231        // before start.
232        mLogger.onError();
233        stopImpl(true);
234    }
235
236}
237