blob: 15a4ee96ecc890a56552479c1feb7e97ba31814c [file] [log] [blame]
Bjorn Bringert50e657b2011-03-08 16:00:40 +00001/*
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.media.AudioFormat;
19import android.media.AudioTrack;
20import android.util.Log;
21
22/**
23 * Speech synthesis request that plays the audio as it is received.
24 */
25class PlaybackSynthesisRequest extends SynthesisRequest {
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 /**
33 * Audio stream type. Must be one of the STREAM_ contants defined in
34 * {@link android.media.AudioManager}.
35 */
36 private final int mStreamType;
37
38 /**
39 * Volume, in the range [0.0f, 1.0f]. The default value is
40 * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f).
41 */
42 private final float mVolume;
43
44 /**
45 * Left/right position of the audio, in the range [-1.0f, 1.0f].
46 * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f).
47 */
48 private final float mPan;
49
50 private final Object mStateLock = new Object();
51 private AudioTrack mAudioTrack = null;
52 private boolean mStopped = false;
53
54 PlaybackSynthesisRequest(String text, int streamType, float volume, float pan) {
55 super(text);
56 mStreamType = streamType;
57 mVolume = volume;
58 mPan = pan;
59 }
60
61 @Override
62 void stop() {
63 if (DBG) Log.d(TAG, "stop()");
64 synchronized (mStateLock) {
65 mStopped = true;
66 cleanUp();
67 }
68 }
69
70 private void cleanUp() {
71 if (DBG) Log.d(TAG, "cleanUp()");
72 if (mAudioTrack != null) {
73 mAudioTrack.flush();
74 mAudioTrack.stop();
75 // TODO: do we need to wait for playback to finish before releasing?
76 mAudioTrack.release();
77 mAudioTrack = null;
78 }
79 }
80
81 // TODO: add a thread that writes to the AudioTrack?
82 @Override
83 public int start(int sampleRateInHz, int audioFormat, int channelCount) {
84 if (DBG) {
85 Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat
86 + "," + channelCount + ")");
87 }
88
89 int channelConfig;
90 if (channelCount == 1) {
91 channelConfig = AudioFormat.CHANNEL_OUT_MONO;
92 } else if (channelCount == 2){
93 channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
94 } else {
95 Log.e(TAG, "Unsupported number of channels: " + channelCount);
96 return TextToSpeech.ERROR;
97 }
98
99 int minBufferSizeInBytes
100 = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
101 int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
102
103 synchronized (mStateLock) {
104 if (mStopped) {
105 if (DBG) Log.d(TAG, "Request has been aborted.");
106 return TextToSpeech.ERROR;
107 }
108 if (mAudioTrack != null) {
109 Log.e(TAG, "start() called twice");
110 cleanUp();
111 return TextToSpeech.ERROR;
112 }
113
114 mAudioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig, audioFormat,
115 bufferSizeInBytes, AudioTrack.MODE_STREAM);
116 if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
117 cleanUp();
118 return TextToSpeech.ERROR;
119 }
120
121 setupVolume();
122 }
123
124 return TextToSpeech.SUCCESS;
125 }
126
127 private void setupVolume() {
128 float vol = clip(mVolume, 0.0f, 1.0f);
129 float panning = clip(mPan, -1.0f, 1.0f);
130 float volLeft = vol;
131 float volRight = vol;
132 if (panning > 0.0f) {
133 volLeft *= (1.0f - panning);
134 } else if (panning < 0.0f) {
135 volRight *= (1.0f + panning);
136 }
137 if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
138 if (mAudioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
139 Log.e(TAG, "Failed to set volume");
140 }
141 }
142
143 private float clip(float value, float min, float max) {
144 return value > max ? max : (value < min ? min : value);
145 }
146
147 @Override
148 public int audioAvailable(byte[] buffer, int offset, int length) {
149 if (DBG) {
150 Log.d(TAG, "audioAvailable(byte[" + buffer.length + "],"
151 + offset + "," + length + "), thread ID=" + android.os.Process.myTid());
152 }
153 synchronized (mStateLock) {
154 if (mStopped) {
155 if (DBG) Log.d(TAG, "Request has been aborted.");
156 return TextToSpeech.ERROR;
157 }
158 if (mAudioTrack == null) {
159 Log.e(TAG, "audioAvailable(): Not started");
160 return TextToSpeech.ERROR;
161 }
162 int playState = mAudioTrack.getPlayState();
163 if (playState == AudioTrack.PLAYSTATE_STOPPED) {
164 if (DBG) Log.d(TAG, "AudioTrack stopped, restarting");
165 mAudioTrack.play();
166 }
167 // TODO: loop until all data is written?
168 if (DBG) Log.d(TAG, "AudioTrack.write()");
169 int count = mAudioTrack.write(buffer, offset, length);
170 if (DBG) Log.d(TAG, "AudioTrack.write() returned " + count);
171 if (count < 0) {
172 Log.e(TAG, "Writing to AudioTrack failed: " + count);
173 cleanUp();
174 return TextToSpeech.ERROR;
175 } else {
176 return TextToSpeech.SUCCESS;
177 }
178 }
179 }
180
181 @Override
182 public int done() {
183 if (DBG) Log.d(TAG, "done()");
184 synchronized (mStateLock) {
185 if (mStopped) {
186 if (DBG) Log.d(TAG, "Request has been aborted.");
187 return TextToSpeech.ERROR;
188 }
189 if (mAudioTrack == null) {
190 Log.e(TAG, "done(): Not started");
191 return TextToSpeech.ERROR;
192 }
193 cleanUp();
194 }
195 return TextToSpeech.SUCCESS;
196 }
197}