Merge "Fix the threading behaviour of TTS audio requests."
This commit is contained in:
committed by
Android (Google) Code Review
commit
233c56a350
37
core/java/android/speech/tts/AudioMessageParams.java
Normal file
37
core/java/android/speech/tts/AudioMessageParams.java
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
|
||||
|
||||
class AudioMessageParams extends MessageParams {
|
||||
private final BlockingMediaPlayer mPlayer;
|
||||
|
||||
AudioMessageParams(UtteranceCompletedDispatcher dispatcher, BlockingMediaPlayer player) {
|
||||
super(dispatcher);
|
||||
mPlayer = player;
|
||||
}
|
||||
|
||||
BlockingMediaPlayer getPlayer() {
|
||||
return mPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_AUDIO;
|
||||
}
|
||||
|
||||
}
|
||||
358
core/java/android/speech/tts/AudioPlaybackHandler.java
Normal file
358
core/java/android/speech/tts/AudioPlaybackHandler.java
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTrack;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.speech.tts.SynthesisMessageParams.ListEntry;
|
||||
import android.util.Log;
|
||||
|
||||
class AudioPlaybackHandler extends Handler {
|
||||
private static final String TAG = "TTS.AudioPlaybackHandler";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
|
||||
|
||||
private static final int SYNTHESIS_START = 1;
|
||||
private static final int SYNTHESIS_DATA_AVAILABLE = 2;
|
||||
private static final int SYNTHESIS_COMPLETE_DATA_AVAILABLE = 3;
|
||||
private static final int SYNTHESIS_DONE = 4;
|
||||
|
||||
private static final int PLAY_AUDIO = 5;
|
||||
private static final int PLAY_SILENCE = 6;
|
||||
|
||||
// Accessed by multiple threads, synchronized by "this".
|
||||
private MessageParams mCurrentParams;
|
||||
// Used only for book keeping and error detection.
|
||||
private SynthesisMessageParams mLastSynthesisRequest;
|
||||
|
||||
AudioPlaybackHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void handleMessage(Message msg) {
|
||||
if (msg.what == SYNTHESIS_START) {
|
||||
mCurrentParams = (SynthesisMessageParams) msg.obj;
|
||||
handleSynthesisStart(msg);
|
||||
} else if (msg.what == SYNTHESIS_DATA_AVAILABLE) {
|
||||
handleSynthesisDataAvailable(msg);
|
||||
} else if (msg.what == SYNTHESIS_DONE) {
|
||||
handleSynthesisDone(msg);
|
||||
} else if (msg.what == SYNTHESIS_COMPLETE_DATA_AVAILABLE) {
|
||||
handleSynthesisCompleteDataAvailable(msg);
|
||||
} else if (msg.what == PLAY_AUDIO) {
|
||||
handleAudio(msg);
|
||||
} else if (msg.what == PLAY_SILENCE) {
|
||||
handleSilence(msg);
|
||||
}
|
||||
|
||||
mCurrentParams = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all synthesis for a given {@code token}. If the current token
|
||||
* is currently being processed, an effort will be made to stop it but
|
||||
* that is not guaranteed.
|
||||
*/
|
||||
synchronized public void stop(MessageParams token) {
|
||||
removeCallbacksAndMessages(token);
|
||||
|
||||
if (token.getType() == MessageParams.TYPE_SYNTHESIS) {
|
||||
sendMessageAtFrontOfQueue(obtainMessage(SYNTHESIS_DONE, token));
|
||||
} else if (token == mCurrentParams) {
|
||||
if (token.getType() == MessageParams.TYPE_AUDIO) {
|
||||
((AudioMessageParams) mCurrentParams).getPlayer().stop();
|
||||
} else if (token.getType() == MessageParams.TYPE_SILENCE) {
|
||||
((SilenceMessageParams) mCurrentParams).getConditionVariable().open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the audio playback thread.
|
||||
*/
|
||||
synchronized public void quit() {
|
||||
if (mCurrentParams != null) {
|
||||
stop(mCurrentParams);
|
||||
}
|
||||
getLooper().quit();
|
||||
}
|
||||
|
||||
void enqueueSynthesisStart(SynthesisMessageParams token) {
|
||||
sendMessage(obtainMessage(SYNTHESIS_START, token));
|
||||
}
|
||||
|
||||
void enqueueSynthesisDataAvailable(SynthesisMessageParams token) {
|
||||
sendMessage(obtainMessage(SYNTHESIS_DATA_AVAILABLE, token));
|
||||
}
|
||||
|
||||
void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) {
|
||||
sendMessage(obtainMessage(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token));
|
||||
}
|
||||
|
||||
void enqueueSynthesisDone(SynthesisMessageParams token) {
|
||||
sendMessage(obtainMessage(SYNTHESIS_DONE, token));
|
||||
}
|
||||
|
||||
void enqueueAudio(AudioMessageParams token) {
|
||||
sendMessage(obtainMessage(PLAY_AUDIO, token));
|
||||
}
|
||||
|
||||
void enqueueSilence(SilenceMessageParams token) {
|
||||
sendMessage(obtainMessage(PLAY_SILENCE, token));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// End of public API methods.
|
||||
// -----------------------------------------
|
||||
|
||||
// Currently implemented as blocking the audio playback thread for the
|
||||
// specified duration. If a call to stop() is made, the thread
|
||||
// unblocks.
|
||||
private void handleSilence(Message msg) {
|
||||
if (DBG) Log.d(TAG, "handleSilence()");
|
||||
SilenceMessageParams params = (SilenceMessageParams) msg.obj;
|
||||
if (params.getSilenceDurationMs() > 0) {
|
||||
params.getConditionVariable().block(params.getSilenceDurationMs());
|
||||
}
|
||||
params.getDispatcher().dispatchUtteranceCompleted();
|
||||
if (DBG) Log.d(TAG, "handleSilence() done.");
|
||||
}
|
||||
|
||||
// Plays back audio from a given URI. No TTS engine involvement here.
|
||||
private void handleAudio(Message msg) {
|
||||
if (DBG) Log.d(TAG, "handleAudio()");
|
||||
AudioMessageParams params = (AudioMessageParams) msg.obj;
|
||||
// Note that the BlockingMediaPlayer spawns a separate thread.
|
||||
//
|
||||
// TODO: This can be avoided.
|
||||
params.getPlayer().startAndWait();
|
||||
params.getDispatcher().dispatchUtteranceCompleted();
|
||||
if (DBG) Log.d(TAG, "handleAudio() done.");
|
||||
}
|
||||
|
||||
// Denotes the start of a new synthesis request. We create a new
|
||||
// audio track, and prepare it for incoming data.
|
||||
//
|
||||
// Note that since all TTS synthesis happens on a single thread, we
|
||||
// should ALWAYS see the following order :
|
||||
//
|
||||
// handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone
|
||||
// OR
|
||||
// handleSynthesisCompleteDataAvailable.
|
||||
private void handleSynthesisStart(Message msg) {
|
||||
if (DBG) Log.d(TAG, "handleSynthesisStart()");
|
||||
final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj;
|
||||
|
||||
// Oops, looks like the engine forgot to call done(). We go through
|
||||
// extra trouble to clean the data to prevent the AudioTrack resources
|
||||
// from being leaked.
|
||||
if (mLastSynthesisRequest != null) {
|
||||
Log.w(TAG, "Error : Missing call to done() for request : " +
|
||||
mLastSynthesisRequest);
|
||||
handleSynthesisDone(mLastSynthesisRequest);
|
||||
}
|
||||
|
||||
mLastSynthesisRequest = param;
|
||||
|
||||
// Create the audio track.
|
||||
final AudioTrack audioTrack = createStreamingAudioTrack(
|
||||
param.mStreamType, param.mSampleRateInHz, param.mAudioFormat,
|
||||
param.mChannelCount, param.mVolume, param.mPan);
|
||||
|
||||
param.setAudioTrack(audioTrack);
|
||||
}
|
||||
|
||||
// More data available to be flushed to the audio track.
|
||||
private void handleSynthesisDataAvailable(Message msg) {
|
||||
final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj;
|
||||
if (param.getAudioTrack() == null) {
|
||||
Log.w(TAG, "Error : null audio track in handleDataAvailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (param != mLastSynthesisRequest) {
|
||||
Log.e(TAG, "Call to dataAvailable without done() / start()");
|
||||
return;
|
||||
}
|
||||
|
||||
final AudioTrack audioTrack = param.getAudioTrack();
|
||||
final ListEntry bufferCopy = param.getNextBuffer();
|
||||
|
||||
if (bufferCopy == null) {
|
||||
Log.e(TAG, "No buffers available to play.");
|
||||
return;
|
||||
}
|
||||
|
||||
int playState = audioTrack.getPlayState();
|
||||
if (playState == AudioTrack.PLAYSTATE_STOPPED) {
|
||||
if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode());
|
||||
audioTrack.play();
|
||||
}
|
||||
int count = 0;
|
||||
while (count < bufferCopy.mLength) {
|
||||
// Note that we don't take bufferCopy.mOffset into account because
|
||||
// it is guaranteed to be 0.
|
||||
int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mLength);
|
||||
if (written <= 0) {
|
||||
break;
|
||||
}
|
||||
count += written;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynthesisDone(Message msg) {
|
||||
final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj;
|
||||
handleSynthesisDone(params);
|
||||
}
|
||||
|
||||
// Flush all remaining data to the audio track, stop it and release
|
||||
// all it's resources.
|
||||
private void handleSynthesisDone(SynthesisMessageParams params) {
|
||||
if (DBG) Log.d(TAG, "handleSynthesisDone()");
|
||||
final AudioTrack audioTrack = params.getAudioTrack();
|
||||
|
||||
try {
|
||||
if (audioTrack != null) {
|
||||
audioTrack.flush();
|
||||
audioTrack.stop();
|
||||
audioTrack.release();
|
||||
}
|
||||
} finally {
|
||||
params.setAudioTrack(null);
|
||||
params.getDispatcher().dispatchUtteranceCompleted();
|
||||
mLastSynthesisRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynthesisCompleteDataAvailable(Message msg) {
|
||||
final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj;
|
||||
if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")");
|
||||
|
||||
// Channel config and bytes per frame are checked before
|
||||
// this message is sent.
|
||||
int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount);
|
||||
int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat);
|
||||
|
||||
ListEntry entry = params.getNextBuffer();
|
||||
|
||||
if (entry == null) {
|
||||
Log.w(TAG, "completeDataAvailable : No buffers available to play.");
|
||||
return;
|
||||
}
|
||||
|
||||
final AudioTrack audioTrack = new AudioTrack(params.mStreamType, params.mSampleRateInHz,
|
||||
channelConfig, params.mAudioFormat, entry.mLength, AudioTrack.MODE_STATIC);
|
||||
|
||||
// So that handleDone can access this correctly.
|
||||
params.mAudioTrack = audioTrack;
|
||||
|
||||
try {
|
||||
audioTrack.write(entry.mBytes, entry.mOffset, entry.mLength);
|
||||
setupVolume(audioTrack, params.mVolume, params.mPan);
|
||||
audioTrack.play();
|
||||
blockUntilDone(audioTrack, bytesPerFrame, entry.mLength);
|
||||
if (DBG) Log.d(TAG, "Wrote data to audio track successfully : " + entry.mLength);
|
||||
} catch (IllegalStateException ex) {
|
||||
Log.e(TAG, "Playback error", ex);
|
||||
} finally {
|
||||
handleSynthesisDone(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) {
|
||||
int lengthInFrames = length / bytesPerFrame;
|
||||
int currentPosition = 0;
|
||||
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) {
|
||||
long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
|
||||
audioTrack.getSampleRate();
|
||||
audioTrack.getPlayState();
|
||||
if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," +
|
||||
" Playback position : " + currentPosition);
|
||||
try {
|
||||
Thread.sleep(estimatedTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioTrack createStreamingAudioTrack(int streamType, int sampleRateInHz,
|
||||
int audioFormat, int channelCount, float volume, float pan) {
|
||||
int channelConfig = getChannelConfig(channelCount);
|
||||
|
||||
int minBufferSizeInBytes
|
||||
= AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
|
||||
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
|
||||
|
||||
AudioTrack audioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig,
|
||||
audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
|
||||
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||
Log.w(TAG, "Unable to create audio track.");
|
||||
audioTrack.release();
|
||||
return null;
|
||||
}
|
||||
|
||||
setupVolume(audioTrack, volume, pan);
|
||||
return audioTrack;
|
||||
}
|
||||
|
||||
static int getChannelConfig(int channelCount) {
|
||||
if (channelCount == 1) {
|
||||
return AudioFormat.CHANNEL_OUT_MONO;
|
||||
} else if (channelCount == 2){
|
||||
return AudioFormat.CHANNEL_OUT_STEREO;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int getBytesPerFrame(int audioFormat) {
|
||||
if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
|
||||
return 1;
|
||||
} else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
|
||||
float vol = clip(volume, 0.0f, 1.0f);
|
||||
float panning = clip(pan, -1.0f, 1.0f);
|
||||
float volLeft = vol;
|
||||
float volRight = vol;
|
||||
if (panning > 0.0f) {
|
||||
volLeft *= (1.0f - panning);
|
||||
} else if (panning < 0.0f) {
|
||||
volRight *= (1.0f + panning);
|
||||
}
|
||||
if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
|
||||
if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
|
||||
Log.e(TAG, "Failed to set volume");
|
||||
}
|
||||
}
|
||||
|
||||
private static float clip(float value, float min, float max) {
|
||||
return value > max ? max : (value < min ? min : value);
|
||||
}
|
||||
|
||||
}
|
||||
36
core/java/android/speech/tts/MessageParams.java
Normal file
36
core/java/android/speech/tts/MessageParams.java
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
|
||||
|
||||
abstract class MessageParams {
|
||||
private final UtteranceCompletedDispatcher mDispatcher;
|
||||
|
||||
static final int TYPE_SYNTHESIS = 1;
|
||||
static final int TYPE_AUDIO = 2;
|
||||
static final int TYPE_SILENCE = 3;
|
||||
|
||||
MessageParams(UtteranceCompletedDispatcher dispatcher) {
|
||||
mDispatcher = dispatcher;
|
||||
}
|
||||
|
||||
UtteranceCompletedDispatcher getDispatcher() {
|
||||
return mDispatcher;
|
||||
}
|
||||
|
||||
abstract int getType();
|
||||
}
|
||||
@@ -15,10 +15,8 @@
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTrack;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
@@ -49,54 +47,48 @@ class PlaybackSynthesisRequest extends SynthesisRequest {
|
||||
*/
|
||||
private final float mPan;
|
||||
|
||||
/**
|
||||
* Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}.
|
||||
*/
|
||||
private final Object mStateLock = new Object();
|
||||
private final Handler mAudioTrackHandler;
|
||||
private volatile AudioTrack mAudioTrack = null;
|
||||
private boolean mStopped = false;
|
||||
private boolean mDone = false;
|
||||
private volatile boolean mWriteErrorOccured;
|
||||
|
||||
PlaybackSynthesisRequest(String text, Bundle params,
|
||||
int streamType, float volume, float pan, Handler audioTrackHandler) {
|
||||
// Handler associated with a thread that plays back audio requests.
|
||||
private final AudioPlaybackHandler mAudioTrackHandler;
|
||||
// A request "token", which will be non null after start() or
|
||||
// completeAudioAvailable() have been called.
|
||||
private SynthesisMessageParams mToken = null;
|
||||
// Whether this request has been stopped. This is useful for keeping
|
||||
// track whether stop() has been called before start(). In all other cases,
|
||||
// a non-null value of mToken will provide the same information.
|
||||
private boolean mStopped = false;
|
||||
|
||||
private volatile boolean mDone = false;
|
||||
|
||||
private final UtteranceCompletedDispatcher mDispatcher;
|
||||
|
||||
PlaybackSynthesisRequest(String text, Bundle params, int streamType, float volume, float pan,
|
||||
AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher) {
|
||||
super(text, params);
|
||||
mStreamType = streamType;
|
||||
mVolume = volume;
|
||||
mPan = pan;
|
||||
mAudioTrackHandler = audioTrackHandler;
|
||||
mWriteErrorOccured = false;
|
||||
mDispatcher = dispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
void stop() {
|
||||
if (DBG) Log.d(TAG, "stop()");
|
||||
|
||||
synchronized (mStateLock) {
|
||||
mStopped = true;
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
// Always guarded by mStateLock.
|
||||
private void cleanUp() {
|
||||
if (DBG) Log.d(TAG, "cleanUp()");
|
||||
if (mAudioTrack == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AudioTrack audioTrack = mAudioTrack;
|
||||
mAudioTrack = null;
|
||||
|
||||
// Clean up on the audiotrack handler thread.
|
||||
//
|
||||
// NOTE: It isn't very clear whether AudioTrack is thread safe.
|
||||
// If it is we can clean up on the current (synthesis) thread.
|
||||
mAudioTrackHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
audioTrack.flush();
|
||||
audioTrack.stop();
|
||||
audioTrack.release();
|
||||
if (mToken == null || mStopped) {
|
||||
Log.w(TAG, "stop() called twice, before start(), or after done()");
|
||||
return;
|
||||
}
|
||||
});
|
||||
mAudioTrackHandler.stop(mToken);
|
||||
mToken = null;
|
||||
mStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,7 +103,6 @@ class PlaybackSynthesisRequest extends SynthesisRequest {
|
||||
return mDone;
|
||||
}
|
||||
|
||||
// TODO: add a thread that writes to the AudioTrack?
|
||||
@Override
|
||||
public int start(int sampleRateInHz, int audioFormat, int channelCount) {
|
||||
if (DBG) {
|
||||
@@ -119,45 +110,28 @@ class PlaybackSynthesisRequest extends SynthesisRequest {
|
||||
+ "," + channelCount + ")");
|
||||
}
|
||||
|
||||
int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount);
|
||||
if (channelConfig == 0) {
|
||||
Log.e(TAG, "Unsupported number of channels :" + channelCount);
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
synchronized (mStateLock) {
|
||||
if (mStopped) {
|
||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mAudioTrack != null) {
|
||||
Log.e(TAG, "start() called twice");
|
||||
cleanUp();
|
||||
if (DBG) Log.d(TAG, "stop() called before start(), returning.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
SynthesisMessageParams params = new SynthesisMessageParams(
|
||||
mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
|
||||
mDispatcher);
|
||||
mAudioTrackHandler.enqueueSynthesisStart(params);
|
||||
|
||||
mAudioTrack = createStreamingAudioTrack(sampleRateInHz, audioFormat, channelCount);
|
||||
if (mAudioTrack == null) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
mToken = params;
|
||||
}
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
private void setupVolume(AudioTrack audioTrack, float volume, float pan) {
|
||||
float vol = clip(volume, 0.0f, 1.0f);
|
||||
float panning = clip(pan, -1.0f, 1.0f);
|
||||
float volLeft = vol;
|
||||
float volRight = vol;
|
||||
if (panning > 0.0f) {
|
||||
volLeft *= (1.0f - panning);
|
||||
} else if (panning < 0.0f) {
|
||||
volRight *= (1.0f + panning);
|
||||
}
|
||||
if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
|
||||
if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
|
||||
Log.e(TAG, "Failed to set volume");
|
||||
}
|
||||
}
|
||||
|
||||
private float clip(float value, float min, float max) {
|
||||
return value > max ? max : (value < min ? min : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int audioAvailable(byte[] buffer, int offset, int length) {
|
||||
@@ -169,195 +143,78 @@ class PlaybackSynthesisRequest extends SynthesisRequest {
|
||||
throw new IllegalArgumentException("buffer is too large or of zero length (" +
|
||||
+ length + " bytes)");
|
||||
}
|
||||
|
||||
synchronized (mStateLock) {
|
||||
if (mWriteErrorOccured) {
|
||||
if (DBG) Log.d(TAG, "Error writing to audio track, count < 0");
|
||||
if (mToken == null) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mStopped) {
|
||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mAudioTrack == null) {
|
||||
Log.e(TAG, "audioAvailable(): Not started");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
final AudioTrack audioTrack = mAudioTrack;
|
||||
|
||||
// Sigh, another copy.
|
||||
final byte[] bufferCopy = new byte[length];
|
||||
System.arraycopy(buffer, offset, bufferCopy, 0, length);
|
||||
|
||||
mAudioTrackHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int playState = audioTrack.getPlayState();
|
||||
if (playState == AudioTrack.PLAYSTATE_STOPPED) {
|
||||
if (DBG) Log.d(TAG, "AudioTrack stopped, restarting");
|
||||
audioTrack.play();
|
||||
}
|
||||
// TODO: loop until all data is written?
|
||||
if (DBG) Log.d(TAG, "AudioTrack.write()");
|
||||
int count = audioTrack.write(bufferCopy, 0, bufferCopy.length);
|
||||
// The semantics of this change very slightly. Earlier, we would
|
||||
// report an error immediately, Now we will return an error on
|
||||
// the next API call, usually done( ) or another audioAvailable( )
|
||||
// call.
|
||||
if (count < 0) {
|
||||
mWriteErrorOccured = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
mToken.addBuffer(bufferCopy);
|
||||
mAudioTrackHandler.enqueueSynthesisDataAvailable(mToken);
|
||||
}
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int done() {
|
||||
if (DBG) Log.d(TAG, "done()");
|
||||
|
||||
synchronized (mStateLock) {
|
||||
if (mWriteErrorOccured) {
|
||||
if (DBG) Log.d(TAG, "Error writing to audio track, count < 0");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mStopped) {
|
||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mAudioTrack == null) {
|
||||
Log.e(TAG, "done(): Not started");
|
||||
if (mDone) {
|
||||
Log.w(TAG, "Duplicate call to done()");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
mDone = true;
|
||||
cleanUp();
|
||||
|
||||
if (mToken == null) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
mAudioTrackHandler.enqueueSynthesisDone(mToken);
|
||||
}
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error() {
|
||||
if (DBG) Log.d(TAG, "error()");
|
||||
synchronized (mStateLock) {
|
||||
cleanUp();
|
||||
}
|
||||
if (DBG) Log.d(TAG, "error() [will call stop]");
|
||||
stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount,
|
||||
byte[] buffer, int offset, int length) {
|
||||
if (DBG) {
|
||||
Log.d(TAG, "completeAudioAvailable(" + sampleRateInHz + "," + audioFormat
|
||||
+ "," + channelCount + "byte[" + buffer.length + "],"
|
||||
+ offset + "," + length + ")");
|
||||
int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount);
|
||||
if (channelConfig == 0) {
|
||||
Log.e(TAG, "Unsupported number of channels :" + channelCount);
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(audioFormat);
|
||||
if (bytesPerFrame < 0) {
|
||||
Log.e(TAG, "Unsupported audio format :" + audioFormat);
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
synchronized (mStateLock) {
|
||||
if (mStopped) {
|
||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
if (mAudioTrack != null) {
|
||||
Log.e(TAG, "start() called before completeAudioAvailable()");
|
||||
cleanUp();
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
SynthesisMessageParams params = new SynthesisMessageParams(
|
||||
mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
|
||||
mDispatcher);
|
||||
params.addBuffer(buffer, offset, length);
|
||||
|
||||
int channelConfig = getChannelConfig(channelCount);
|
||||
if (channelConfig < 0) {
|
||||
Log.e(TAG, "Unsupported number of channels :" + channelCount);
|
||||
cleanUp();
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
int bytesPerFrame = getBytesPerFrame(audioFormat);
|
||||
if (bytesPerFrame < 0) {
|
||||
Log.e(TAG, "Unsupported audio format :" + audioFormat);
|
||||
cleanUp();
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
mAudioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig,
|
||||
audioFormat, buffer.length, AudioTrack.MODE_STATIC);
|
||||
if (mAudioTrack == null) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
mAudioTrack.write(buffer, offset, length);
|
||||
setupVolume(mAudioTrack, mVolume, mPan);
|
||||
mAudioTrack.play();
|
||||
blockUntilDone(mAudioTrack, bytesPerFrame, length);
|
||||
mDone = true;
|
||||
if (DBG) Log.d(TAG, "Wrote data to audio track succesfully : " + length);
|
||||
} catch (IllegalStateException ex) {
|
||||
Log.e(TAG, "Playback error", ex);
|
||||
return TextToSpeech.ERROR;
|
||||
} finally {
|
||||
cleanUp();
|
||||
}
|
||||
mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params);
|
||||
mToken = params;
|
||||
}
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
private void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) {
|
||||
int lengthInFrames = length / bytesPerFrame;
|
||||
int currentPosition = 0;
|
||||
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) {
|
||||
long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
|
||||
audioTrack.getSampleRate();
|
||||
if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," +
|
||||
" Playback position : " + currentPosition);
|
||||
try {
|
||||
Thread.sleep(estimatedTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getBytesPerFrame(int audioFormat) {
|
||||
if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
|
||||
return 1;
|
||||
} else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getChannelConfig(int channelCount) {
|
||||
if (channelCount == 1) {
|
||||
return AudioFormat.CHANNEL_OUT_MONO;
|
||||
} else if (channelCount == 2){
|
||||
return AudioFormat.CHANNEL_OUT_STEREO;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private AudioTrack createStreamingAudioTrack(int sampleRateInHz, int audioFormat,
|
||||
int channelCount) {
|
||||
int channelConfig = getChannelConfig(channelCount);
|
||||
|
||||
if (channelConfig < 0) {
|
||||
Log.e(TAG, "Unsupported number of channels : " + channelCount);
|
||||
return null;
|
||||
}
|
||||
|
||||
int minBufferSizeInBytes
|
||||
= AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
|
||||
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
|
||||
AudioTrack audioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig,
|
||||
audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
|
||||
if (audioTrack == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||
audioTrack.release();
|
||||
return null;
|
||||
}
|
||||
setupVolume(audioTrack, mVolume, mPan);
|
||||
return audioTrack;
|
||||
}
|
||||
}
|
||||
|
||||
43
core/java/android/speech/tts/SilenceMessageParams.java
Normal file
43
core/java/android/speech/tts/SilenceMessageParams.java
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.os.ConditionVariable;
|
||||
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
|
||||
|
||||
class SilenceMessageParams extends MessageParams {
|
||||
private final ConditionVariable mCondVar = new ConditionVariable();
|
||||
private final long mSilenceDurationMs;
|
||||
|
||||
SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, long silenceDurationMs) {
|
||||
super(dispatcher);
|
||||
mSilenceDurationMs = silenceDurationMs;
|
||||
}
|
||||
|
||||
long getSilenceDurationMs() {
|
||||
return mSilenceDurationMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_SILENCE;
|
||||
}
|
||||
|
||||
ConditionVariable getConditionVariable() {
|
||||
return mCondVar;
|
||||
}
|
||||
|
||||
}
|
||||
92
core/java/android/speech/tts/SynthesisMessageParams.java
Normal file
92
core/java/android/speech/tts/SynthesisMessageParams.java
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.media.AudioTrack;
|
||||
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Params required to play back a synthesis request.
|
||||
*/
|
||||
final class SynthesisMessageParams extends MessageParams {
|
||||
final int mStreamType;
|
||||
final int mSampleRateInHz;
|
||||
final int mAudioFormat;
|
||||
final int mChannelCount;
|
||||
final float mVolume;
|
||||
final float mPan;
|
||||
|
||||
public volatile AudioTrack mAudioTrack;
|
||||
|
||||
private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
|
||||
|
||||
SynthesisMessageParams(int streamType, int sampleRate,
|
||||
int audioFormat, int channelCount,
|
||||
float volume, float pan, UtteranceCompletedDispatcher dispatcher) {
|
||||
super(dispatcher);
|
||||
|
||||
mStreamType = streamType;
|
||||
mSampleRateInHz = sampleRate;
|
||||
mAudioFormat = audioFormat;
|
||||
mChannelCount = channelCount;
|
||||
mVolume = volume;
|
||||
mPan = pan;
|
||||
|
||||
// initially null.
|
||||
mAudioTrack = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_SYNTHESIS;
|
||||
}
|
||||
|
||||
synchronized void addBuffer(byte[] buffer, int offset, int length) {
|
||||
mDataBufferList.add(new ListEntry(buffer, offset, length));
|
||||
}
|
||||
|
||||
synchronized void addBuffer(byte[] buffer) {
|
||||
mDataBufferList.add(new ListEntry(buffer, 0, buffer.length));
|
||||
}
|
||||
|
||||
synchronized ListEntry getNextBuffer() {
|
||||
return mDataBufferList.poll();
|
||||
}
|
||||
|
||||
|
||||
void setAudioTrack(AudioTrack audioTrack) {
|
||||
mAudioTrack = audioTrack;
|
||||
}
|
||||
|
||||
AudioTrack getAudioTrack() {
|
||||
return mAudioTrack;
|
||||
}
|
||||
|
||||
static final class ListEntry {
|
||||
final byte[] mBytes;
|
||||
final int mOffset;
|
||||
final int mLength;
|
||||
|
||||
ListEntry(byte[] bytes, int offset, int length) {
|
||||
mBytes = bytes;
|
||||
mOffset = offset;
|
||||
mLength = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ public abstract class TextToSpeechService extends Service {
|
||||
private static final String SYNTH_THREAD_NAME = "SynthThread";
|
||||
|
||||
private SynthHandler mSynthHandler;
|
||||
private Handler mAudioTrackHandler;
|
||||
// A thread and it's associated handler for playing back any audio
|
||||
// associated with this TTS engine. Will handle all requests except synthesis
|
||||
// to file requests, which occur on the synthesis thread.
|
||||
private AudioPlaybackHandler mAudioPlaybackHandler;
|
||||
|
||||
private CallbackMap mCallbacks;
|
||||
|
||||
@@ -68,7 +71,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread");
|
||||
audioTrackThread.start();
|
||||
mAudioTrackHandler = new Handler(audioTrackThread.getLooper());
|
||||
mAudioPlaybackHandler = new AudioPlaybackHandler(audioTrackThread.getLooper());
|
||||
|
||||
mCallbacks = new CallbackMap();
|
||||
|
||||
@@ -83,8 +86,8 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
// Tell the synthesizer to stop
|
||||
mSynthHandler.quit();
|
||||
mAudioTrackHandler.getLooper().quit();
|
||||
|
||||
// Tell the audio playback thread to stop.
|
||||
mAudioPlaybackHandler.quit();
|
||||
// Unregister all callbacks.
|
||||
mCallbacks.kill();
|
||||
|
||||
@@ -236,13 +239,6 @@ public abstract class TextToSpeechService extends Service {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
private void dispatchUtteranceCompleted(SpeechItem item) {
|
||||
String utteranceId = item.getUtteranceId();
|
||||
if (!TextUtils.isEmpty(utteranceId)) {
|
||||
mCallbacks.dispatchUtteranceCompleted(item.getCallingApp(), utteranceId);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized SpeechItem getCurrentSpeechItem() {
|
||||
return mCurrentSpeechItem;
|
||||
}
|
||||
@@ -286,9 +282,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
@Override
|
||||
public void run() {
|
||||
setCurrentSpeechItem(speechItem);
|
||||
if (speechItem.play() == TextToSpeech.SUCCESS) {
|
||||
dispatchUtteranceCompleted(speechItem);
|
||||
}
|
||||
speechItem.play();
|
||||
setCurrentSpeechItem(null);
|
||||
}
|
||||
};
|
||||
@@ -318,14 +312,19 @@ public abstract class TextToSpeechService extends Service {
|
||||
if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) {
|
||||
current.stop();
|
||||
}
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
interface UtteranceCompletedDispatcher {
|
||||
public void dispatchUtteranceCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* An item in the synth thread queue.
|
||||
*/
|
||||
private static abstract class SpeechItem {
|
||||
private abstract class SpeechItem implements UtteranceCompletedDispatcher {
|
||||
private final String mCallingApp;
|
||||
protected final Bundle mParams;
|
||||
private boolean mStarted = false;
|
||||
@@ -380,6 +379,13 @@ public abstract class TextToSpeechService extends Service {
|
||||
stopImpl();
|
||||
}
|
||||
|
||||
public void dispatchUtteranceCompleted() {
|
||||
final String utteranceId = getUtteranceId();
|
||||
if (!TextUtils.isEmpty(utteranceId)) {
|
||||
mCallbacks.dispatchUtteranceCompleted(getCallingApp(), utteranceId);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int playImpl();
|
||||
|
||||
protected abstract void stopImpl();
|
||||
@@ -413,7 +419,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private class SynthesisSpeechItem extends SpeechItem {
|
||||
class SynthesisSpeechItem extends SpeechItem {
|
||||
private final String mText;
|
||||
private SynthesisRequest mSynthesisRequest;
|
||||
|
||||
@@ -453,7 +459,8 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
protected SynthesisRequest createSynthesisRequest() {
|
||||
return new PlaybackSynthesisRequest(mText, mParams,
|
||||
getStreamType(), getVolume(), getPan(), mAudioTrackHandler);
|
||||
getStreamType(), getVolume(), getPan(), mAudioPlaybackHandler,
|
||||
this);
|
||||
}
|
||||
|
||||
private void setRequestParams(SynthesisRequest request) {
|
||||
@@ -526,6 +533,15 @@ public abstract class TextToSpeechService extends Service {
|
||||
return new FileSynthesisRequest(getText(), mParams, mFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int playImpl() {
|
||||
int status = super.playImpl();
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
dispatchUtteranceCompleted();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the given file can be used for synthesis output.
|
||||
*/
|
||||
@@ -557,6 +573,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
private class AudioSpeechItem extends SpeechItem {
|
||||
|
||||
private final BlockingMediaPlayer mPlayer;
|
||||
private AudioMessageParams mToken;
|
||||
|
||||
public AudioSpeechItem(String callingApp, Bundle params, Uri uri) {
|
||||
super(callingApp, params);
|
||||
@@ -570,23 +587,26 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
@Override
|
||||
protected int playImpl() {
|
||||
return mPlayer.startAndWait() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR;
|
||||
mToken = new AudioMessageParams(this, mPlayer);
|
||||
mAudioPlaybackHandler.enqueueAudio(mToken);
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopImpl() {
|
||||
mPlayer.stop();
|
||||
if (mToken != null) {
|
||||
mAudioPlaybackHandler.stop(mToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SilenceSpeechItem extends SpeechItem {
|
||||
private final long mDuration;
|
||||
private final ConditionVariable mDone;
|
||||
private SilenceMessageParams mToken;
|
||||
|
||||
public SilenceSpeechItem(String callingApp, Bundle params, long duration) {
|
||||
super(callingApp, params);
|
||||
mDuration = duration;
|
||||
mDone = new ConditionVariable();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -596,13 +616,16 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
@Override
|
||||
protected int playImpl() {
|
||||
boolean aborted = mDone.block(mDuration);
|
||||
return aborted ? TextToSpeech.ERROR : TextToSpeech.SUCCESS;
|
||||
mToken = new SilenceMessageParams(this, mDuration);
|
||||
mAudioPlaybackHandler.enqueueSilence(mToken);
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopImpl() {
|
||||
mDone.open();
|
||||
if (mToken != null) {
|
||||
mAudioPlaybackHandler.stop(mToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user