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
22/**
23 * Speech synthesis request that plays the audio as it is received.
24 */
25class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
26
27    private static final String TAG = "PlaybackSynthesisRequest";
28    private static final boolean DBG = false;
29
30    private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
31
32    private final AudioOutputParams mAudioParams;
33
34    /**
35     * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}.
36     */
37    private final Object mStateLock = new Object();
38
39    // Handler associated with a thread that plays back audio requests.
40    private final AudioPlaybackHandler mAudioTrackHandler;
41    // A request "token", which will be non null after start() has been called.
42    private SynthesisPlaybackQueueItem mItem = null;
43
44    private volatile boolean mDone = false;
45
46    /** Status code of synthesis */
47    protected int mStatusCode;
48
49    private final UtteranceProgressDispatcher mDispatcher;
50    private final Object mCallerIdentity;
51    private final AbstractEventLogger mLogger;
52
53    PlaybackSynthesisCallback(AudioOutputParams audioParams, AudioPlaybackHandler audioTrackHandler,
54            UtteranceProgressDispatcher dispatcher, Object callerIdentity,
55            AbstractEventLogger logger, boolean clientIsUsingV2) {
56        super(clientIsUsingV2);
57        mAudioParams = audioParams;
58        mAudioTrackHandler = audioTrackHandler;
59        mDispatcher = dispatcher;
60        mCallerIdentity = callerIdentity;
61        mLogger = logger;
62        mStatusCode = TextToSpeech.SUCCESS;
63    }
64
65    @Override
66    void stop() {
67        if (DBG) Log.d(TAG, "stop()");
68
69        SynthesisPlaybackQueueItem item;
70        synchronized (mStateLock) {
71            if (mDone) {
72                return;
73            }
74            if (mStatusCode == TextToSpeech.STOPPED) {
75                Log.w(TAG, "stop() called twice");
76                return;
77            }
78
79            item = mItem;
80            mStatusCode = TextToSpeech.STOPPED;
81        }
82
83        if (item != null) {
84            // This might result in the synthesis thread being woken up, at which
85            // point it will write an additional buffer to the item - but we
86            // won't worry about that because the audio playback queue will be cleared
87            // soon after (see SynthHandler#stop(String).
88            item.stop(TextToSpeech.STOPPED);
89        } else {
90            // This happens when stop() or error() were called before start() was.
91
92            // In all other cases, mAudioTrackHandler.stop() will
93            // result in onSynthesisDone being called, and we will
94            // write data there.
95            mLogger.onCompleted(TextToSpeech.STOPPED);
96            mDispatcher.dispatchOnStop();
97        }
98    }
99
100    @Override
101    public int getMaxBufferSize() {
102        // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be
103        // a safe buffer size to pass in.
104        return MIN_AUDIO_BUFFER_SIZE;
105    }
106
107    @Override
108    public boolean hasStarted() {
109        synchronized (mStateLock) {
110            return mItem != null;
111        }
112    }
113
114    @Override
115    public boolean hasFinished() {
116        synchronized (mStateLock) {
117            return mDone;
118        }
119    }
120
121    @Override
122    public int start(int sampleRateInHz, int audioFormat, int channelCount) {
123        if (DBG) Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + "," + channelCount
124                + ")");
125
126        int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
127
128        synchronized (mStateLock) {
129            if (channelConfig == 0) {
130                Log.e(TAG, "Unsupported number of channels :" + channelCount);
131                mStatusCode = TextToSpeech.ERROR_OUTPUT;
132                return TextToSpeech.ERROR;
133            }
134            if (mStatusCode == TextToSpeech.STOPPED) {
135                if (DBG) Log.d(TAG, "stop() called before start(), returning.");
136                return errorCodeOnStop();
137            }
138            if (mStatusCode != TextToSpeech.SUCCESS) {
139                if (DBG) Log.d(TAG, "Error was raised");
140                return TextToSpeech.ERROR;
141            }
142            if (mItem != null) {
143                Log.e(TAG, "Start called twice");
144                return TextToSpeech.ERROR;
145            }
146            SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
147                    mAudioParams, sampleRateInHz, audioFormat, channelCount,
148                    mDispatcher, mCallerIdentity, mLogger);
149            mAudioTrackHandler.enqueue(item);
150            mItem = item;
151        }
152
153        return TextToSpeech.SUCCESS;
154    }
155
156    @Override
157    public int audioAvailable(byte[] buffer, int offset, int length) {
158        if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length
159                + ")");
160
161        if (length > getMaxBufferSize() || length <= 0) {
162            throw new IllegalArgumentException("buffer is too large or of zero length (" +
163                    + length + " bytes)");
164        }
165
166        SynthesisPlaybackQueueItem item = null;
167        synchronized (mStateLock) {
168            if (mItem == null) {
169                mStatusCode = TextToSpeech.ERROR_OUTPUT;
170                return TextToSpeech.ERROR;
171            }
172            if (mStatusCode != TextToSpeech.SUCCESS) {
173                if (DBG) Log.d(TAG, "Error was raised");
174                return TextToSpeech.ERROR;
175            }
176            if (mStatusCode == TextToSpeech.STOPPED) {
177                return errorCodeOnStop();
178            }
179            item = mItem;
180        }
181
182        // Sigh, another copy.
183        final byte[] bufferCopy = new byte[length];
184        System.arraycopy(buffer, offset, bufferCopy, 0, length);
185
186        // Might block on mItem.this, if there are too many buffers waiting to
187        // be consumed.
188        try {
189            item.put(bufferCopy);
190        } catch (InterruptedException ie) {
191            synchronized (mStateLock) {
192                mStatusCode = TextToSpeech.ERROR_OUTPUT;
193                return TextToSpeech.ERROR;
194            }
195        }
196
197        mLogger.onEngineDataReceived();
198        return TextToSpeech.SUCCESS;
199    }
200
201    @Override
202    public int done() {
203        if (DBG) Log.d(TAG, "done()");
204
205        int statusCode = 0;
206        SynthesisPlaybackQueueItem item = null;
207        synchronized (mStateLock) {
208            if (mDone) {
209                Log.w(TAG, "Duplicate call to done()");
210                // Not an error that would prevent synthesis. Hence no
211                // setStatusCode
212                return TextToSpeech.ERROR;
213            }
214            if (mStatusCode == TextToSpeech.STOPPED) {
215                if (DBG) Log.d(TAG, "Request has been aborted.");
216                return errorCodeOnStop();
217            }
218            mDone = true;
219
220            if (mItem == null) {
221                // .done() was called before .start. Treat it as successful synthesis
222                // for a client, despite service bad implementation.
223                Log.w(TAG, "done() was called before start() call");
224                if (mStatusCode == TextToSpeech.SUCCESS) {
225                    mDispatcher.dispatchOnSuccess();
226                } else {
227                    mDispatcher.dispatchOnError(mStatusCode);
228                }
229                mLogger.onEngineComplete();
230                return TextToSpeech.ERROR;
231            }
232
233            item = mItem;
234            statusCode = mStatusCode;
235        }
236
237        // Signal done or error to item
238        if (statusCode == TextToSpeech.SUCCESS) {
239            item.done();
240        } else {
241            item.stop(statusCode);
242        }
243        mLogger.onEngineComplete();
244        return TextToSpeech.SUCCESS;
245    }
246
247    @Override
248    public void error() {
249        error(TextToSpeech.ERROR_SYNTHESIS);
250    }
251
252    @Override
253    public void error(int errorCode) {
254        if (DBG) Log.d(TAG, "error() [will call stop]");
255        synchronized (mStateLock) {
256            if (mDone) {
257                return;
258            }
259            mStatusCode = errorCode;
260        }
261    }
262}
263