PlaybackSynthesisCallback.java revision 754c72ed9e8e83e5a913aa7552fc2e1b1b5277e0
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 #mToken} 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 SynthesisMessageParams mToken = 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 mToken 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 String mCallingApp;
67    private final EventLogger mLogger;
68
69    PlaybackSynthesisCallback(int streamType, float volume, float pan,
70            AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher,
71            String callingApp, EventLogger logger) {
72        mStreamType = streamType;
73        mVolume = volume;
74        mPan = pan;
75        mAudioTrackHandler = audioTrackHandler;
76        mDispatcher = dispatcher;
77        mCallingApp = callingApp;
78        mLogger = logger;
79    }
80
81    @Override
82    void stop() {
83        if (DBG) Log.d(TAG, "stop()");
84
85        // Note that mLogger.mError might be true too at this point.
86        mLogger.onStopped();
87
88        SynthesisMessageParams token = null;
89        synchronized (mStateLock) {
90            if (mStopped) {
91                Log.w(TAG, "stop() called twice");
92                return;
93            }
94
95            // mToken will be null if the engine encounters
96            // an error before it called start().
97            if (mToken == null) {
98                // In all other cases, mAudioTrackHandler.stop() will
99                // result in onComplete being called.
100                mLogger.onWriteData();
101            } else {
102                token = mToken;
103            }
104            mStopped = true;
105        }
106
107        if (token != null) {
108            // This might result in the synthesis thread being woken up, at which
109            // point it will write an additional buffer to the token - but we
110            // won't worry about that because the audio playback queue will be cleared
111            // soon after (see SynthHandler#stop(String).
112            token.clearBuffers();
113        }
114    }
115
116    @Override
117    public int getMaxBufferSize() {
118        // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be
119        // a safe buffer size to pass in.
120        return MIN_AUDIO_BUFFER_SIZE;
121    }
122
123    @Override
124    boolean isDone() {
125        return mDone;
126    }
127
128    @Override
129    public int start(int sampleRateInHz, int audioFormat, int channelCount) {
130        if (DBG) {
131            Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat
132                    + "," + channelCount + ")");
133        }
134
135        int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount);
136        if (channelConfig == 0) {
137            Log.e(TAG, "Unsupported number of channels :" + channelCount);
138            return TextToSpeech.ERROR;
139        }
140
141        synchronized (mStateLock) {
142            if (mStopped) {
143                if (DBG) Log.d(TAG, "stop() called before start(), returning.");
144                return TextToSpeech.ERROR;
145            }
146            SynthesisMessageParams params = new SynthesisMessageParams(
147                    mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
148                    mDispatcher, mCallingApp, mLogger);
149            mAudioTrackHandler.enqueueSynthesisStart(params);
150
151            mToken = params;
152        }
153
154        return TextToSpeech.SUCCESS;
155    }
156
157
158    @Override
159    public int audioAvailable(byte[] buffer, int offset, int length) {
160        if (DBG) {
161            Log.d(TAG, "audioAvailable(byte[" + buffer.length + "],"
162                    + offset + "," + length + ")");
163        }
164        if (length > getMaxBufferSize() || length <= 0) {
165            throw new IllegalArgumentException("buffer is too large or of zero length (" +
166                    + length + " bytes)");
167        }
168
169        SynthesisMessageParams token = null;
170        synchronized (mStateLock) {
171            if (mToken == null || mStopped) {
172                return TextToSpeech.ERROR;
173            }
174            token = mToken;
175        }
176
177        // Sigh, another copy.
178        final byte[] bufferCopy = new byte[length];
179        System.arraycopy(buffer, offset, bufferCopy, 0, length);
180        // Might block on mToken.this, if there are too many buffers waiting to
181        // be consumed.
182        token.addBuffer(bufferCopy);
183        mAudioTrackHandler.enqueueSynthesisDataAvailable(token);
184
185        mLogger.onEngineDataReceived();
186
187        return TextToSpeech.SUCCESS;
188    }
189
190    @Override
191    public int done() {
192        if (DBG) Log.d(TAG, "done()");
193
194        SynthesisMessageParams token = null;
195        synchronized (mStateLock) {
196            if (mDone) {
197                Log.w(TAG, "Duplicate call to done()");
198                return TextToSpeech.ERROR;
199            }
200
201            mDone = true;
202
203            if (mToken == null) {
204                return TextToSpeech.ERROR;
205            }
206
207            token = mToken;
208        }
209
210        mAudioTrackHandler.enqueueSynthesisDone(token);
211        mLogger.onEngineComplete();
212
213        return TextToSpeech.SUCCESS;
214    }
215
216    @Override
217    public void error() {
218        if (DBG) Log.d(TAG, "error() [will call stop]");
219        // Currently, this call will not be logged if error( ) is called
220        // before start.
221        mLogger.onError();
222        stop();
223    }
224
225}
226