Simplify the implementation of AudioPlaybackHandler.
Now uses a queue of runnables, one per synthesis. Also introduce an abstraction that wraps AudioTrack that implements the blocking semantics that we desire. bug:5680699 Change-Id: I34a1248ff05766a7d9b3002055fb5b24aa9f230b
This commit is contained in:
@@ -15,44 +15,20 @@
|
||||
*/
|
||||
package android.speech.tts;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTrack;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
class AudioPlaybackHandler {
|
||||
private static final String TAG = "TTS.AudioPlaybackHandler";
|
||||
private static final boolean DBG_THREADING = false;
|
||||
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_DONE = 3;
|
||||
|
||||
private static final int PLAY_AUDIO = 5;
|
||||
private static final int PLAY_SILENCE = 6;
|
||||
|
||||
private static final int SHUTDOWN = -1;
|
||||
|
||||
private static final int DEFAULT_PRIORITY = 1;
|
||||
private static final int HIGH_PRIORITY = 0;
|
||||
|
||||
private final PriorityBlockingQueue<ListEntry> mQueue =
|
||||
new PriorityBlockingQueue<ListEntry>();
|
||||
private final LinkedBlockingQueue<PlaybackQueueItem> mQueue =
|
||||
new LinkedBlockingQueue<PlaybackQueueItem>();
|
||||
private final Thread mHandlerThread;
|
||||
|
||||
private volatile MessageParams mCurrentParams = null;
|
||||
// Used only for book keeping and error detection.
|
||||
private volatile SynthesisMessageParams mLastSynthesisRequest = null;
|
||||
// Used to order incoming messages in our priority queue.
|
||||
private final AtomicLong mSequenceIdCtr = new AtomicLong(0);
|
||||
|
||||
private volatile PlaybackQueueItem mCurrentWorkItem = null;
|
||||
|
||||
AudioPlaybackHandler() {
|
||||
mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread");
|
||||
@@ -62,82 +38,38 @@ class AudioPlaybackHandler {
|
||||
mHandlerThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* NOTE: This assumes that all other messages in the queue with {@code token}
|
||||
* have been removed already.
|
||||
*
|
||||
* NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}.
|
||||
*/
|
||||
private void stop(MessageParams token) {
|
||||
if (token == null) {
|
||||
private void stop(PlaybackQueueItem item) {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DBG) Log.d(TAG, "Stopping token : " + token);
|
||||
item.stop(false);
|
||||
}
|
||||
|
||||
if (token.getType() == MessageParams.TYPE_SYNTHESIS) {
|
||||
AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack();
|
||||
if (current != null) {
|
||||
// Stop the current audio track if it's still playing.
|
||||
// The audio track is thread safe in this regard. The current
|
||||
// handleSynthesisDataAvailable call will return soon after this
|
||||
// call.
|
||||
current.stop();
|
||||
}
|
||||
// This is safe because PlaybackSynthesisCallback#stop would have
|
||||
// been called before this method, and will no longer enqueue any
|
||||
// audio for this token.
|
||||
//
|
||||
// (Even if it did, all it would result in is a warning message).
|
||||
mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY));
|
||||
} else if (token.getType() == MessageParams.TYPE_AUDIO) {
|
||||
((AudioMessageParams) token).getPlayer().stop();
|
||||
// No cleanup required for audio messages.
|
||||
} else if (token.getType() == MessageParams.TYPE_SILENCE) {
|
||||
((SilenceMessageParams) token).getConditionVariable().open();
|
||||
// No cleanup required for silence messages.
|
||||
public void enqueue(PlaybackQueueItem item) {
|
||||
try {
|
||||
mQueue.put(item);
|
||||
} catch (InterruptedException ie) {
|
||||
// This exception will never be thrown, since we allow our queue
|
||||
// to be have an unbounded size. put() will therefore never block.
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Methods that add and remove elements from the queue. These do not
|
||||
// need to be synchronized strictly speaking, but they make the behaviour
|
||||
// a lot more predictable. (though it would still be correct without
|
||||
// synchronization).
|
||||
// -----------------------------------------------------
|
||||
public void stopForApp(Object callerIdentity) {
|
||||
if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity);
|
||||
removeWorkItemsFor(callerIdentity);
|
||||
|
||||
synchronized public void removePlaybackItems(Object callerIdentity) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callerIdentity);
|
||||
removeMessages(callerIdentity);
|
||||
|
||||
final MessageParams current = getCurrentParams();
|
||||
final PlaybackQueueItem current = mCurrentWorkItem;
|
||||
if (current != null && (current.getCallerIdentity() == callerIdentity)) {
|
||||
stop(current);
|
||||
}
|
||||
|
||||
final MessageParams lastSynthesis = mLastSynthesisRequest;
|
||||
|
||||
if (lastSynthesis != null && lastSynthesis != current &&
|
||||
(lastSynthesis.getCallerIdentity() == callerIdentity)) {
|
||||
stop(lastSynthesis);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized public void removeAllItems() {
|
||||
if (DBG_THREADING) Log.d(TAG, "Removing all items");
|
||||
public void stop() {
|
||||
if (DBG) Log.d(TAG, "Stopping all items");
|
||||
removeAllMessages();
|
||||
|
||||
final MessageParams current = getCurrentParams();
|
||||
final MessageParams lastSynthesis = mLastSynthesisRequest;
|
||||
stop(current);
|
||||
|
||||
if (lastSynthesis != null && lastSynthesis != current) {
|
||||
stop(lastSynthesis);
|
||||
}
|
||||
stop(mCurrentWorkItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,51 +77,39 @@ class AudioPlaybackHandler {
|
||||
* being handled, true otherwise.
|
||||
*/
|
||||
public boolean isSpeaking() {
|
||||
return (mQueue.peek() != null) || (mCurrentParams != null);
|
||||
return (mQueue.peek() != null) || (mCurrentWorkItem != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the audio playback thread.
|
||||
*/
|
||||
synchronized public void quit() {
|
||||
public void quit() {
|
||||
removeAllMessages();
|
||||
stop(getCurrentParams());
|
||||
mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY));
|
||||
stop(mCurrentWorkItem);
|
||||
mHandlerThread.interrupt();
|
||||
}
|
||||
|
||||
synchronized void enqueueSynthesisStart(SynthesisMessageParams token) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token);
|
||||
mQueue.add(new ListEntry(SYNTHESIS_START, token));
|
||||
/*
|
||||
* Atomically clear the queue of all messages.
|
||||
*/
|
||||
private void removeAllMessages() {
|
||||
mQueue.clear();
|
||||
}
|
||||
|
||||
synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token);
|
||||
mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token));
|
||||
/*
|
||||
* Remove all messages that originate from a given calling app.
|
||||
*/
|
||||
private void removeWorkItemsFor(Object callerIdentity) {
|
||||
Iterator<PlaybackQueueItem> it = mQueue.iterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
final PlaybackQueueItem item = it.next();
|
||||
if (item.getCallerIdentity() == callerIdentity) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void enqueueSynthesisDone(SynthesisMessageParams token) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token);
|
||||
mQueue.add(new ListEntry(SYNTHESIS_DONE, token));
|
||||
}
|
||||
|
||||
synchronized void enqueueAudio(AudioMessageParams token) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token);
|
||||
mQueue.add(new ListEntry(PLAY_AUDIO, token));
|
||||
}
|
||||
|
||||
synchronized void enqueueSilence(SilenceMessageParams token) {
|
||||
if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token);
|
||||
mQueue.add(new ListEntry(PLAY_SILENCE, token));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// End of public API methods.
|
||||
// -----------------------------------------
|
||||
|
||||
// -----------------------------------------
|
||||
// Methods for managing the message queue.
|
||||
// -----------------------------------------
|
||||
|
||||
/*
|
||||
* The MessageLoop is a handler like implementation that
|
||||
* processes messages from a priority queue.
|
||||
@@ -198,436 +118,23 @@ class AudioPlaybackHandler {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
ListEntry entry = null;
|
||||
PlaybackQueueItem item = null;
|
||||
try {
|
||||
entry = mQueue.take();
|
||||
item = mQueue.take();
|
||||
} catch (InterruptedException ie) {
|
||||
if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.mWhat == SHUTDOWN) {
|
||||
if (DBG) Log.d(TAG, "MessageLoop : Shutting down");
|
||||
return;
|
||||
}
|
||||
// If stop() or stopForApp() are called between mQueue.take()
|
||||
// returning and mCurrentWorkItem being set, the current work item
|
||||
// will be run anyway.
|
||||
|
||||
if (DBG) {
|
||||
Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat
|
||||
+ " ,seqId : " + entry.mSequenceId);
|
||||
}
|
||||
|
||||
setCurrentParams(entry.mMessage);
|
||||
handleMessage(entry);
|
||||
setCurrentParams(null);
|
||||
mCurrentWorkItem = item;
|
||||
item.run();
|
||||
mCurrentWorkItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Atomically clear the queue of all messages.
|
||||
*/
|
||||
synchronized private void removeAllMessages() {
|
||||
mQueue.clear();
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove all messages that originate from a given calling app.
|
||||
*/
|
||||
synchronized private void removeMessages(Object callerIdentity) {
|
||||
Iterator<ListEntry> it = mQueue.iterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
final ListEntry current = it.next();
|
||||
// The null check is to prevent us from removing control messages,
|
||||
// such as a shutdown message.
|
||||
if (current.mMessage != null &&
|
||||
current.mMessage.getCallerIdentity() == callerIdentity) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* An element of our priority queue of messages. Each message has a priority,
|
||||
* and a sequence id (defined by the order of enqueue calls). Among messages
|
||||
* with the same priority, messages that were received earlier win out.
|
||||
*/
|
||||
private final class ListEntry implements Comparable<ListEntry> {
|
||||
final int mWhat;
|
||||
final MessageParams mMessage;
|
||||
final int mPriority;
|
||||
final long mSequenceId;
|
||||
|
||||
private ListEntry(int what, MessageParams message) {
|
||||
this(what, message, DEFAULT_PRIORITY);
|
||||
}
|
||||
|
||||
private ListEntry(int what, MessageParams message, int priority) {
|
||||
mWhat = what;
|
||||
mMessage = message;
|
||||
mPriority = priority;
|
||||
mSequenceId = mSequenceIdCtr.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ListEntry that) {
|
||||
if (that == this) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Note that this is always 0, 1 or -1.
|
||||
int priorityDiff = mPriority - that.mPriority;
|
||||
if (priorityDiff == 0) {
|
||||
// The == case cannot occur.
|
||||
return (mSequenceId < that.mSequenceId) ? -1 : 1;
|
||||
}
|
||||
|
||||
return priorityDiff;
|
||||
}
|
||||
}
|
||||
|
||||
private void setCurrentParams(MessageParams p) {
|
||||
if (DBG_THREADING) {
|
||||
if (p != null) {
|
||||
Log.d(TAG, "Started handling :" + p);
|
||||
} else {
|
||||
Log.d(TAG, "End handling : " + mCurrentParams);
|
||||
}
|
||||
}
|
||||
mCurrentParams = p;
|
||||
}
|
||||
|
||||
private MessageParams getCurrentParams() {
|
||||
return mCurrentParams;
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Methods for dealing with individual messages, the methods
|
||||
// below do the actual work.
|
||||
// -----------------------------------------
|
||||
|
||||
private void handleMessage(ListEntry entry) {
|
||||
final MessageParams msg = entry.mMessage;
|
||||
if (entry.mWhat == SYNTHESIS_START) {
|
||||
handleSynthesisStart(msg);
|
||||
} else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) {
|
||||
handleSynthesisDataAvailable(msg);
|
||||
} else if (entry.mWhat == SYNTHESIS_DONE) {
|
||||
handleSynthesisDone(msg);
|
||||
} else if (entry.mWhat == PLAY_AUDIO) {
|
||||
handleAudio(msg);
|
||||
} else if (entry.mWhat == PLAY_SILENCE) {
|
||||
handleSilence(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 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(MessageParams msg) {
|
||||
if (DBG) Log.d(TAG, "handleSilence()");
|
||||
SilenceMessageParams params = (SilenceMessageParams) msg;
|
||||
params.getDispatcher().dispatchOnStart();
|
||||
if (params.getSilenceDurationMs() > 0) {
|
||||
params.getConditionVariable().block(params.getSilenceDurationMs());
|
||||
}
|
||||
params.getDispatcher().dispatchOnDone();
|
||||
if (DBG) Log.d(TAG, "handleSilence() done.");
|
||||
}
|
||||
|
||||
// Plays back audio from a given URI. No TTS engine involvement here.
|
||||
private void handleAudio(MessageParams msg) {
|
||||
if (DBG) Log.d(TAG, "handleAudio()");
|
||||
AudioMessageParams params = (AudioMessageParams) msg;
|
||||
params.getDispatcher().dispatchOnStart();
|
||||
// Note that the BlockingMediaPlayer spawns a separate thread.
|
||||
//
|
||||
// TODO: This can be avoided.
|
||||
params.getPlayer().startAndWait();
|
||||
params.getDispatcher().dispatchOnDone();
|
||||
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(MessageParams msg) {
|
||||
if (DBG) Log.d(TAG, "handleSynthesisStart()");
|
||||
final SynthesisMessageParams param = (SynthesisMessageParams) msg;
|
||||
|
||||
// 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.e(TAG, "Error : Missing call to done() for request : " +
|
||||
mLastSynthesisRequest);
|
||||
handleSynthesisDone(mLastSynthesisRequest);
|
||||
}
|
||||
|
||||
mLastSynthesisRequest = param;
|
||||
|
||||
// Create the audio track.
|
||||
final AudioTrack audioTrack = createStreamingAudioTrack(param);
|
||||
|
||||
if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]");
|
||||
|
||||
param.setAudioTrack(audioTrack);
|
||||
msg.getDispatcher().dispatchOnStart();
|
||||
}
|
||||
|
||||
// More data available to be flushed to the audio track.
|
||||
private void handleSynthesisDataAvailable(MessageParams msg) {
|
||||
final SynthesisMessageParams param = (SynthesisMessageParams) msg;
|
||||
if (param.getAudioTrack() == null) {
|
||||
Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param);
|
||||
return;
|
||||
}
|
||||
|
||||
if (param != mLastSynthesisRequest) {
|
||||
Log.e(TAG, "Call to dataAvailable without done() / start()");
|
||||
return;
|
||||
}
|
||||
|
||||
final AudioTrack audioTrack = param.getAudioTrack();
|
||||
final SynthesisMessageParams.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.mBytes.length) {
|
||||
// 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.mBytes.length);
|
||||
if (written <= 0) {
|
||||
break;
|
||||
}
|
||||
count += written;
|
||||
}
|
||||
param.mBytesWritten += count;
|
||||
param.mLogger.onPlaybackStart();
|
||||
}
|
||||
|
||||
// Wait for the audio track to stop playing, and then release its resources.
|
||||
private void handleSynthesisDone(MessageParams msg) {
|
||||
final SynthesisMessageParams params = (SynthesisMessageParams) msg;
|
||||
|
||||
if (DBG) Log.d(TAG, "handleSynthesisDone()");
|
||||
final AudioTrack audioTrack = params.getAudioTrack();
|
||||
|
||||
if (audioTrack == null) {
|
||||
// There was already a call to handleSynthesisDone for
|
||||
// this token.
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.mBytesWritten < params.mAudioBufferSize) {
|
||||
if (DBG) Log.d(TAG, "Stopping audio track to flush audio, state was : " +
|
||||
audioTrack.getPlayState());
|
||||
params.mIsShortUtterance = true;
|
||||
audioTrack.stop();
|
||||
}
|
||||
|
||||
if (DBG) Log.d(TAG, "Waiting for audio track to complete : " +
|
||||
audioTrack.hashCode());
|
||||
blockUntilDone(params);
|
||||
if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]");
|
||||
|
||||
// The last call to AudioTrack.write( ) will return only after
|
||||
// all data from the audioTrack has been sent to the mixer, so
|
||||
// it's safe to release at this point. Make sure release() and the call
|
||||
// that set the audio track to null are performed atomically.
|
||||
synchronized (this) {
|
||||
// Never allow the audioTrack to be observed in a state where
|
||||
// it is released but non null. The only case this might happen
|
||||
// is in the various stopFoo methods that call AudioTrack#stop from
|
||||
// different threads, but they are synchronized on AudioPlayBackHandler#this
|
||||
// too.
|
||||
audioTrack.release();
|
||||
params.setAudioTrack(null);
|
||||
}
|
||||
if (params.isError()) {
|
||||
params.getDispatcher().dispatchOnError();
|
||||
} else {
|
||||
params.getDispatcher().dispatchOnDone();
|
||||
}
|
||||
mLastSynthesisRequest = null;
|
||||
params.mLogger.onWriteData();
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum increment of time to wait for an audiotrack to finish
|
||||
* playing.
|
||||
*/
|
||||
private static final long MIN_SLEEP_TIME_MS = 20;
|
||||
|
||||
/**
|
||||
* The maximum increment of time to sleep while waiting for an audiotrack
|
||||
* to finish playing.
|
||||
*/
|
||||
private static final long MAX_SLEEP_TIME_MS = 2500;
|
||||
|
||||
/**
|
||||
* The maximum amount of time to wait for an audio track to make progress while
|
||||
* it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
|
||||
* could happen in exceptional circumstances like a media_server crash.
|
||||
*/
|
||||
private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
|
||||
|
||||
private static void blockUntilDone(SynthesisMessageParams params) {
|
||||
if (params.mAudioTrack == null || params.mBytesWritten <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.mIsShortUtterance) {
|
||||
// In this case we would have called AudioTrack#stop() to flush
|
||||
// buffers to the mixer. This makes the playback head position
|
||||
// unobservable and notification markers do not work reliably. We
|
||||
// have no option but to wait until we think the track would finish
|
||||
// playing and release it after.
|
||||
//
|
||||
// This isn't as bad as it looks because (a) We won't end up waiting
|
||||
// for much longer than we should because even at 4khz mono, a short
|
||||
// utterance weighs in at about 2 seconds, and (b) such short utterances
|
||||
// are expected to be relatively infrequent and in a stream of utterances
|
||||
// this shows up as a slightly longer pause.
|
||||
blockUntilEstimatedCompletion(params);
|
||||
} else {
|
||||
blockUntilCompletion(params);
|
||||
}
|
||||
}
|
||||
|
||||
private static void blockUntilEstimatedCompletion(SynthesisMessageParams params) {
|
||||
final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
|
||||
final long estimatedTimeMs = (lengthInFrames * 1000 / params.mSampleRateInHz);
|
||||
|
||||
if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
|
||||
|
||||
try {
|
||||
Thread.sleep(estimatedTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
private static void blockUntilCompletion(SynthesisMessageParams params) {
|
||||
final AudioTrack audioTrack = params.mAudioTrack;
|
||||
final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
|
||||
|
||||
int previousPosition = -1;
|
||||
int currentPosition = 0;
|
||||
long blockedTimeMs = 0;
|
||||
|
||||
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
|
||||
audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
|
||||
|
||||
final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
|
||||
audioTrack.getSampleRate();
|
||||
final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
|
||||
|
||||
// Check if the audio track has made progress since the last loop
|
||||
// iteration. We should then add in the amount of time that was
|
||||
// spent sleeping in the last iteration.
|
||||
if (currentPosition == previousPosition) {
|
||||
// This works only because the sleep time that would have been calculated
|
||||
// would be the same in the previous iteration too.
|
||||
blockedTimeMs += sleepTimeMs;
|
||||
// If we've taken too long to make progress, bail.
|
||||
if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
|
||||
Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
|
||||
"for AudioTrack to make progress, Aborting");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
blockedTimeMs = 0;
|
||||
}
|
||||
previousPosition = currentPosition;
|
||||
|
||||
if (DBG) Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
|
||||
" Playback position : " + currentPosition + ", Length in frames : "
|
||||
+ lengthInFrames);
|
||||
try {
|
||||
Thread.sleep(sleepTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final long clip(long value, long min, long max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static AudioTrack createStreamingAudioTrack(SynthesisMessageParams params) {
|
||||
final int channelConfig = getChannelConfig(params.mChannelCount);
|
||||
final int sampleRateInHz = params.mSampleRateInHz;
|
||||
final int audioFormat = params.mAudioFormat;
|
||||
|
||||
int minBufferSizeInBytes
|
||||
= AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
|
||||
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
|
||||
|
||||
AudioTrack audioTrack = new AudioTrack(params.mStreamType, 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;
|
||||
}
|
||||
params.mAudioBufferSize = bufferSizeInBytes;
|
||||
|
||||
setupVolume(audioTrack, params.mVolume, params.mPan);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,23 +16,26 @@
|
||||
package android.speech.tts;
|
||||
|
||||
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
|
||||
import android.util.Log;
|
||||
|
||||
class AudioMessageParams extends MessageParams {
|
||||
class AudioPlaybackQueueItem extends PlaybackQueueItem {
|
||||
private final BlockingMediaPlayer mPlayer;
|
||||
|
||||
AudioMessageParams(UtteranceProgressDispatcher dispatcher,
|
||||
AudioPlaybackQueueItem(UtteranceProgressDispatcher dispatcher,
|
||||
Object callerIdentity, BlockingMediaPlayer player) {
|
||||
super(dispatcher, callerIdentity);
|
||||
mPlayer = player;
|
||||
}
|
||||
|
||||
BlockingMediaPlayer getPlayer() {
|
||||
return mPlayer;
|
||||
@Override
|
||||
public void run() {
|
||||
getDispatcher().dispatchOnStart();
|
||||
// TODO: This can be avoided. Will be fixed later in this CL.
|
||||
mPlayer.startAndWait();
|
||||
getDispatcher().dispatchOnDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_AUDIO;
|
||||
void stop(boolean isError) {
|
||||
mPlayer.stop();
|
||||
}
|
||||
|
||||
}
|
||||
338
core/java/android/speech/tts/BlockingAudioTrack.java
Normal file
338
core/java/android/speech/tts/BlockingAudioTrack.java
Normal file
@@ -0,0 +1,338 @@
|
||||
// Copyright 2011 Google Inc. All Rights Reserved.
|
||||
|
||||
package android.speech.tts;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTrack;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Exposes parts of the {@link AudioTrack} API by delegating calls to an
|
||||
* underlying {@link AudioTrack}. Additionally, provides methods like
|
||||
* {@link #waitAndRelease()} that will block until all audiotrack
|
||||
* data has been flushed to the mixer, and is estimated to have completed
|
||||
* playback.
|
||||
*/
|
||||
class BlockingAudioTrack {
|
||||
private static final String TAG = "TTS.BlockingAudioTrack";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
|
||||
/**
|
||||
* The minimum increment of time to wait for an AudioTrack to finish
|
||||
* playing.
|
||||
*/
|
||||
private static final long MIN_SLEEP_TIME_MS = 20;
|
||||
|
||||
/**
|
||||
* The maximum increment of time to sleep while waiting for an AudioTrack
|
||||
* to finish playing.
|
||||
*/
|
||||
private static final long MAX_SLEEP_TIME_MS = 2500;
|
||||
|
||||
/**
|
||||
* The maximum amount of time to wait for an audio track to make progress while
|
||||
* it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
|
||||
* could happen in exceptional circumstances like a media_server crash.
|
||||
*/
|
||||
private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
|
||||
|
||||
/**
|
||||
* Minimum size of the buffer of the underlying {@link android.media.AudioTrack}
|
||||
* we create.
|
||||
*/
|
||||
private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
|
||||
|
||||
|
||||
private final int mStreamType;
|
||||
private final int mSampleRateInHz;
|
||||
private final int mAudioFormat;
|
||||
private final int mChannelCount;
|
||||
private final float mVolume;
|
||||
private final float mPan;
|
||||
|
||||
private final int mBytesPerFrame;
|
||||
/**
|
||||
* A "short utterance" is one that uses less bytes than the audio
|
||||
* track buffer size (mAudioBufferSize). In this case, we need to call
|
||||
* {@link AudioTrack#stop()} to send pending buffers to the mixer, and slightly
|
||||
* different logic is required to wait for the track to finish.
|
||||
*
|
||||
* Not volatile, accessed only from the audio playback thread.
|
||||
*/
|
||||
private boolean mIsShortUtterance;
|
||||
/**
|
||||
* Will be valid after a call to {@link #init()}.
|
||||
*/
|
||||
private int mAudioBufferSize;
|
||||
private int mBytesWritten = 0;
|
||||
|
||||
private AudioTrack mAudioTrack;
|
||||
private volatile boolean mStopped;
|
||||
// Locks the initialization / uninitialization of the audio track.
|
||||
// This is required because stop() will throw an illegal state exception
|
||||
// if called before init() or after mAudioTrack.release().
|
||||
private final Object mAudioTrackLock = new Object();
|
||||
|
||||
BlockingAudioTrack(int streamType, int sampleRate,
|
||||
int audioFormat, int channelCount,
|
||||
float volume, float pan) {
|
||||
mStreamType = streamType;
|
||||
mSampleRateInHz = sampleRate;
|
||||
mAudioFormat = audioFormat;
|
||||
mChannelCount = channelCount;
|
||||
mVolume = volume;
|
||||
mPan = pan;
|
||||
|
||||
mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount;
|
||||
mIsShortUtterance = false;
|
||||
mAudioBufferSize = 0;
|
||||
mBytesWritten = 0;
|
||||
|
||||
mAudioTrack = null;
|
||||
mStopped = false;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
AudioTrack track = createStreamingAudioTrack();
|
||||
|
||||
synchronized (mAudioTrackLock) {
|
||||
mAudioTrack = track;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
synchronized (mAudioTrackLock) {
|
||||
if (mAudioTrack != null) {
|
||||
mAudioTrack.stop();
|
||||
}
|
||||
}
|
||||
mStopped = true;
|
||||
}
|
||||
|
||||
public int write(byte[] data) {
|
||||
if (mAudioTrack == null || mStopped) {
|
||||
return -1;
|
||||
}
|
||||
final int bytesWritten = writeToAudioTrack(mAudioTrack, data);
|
||||
mBytesWritten += bytesWritten;
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
public void waitAndRelease() {
|
||||
// For "small" audio tracks, we have to stop() them to make them mixable,
|
||||
// else the audio subsystem will wait indefinitely for us to fill the buffer
|
||||
// before rendering the track mixable.
|
||||
//
|
||||
// If mStopped is true, the track would already have been stopped, so not
|
||||
// much point not doing that again.
|
||||
if (mBytesWritten < mAudioBufferSize && !mStopped) {
|
||||
if (DBG) {
|
||||
Log.d(TAG, "Stopping audio track to flush audio, state was : " +
|
||||
mAudioTrack.getPlayState() + ",stopped= " + mStopped);
|
||||
}
|
||||
|
||||
mIsShortUtterance = true;
|
||||
mAudioTrack.stop();
|
||||
}
|
||||
|
||||
// Block until the audio track is done only if we haven't stopped yet.
|
||||
if (!mStopped) {
|
||||
if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + mAudioTrack.hashCode());
|
||||
blockUntilDone(mAudioTrack);
|
||||
}
|
||||
|
||||
// The last call to AudioTrack.write( ) will return only after
|
||||
// all data from the audioTrack has been sent to the mixer, so
|
||||
// it's safe to release at this point.
|
||||
if (DBG) Log.d(TAG, "Releasing audio track [" + mAudioTrack.hashCode() + "]");
|
||||
synchronized (mAudioTrackLock) {
|
||||
mAudioTrack.release();
|
||||
mAudioTrack = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static int getChannelConfig(int channelCount) {
|
||||
if (channelCount == 1) {
|
||||
return AudioFormat.CHANNEL_OUT_MONO;
|
||||
} else if (channelCount == 2){
|
||||
return AudioFormat.CHANNEL_OUT_STEREO;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
long getAudioLengthMs(int numBytes) {
|
||||
final int unconsumedFrames = numBytes / mBytesPerFrame;
|
||||
final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz;
|
||||
|
||||
return estimatedTimeMs;
|
||||
}
|
||||
|
||||
private static int writeToAudioTrack(AudioTrack audioTrack, byte[] bytes) {
|
||||
if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
|
||||
if (DBG) Log.d(TAG, "AudioTrack not playing, restarting : " + audioTrack.hashCode());
|
||||
audioTrack.play();
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while (count < bytes.length) {
|
||||
// Note that we don't take bufferCopy.mOffset into account because
|
||||
// it is guaranteed to be 0.
|
||||
int written = audioTrack.write(bytes, count, bytes.length);
|
||||
if (written <= 0) {
|
||||
break;
|
||||
}
|
||||
count += written;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private AudioTrack createStreamingAudioTrack() {
|
||||
final int channelConfig = getChannelConfig(mChannelCount);
|
||||
|
||||
int minBufferSizeInBytes
|
||||
= AudioTrack.getMinBufferSize(mSampleRateInHz, channelConfig, mAudioFormat);
|
||||
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
|
||||
|
||||
AudioTrack audioTrack = new AudioTrack(mStreamType, mSampleRateInHz, channelConfig,
|
||||
mAudioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
|
||||
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||
Log.w(TAG, "Unable to create audio track.");
|
||||
audioTrack.release();
|
||||
return null;
|
||||
}
|
||||
|
||||
mAudioBufferSize = bufferSizeInBytes;
|
||||
|
||||
setupVolume(audioTrack, mVolume, mPan);
|
||||
return audioTrack;
|
||||
}
|
||||
|
||||
private 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 void blockUntilDone(AudioTrack audioTrack) {
|
||||
if (mBytesWritten <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mIsShortUtterance) {
|
||||
// In this case we would have called AudioTrack#stop() to flush
|
||||
// buffers to the mixer. This makes the playback head position
|
||||
// unobservable and notification markers do not work reliably. We
|
||||
// have no option but to wait until we think the track would finish
|
||||
// playing and release it after.
|
||||
//
|
||||
// This isn't as bad as it looks because (a) We won't end up waiting
|
||||
// for much longer than we should because even at 4khz mono, a short
|
||||
// utterance weighs in at about 2 seconds, and (b) such short utterances
|
||||
// are expected to be relatively infrequent and in a stream of utterances
|
||||
// this shows up as a slightly longer pause.
|
||||
blockUntilEstimatedCompletion();
|
||||
} else {
|
||||
blockUntilCompletion(audioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
private void blockUntilEstimatedCompletion() {
|
||||
final int lengthInFrames = mBytesWritten / mBytesPerFrame;
|
||||
final long estimatedTimeMs = (lengthInFrames * 1000 / mSampleRateInHz);
|
||||
|
||||
if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
|
||||
|
||||
try {
|
||||
Thread.sleep(estimatedTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
private void blockUntilCompletion(AudioTrack audioTrack) {
|
||||
final int lengthInFrames = mBytesWritten / mBytesPerFrame;
|
||||
|
||||
int previousPosition = -1;
|
||||
int currentPosition = 0;
|
||||
long blockedTimeMs = 0;
|
||||
|
||||
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
|
||||
audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && !mStopped) {
|
||||
|
||||
final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
|
||||
audioTrack.getSampleRate();
|
||||
final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
|
||||
|
||||
// Check if the audio track has made progress since the last loop
|
||||
// iteration. We should then add in the amount of time that was
|
||||
// spent sleeping in the last iteration.
|
||||
if (currentPosition == previousPosition) {
|
||||
// This works only because the sleep time that would have been calculated
|
||||
// would be the same in the previous iteration too.
|
||||
blockedTimeMs += sleepTimeMs;
|
||||
// If we've taken too long to make progress, bail.
|
||||
if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
|
||||
Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
|
||||
"for AudioTrack to make progress, Aborting");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
blockedTimeMs = 0;
|
||||
}
|
||||
previousPosition = currentPosition;
|
||||
|
||||
if (DBG) {
|
||||
Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
|
||||
" Playback position : " + currentPosition + ", Length in frames : "
|
||||
+ lengthInFrames);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(sleepTimeMs);
|
||||
} catch (InterruptedException ie) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
|
||||
final float vol = clip(volume, 0.0f, 1.0f);
|
||||
final 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 final long clip(long value, long min, long max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static float clip(float value, float min, float max) {
|
||||
return value > max ? max : (value < min ? min : value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,7 +54,6 @@ class BlockingMediaPlayer {
|
||||
mUri = uri;
|
||||
mStreamType = streamType;
|
||||
mDone = new ConditionVariable();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ package android.speech.tts;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Writes data about a given speech synthesis request to the event logs.
|
||||
@@ -24,7 +25,7 @@ import android.text.TextUtils;
|
||||
* speech rate / pitch and the latency and overall time taken.
|
||||
*
|
||||
* Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()}
|
||||
* might be called from any thread, but on {@link EventLogger#onPlaybackStart()} and
|
||||
* might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and
|
||||
* {@link EventLogger#onComplete()} must be called from a single thread
|
||||
* (usually the audio playback thread}
|
||||
*/
|
||||
@@ -81,10 +82,10 @@ class EventLogger {
|
||||
/**
|
||||
* Notifies the logger that audio playback has started for some section
|
||||
* of the synthesis. This is normally some amount of time after the engine
|
||||
* has synthesized data and varides depending on utterances and
|
||||
* has synthesized data and varies depending on utterances and
|
||||
* other audio currently in the queue.
|
||||
*/
|
||||
public void onPlaybackStart() {
|
||||
public void onAudioDataWritten() {
|
||||
// For now, keep track of only the first chunk of audio
|
||||
// that was played.
|
||||
if (mPlaybackStartTime == -1) {
|
||||
@@ -120,7 +121,7 @@ class EventLogger {
|
||||
}
|
||||
|
||||
long completionTime = SystemClock.elapsedRealtime();
|
||||
// onPlaybackStart() should normally always be called if an
|
||||
// onAudioDataWritten() should normally always be called if an
|
||||
// error does not occur.
|
||||
if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) {
|
||||
EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid,
|
||||
@@ -139,6 +140,7 @@ class EventLogger {
|
||||
final long audioLatency = mPlaybackStartTime - mReceivedTime;
|
||||
final long engineLatency = mEngineStartTime - mRequestProcessingStartTime;
|
||||
final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime;
|
||||
|
||||
EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid,
|
||||
getUtteranceLength(), getLocaleString(),
|
||||
mRequest.getSpeechRate(), mRequest.getPitch(),
|
||||
|
||||
27
core/java/android/speech/tts/PlaybackQueueItem.java
Normal file
27
core/java/android/speech/tts/PlaybackQueueItem.java
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2011 Google Inc. All Rights Reserved.
|
||||
|
||||
package android.speech.tts;
|
||||
|
||||
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
|
||||
|
||||
abstract class PlaybackQueueItem implements Runnable {
|
||||
private final UtteranceProgressDispatcher mDispatcher;
|
||||
private final Object mCallerIdentity;
|
||||
|
||||
PlaybackQueueItem(TextToSpeechService.UtteranceProgressDispatcher dispatcher,
|
||||
Object callerIdentity) {
|
||||
mDispatcher = dispatcher;
|
||||
mCallerIdentity = callerIdentity;
|
||||
}
|
||||
|
||||
Object getCallerIdentity() {
|
||||
return mCallerIdentity;
|
||||
}
|
||||
|
||||
protected UtteranceProgressDispatcher getDispatcher() {
|
||||
return mDispatcher;
|
||||
}
|
||||
|
||||
public abstract void run();
|
||||
abstract void stop(boolean isError);
|
||||
}
|
||||
@@ -47,17 +47,17 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
private final float mPan;
|
||||
|
||||
/**
|
||||
* Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}.
|
||||
* Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}.
|
||||
*/
|
||||
private final Object mStateLock = new Object();
|
||||
|
||||
// Handler associated with a thread that plays back audio requests.
|
||||
private final AudioPlaybackHandler mAudioTrackHandler;
|
||||
// A request "token", which will be non null after start() has been called.
|
||||
private SynthesisMessageParams mToken = null;
|
||||
private SynthesisPlaybackQueueItem mItem = 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.
|
||||
// a non-null value of mItem will provide the same information.
|
||||
private boolean mStopped = false;
|
||||
|
||||
private volatile boolean mDone = false;
|
||||
@@ -89,28 +89,23 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
// Note that mLogger.mError might be true too at this point.
|
||||
mLogger.onStopped();
|
||||
|
||||
SynthesisMessageParams token;
|
||||
SynthesisPlaybackQueueItem item;
|
||||
synchronized (mStateLock) {
|
||||
if (mStopped) {
|
||||
Log.w(TAG, "stop() called twice");
|
||||
return;
|
||||
}
|
||||
|
||||
token = mToken;
|
||||
item = mItem;
|
||||
mStopped = true;
|
||||
}
|
||||
|
||||
if (token != null) {
|
||||
if (item != null) {
|
||||
// This might result in the synthesis thread being woken up, at which
|
||||
// point it will write an additional buffer to the token - but we
|
||||
// point it will write an additional buffer to the item - but we
|
||||
// won't worry about that because the audio playback queue will be cleared
|
||||
// soon after (see SynthHandler#stop(String).
|
||||
token.setIsError(wasError);
|
||||
token.clearBuffers();
|
||||
if (wasError) {
|
||||
// Also clean up the audio track if an error occurs.
|
||||
mAudioTrackHandler.enqueueSynthesisDone(token);
|
||||
}
|
||||
item.stop(wasError);
|
||||
} else {
|
||||
// This happens when stop() or error() were called before start() was.
|
||||
|
||||
@@ -145,7 +140,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
+ "," + channelCount + ")");
|
||||
}
|
||||
|
||||
int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount);
|
||||
int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
|
||||
if (channelConfig == 0) {
|
||||
Log.e(TAG, "Unsupported number of channels :" + channelCount);
|
||||
return TextToSpeech.ERROR;
|
||||
@@ -156,12 +151,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
if (DBG) Log.d(TAG, "stop() called before start(), returning.");
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
SynthesisMessageParams params = new SynthesisMessageParams(
|
||||
SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
|
||||
mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
|
||||
mDispatcher, mCallerIdentity, mLogger);
|
||||
mAudioTrackHandler.enqueueSynthesisStart(params);
|
||||
|
||||
mToken = params;
|
||||
mAudioTrackHandler.enqueue(item);
|
||||
mItem = item;
|
||||
}
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
@@ -179,21 +173,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
+ length + " bytes)");
|
||||
}
|
||||
|
||||
SynthesisMessageParams token = null;
|
||||
SynthesisPlaybackQueueItem item = null;
|
||||
synchronized (mStateLock) {
|
||||
if (mToken == null || mStopped) {
|
||||
if (mItem == null || mStopped) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
token = mToken;
|
||||
item = mItem;
|
||||
}
|
||||
|
||||
// Sigh, another copy.
|
||||
final byte[] bufferCopy = new byte[length];
|
||||
System.arraycopy(buffer, offset, bufferCopy, 0, length);
|
||||
// Might block on mToken.this, if there are too many buffers waiting to
|
||||
|
||||
// Might block on mItem.this, if there are too many buffers waiting to
|
||||
// be consumed.
|
||||
token.addBuffer(bufferCopy);
|
||||
mAudioTrackHandler.enqueueSynthesisDataAvailable(token);
|
||||
try {
|
||||
item.put(bufferCopy);
|
||||
} catch (InterruptedException ie) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
mLogger.onEngineDataReceived();
|
||||
|
||||
@@ -204,7 +202,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
public int done() {
|
||||
if (DBG) Log.d(TAG, "done()");
|
||||
|
||||
SynthesisMessageParams token = null;
|
||||
SynthesisPlaybackQueueItem item = null;
|
||||
synchronized (mStateLock) {
|
||||
if (mDone) {
|
||||
Log.w(TAG, "Duplicate call to done()");
|
||||
@@ -213,14 +211,14 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
|
||||
|
||||
mDone = true;
|
||||
|
||||
if (mToken == null) {
|
||||
if (mItem == null) {
|
||||
return TextToSpeech.ERROR;
|
||||
}
|
||||
|
||||
token = mToken;
|
||||
item = mItem;
|
||||
}
|
||||
|
||||
mAudioTrackHandler.enqueueSynthesisDone(token);
|
||||
item.done();
|
||||
mLogger.onEngineComplete();
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
|
||||
@@ -17,28 +17,29 @@ package android.speech.tts;
|
||||
|
||||
import android.os.ConditionVariable;
|
||||
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
|
||||
import android.util.Log;
|
||||
|
||||
class SilenceMessageParams extends MessageParams {
|
||||
class SilencePlaybackQueueItem extends PlaybackQueueItem {
|
||||
private final ConditionVariable mCondVar = new ConditionVariable();
|
||||
private final long mSilenceDurationMs;
|
||||
|
||||
SilenceMessageParams(UtteranceProgressDispatcher dispatcher,
|
||||
SilencePlaybackQueueItem(UtteranceProgressDispatcher dispatcher,
|
||||
Object callerIdentity, long silenceDurationMs) {
|
||||
super(dispatcher, callerIdentity);
|
||||
mSilenceDurationMs = silenceDurationMs;
|
||||
}
|
||||
|
||||
long getSilenceDurationMs() {
|
||||
return mSilenceDurationMs;
|
||||
@Override
|
||||
public void run() {
|
||||
getDispatcher().dispatchOnStart();
|
||||
if (mSilenceDurationMs > 0) {
|
||||
mCondVar.block(mSilenceDurationMs);
|
||||
}
|
||||
getDispatcher().dispatchOnDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_SILENCE;
|
||||
void stop(boolean isError) {
|
||||
mCondVar.open();
|
||||
}
|
||||
|
||||
ConditionVariable getConditionVariable() {
|
||||
return mCondVar;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Params required to play back a synthesis request.
|
||||
*/
|
||||
final class SynthesisMessageParams extends MessageParams {
|
||||
private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
|
||||
|
||||
final int mStreamType;
|
||||
final int mSampleRateInHz;
|
||||
final int mAudioFormat;
|
||||
final int mChannelCount;
|
||||
final float mVolume;
|
||||
final float mPan;
|
||||
final EventLogger mLogger;
|
||||
|
||||
final int mBytesPerFrame;
|
||||
|
||||
volatile AudioTrack mAudioTrack;
|
||||
// Written by the synthesis thread, but read on the audio playback
|
||||
// thread.
|
||||
volatile int mBytesWritten;
|
||||
// A "short utterance" is one that uses less bytes than the audio
|
||||
// track buffer size (mAudioBufferSize). In this case, we need to call
|
||||
// AudioTrack#stop() to send pending buffers to the mixer, and slightly
|
||||
// different logic is required to wait for the track to finish.
|
||||
//
|
||||
// Not volatile, accessed only from the audio playback thread.
|
||||
boolean mIsShortUtterance;
|
||||
int mAudioBufferSize;
|
||||
// Always synchronized on "this".
|
||||
int mUnconsumedBytes;
|
||||
volatile boolean mIsError;
|
||||
|
||||
private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
|
||||
|
||||
SynthesisMessageParams(int streamType, int sampleRate,
|
||||
int audioFormat, int channelCount,
|
||||
float volume, float pan, UtteranceProgressDispatcher dispatcher,
|
||||
Object callerIdentity, EventLogger logger) {
|
||||
super(dispatcher, callerIdentity);
|
||||
|
||||
mStreamType = streamType;
|
||||
mSampleRateInHz = sampleRate;
|
||||
mAudioFormat = audioFormat;
|
||||
mChannelCount = channelCount;
|
||||
mVolume = volume;
|
||||
mPan = pan;
|
||||
mLogger = logger;
|
||||
|
||||
mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount;
|
||||
|
||||
// initially null.
|
||||
mAudioTrack = null;
|
||||
mBytesWritten = 0;
|
||||
mAudioBufferSize = 0;
|
||||
mIsError = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return TYPE_SYNTHESIS;
|
||||
}
|
||||
|
||||
synchronized void addBuffer(byte[] buffer) {
|
||||
long unconsumedAudioMs = 0;
|
||||
|
||||
while ((unconsumedAudioMs = getUnconsumedAudioLengthMs()) > MAX_UNCONSUMED_AUDIO_MS) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException ie) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mDataBufferList.add(new ListEntry(buffer));
|
||||
mUnconsumedBytes += buffer.length;
|
||||
}
|
||||
|
||||
synchronized void clearBuffers() {
|
||||
mDataBufferList.clear();
|
||||
mUnconsumedBytes = 0;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized ListEntry getNextBuffer() {
|
||||
ListEntry entry = mDataBufferList.poll();
|
||||
if (entry != null) {
|
||||
mUnconsumedBytes -= entry.mBytes.length;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void setAudioTrack(AudioTrack audioTrack) {
|
||||
mAudioTrack = audioTrack;
|
||||
}
|
||||
|
||||
AudioTrack getAudioTrack() {
|
||||
return mAudioTrack;
|
||||
}
|
||||
|
||||
void setIsError(boolean isError) {
|
||||
mIsError = isError;
|
||||
}
|
||||
|
||||
boolean isError() {
|
||||
return mIsError;
|
||||
}
|
||||
|
||||
// Must be called synchronized on this.
|
||||
private long getUnconsumedAudioLengthMs() {
|
||||
final int unconsumedFrames = mUnconsumedBytes / mBytesPerFrame;
|
||||
final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz;
|
||||
|
||||
return estimatedTimeMs;
|
||||
}
|
||||
|
||||
private static int getBytesPerFrame(int audioFormat) {
|
||||
if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
|
||||
return 1;
|
||||
} else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static final class ListEntry {
|
||||
final byte[] mBytes;
|
||||
|
||||
ListEntry(byte[] bytes) {
|
||||
mBytes = bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
245
core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
Normal file
245
core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
Normal file
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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.UtteranceProgressDispatcher;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Manages the playback of a list of byte arrays representing audio data
|
||||
* that are queued by the engine to an audio track.
|
||||
*/
|
||||
final class SynthesisPlaybackQueueItem extends PlaybackQueueItem {
|
||||
private static final String TAG = "TTS.SynthQueueItem";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
/**
|
||||
* Maximum length of audio we leave unconsumed by the audio track.
|
||||
* Calls to {@link #put(byte[])} will block until we have less than
|
||||
* this amount of audio left to play back.
|
||||
*/
|
||||
private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
|
||||
|
||||
/**
|
||||
* Guards accesses to mDataBufferList and mUnconsumedBytes.
|
||||
*/
|
||||
private final Lock mListLock = new ReentrantLock();
|
||||
private final Condition mReadReady = mListLock.newCondition();
|
||||
private final Condition mNotFull = mListLock.newCondition();
|
||||
|
||||
// Guarded by mListLock.
|
||||
private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
|
||||
// Guarded by mListLock.
|
||||
private int mUnconsumedBytes;
|
||||
|
||||
/*
|
||||
* While mStopped and mIsError can be written from any thread, mDone is written
|
||||
* only from the synthesis thread. All three variables are read from the
|
||||
* audio playback thread.
|
||||
*/
|
||||
private volatile boolean mStopped;
|
||||
private volatile boolean mDone;
|
||||
private volatile boolean mIsError;
|
||||
|
||||
private final BlockingAudioTrack mAudioTrack;
|
||||
private final EventLogger mLogger;
|
||||
|
||||
|
||||
SynthesisPlaybackQueueItem(int streamType, int sampleRate,
|
||||
int audioFormat, int channelCount,
|
||||
float volume, float pan, UtteranceProgressDispatcher dispatcher,
|
||||
Object callerIdentity, EventLogger logger) {
|
||||
super(dispatcher, callerIdentity);
|
||||
|
||||
mUnconsumedBytes = 0;
|
||||
|
||||
mStopped = false;
|
||||
mDone = false;
|
||||
mIsError = false;
|
||||
|
||||
mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat,
|
||||
channelCount, volume, pan);
|
||||
mLogger = logger;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final UtteranceProgressDispatcher dispatcher = getDispatcher();
|
||||
dispatcher.dispatchOnStart();
|
||||
|
||||
|
||||
mAudioTrack.init();
|
||||
|
||||
try {
|
||||
byte[] buffer = null;
|
||||
|
||||
// take() will block until:
|
||||
//
|
||||
// (a) there is a buffer available to tread. In which case
|
||||
// a non null value is returned.
|
||||
// OR (b) stop() is called in which case it will return null.
|
||||
// OR (c) done() is called in which case it will return null.
|
||||
while ((buffer = take()) != null) {
|
||||
mAudioTrack.write(buffer);
|
||||
mLogger.onAudioDataWritten();
|
||||
}
|
||||
|
||||
} catch (InterruptedException ie) {
|
||||
if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
|
||||
}
|
||||
|
||||
mAudioTrack.waitAndRelease();
|
||||
|
||||
if (mIsError) {
|
||||
dispatcher.dispatchOnError();
|
||||
} else {
|
||||
dispatcher.dispatchOnDone();
|
||||
}
|
||||
|
||||
mLogger.onWriteData();
|
||||
}
|
||||
|
||||
@Override
|
||||
void stop(boolean isError) {
|
||||
try {
|
||||
mListLock.lock();
|
||||
|
||||
// Update our internal state.
|
||||
mStopped = true;
|
||||
mIsError = isError;
|
||||
|
||||
// Wake up the audio playback thread if it was waiting on take().
|
||||
// take() will return null since mStopped was true, and will then
|
||||
// break out of the data write loop.
|
||||
mReadReady.signal();
|
||||
|
||||
// Wake up the synthesis thread if it was waiting on put(). Its
|
||||
// buffers will no longer be copied since mStopped is true. The
|
||||
// PlaybackSynthesisCallback that this synthesis corresponds to
|
||||
// would also have been stopped, and so all calls to
|
||||
// Callback.onDataAvailable( ) will return errors too.
|
||||
mNotFull.signal();
|
||||
} finally {
|
||||
mListLock.unlock();
|
||||
}
|
||||
|
||||
// Stop the underlying audio track. This will stop sending
|
||||
// data to the mixer and discard any pending buffers that the
|
||||
// track holds.
|
||||
mAudioTrack.stop();
|
||||
}
|
||||
|
||||
void done() {
|
||||
try {
|
||||
mListLock.lock();
|
||||
|
||||
// Update state.
|
||||
mDone = true;
|
||||
|
||||
// Unblocks the audio playback thread if it was waiting on take()
|
||||
// after having consumed all available buffers. It will then return
|
||||
// null and leave the write loop.
|
||||
mReadReady.signal();
|
||||
|
||||
// Just so that engines that try to queue buffers after
|
||||
// calling done() don't block the synthesis thread forever. Ideally
|
||||
// this should be called from the same thread as put() is, and hence
|
||||
// this call should be pointless.
|
||||
mNotFull.signal();
|
||||
} finally {
|
||||
mListLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void put(byte[] buffer) throws InterruptedException {
|
||||
try {
|
||||
mListLock.lock();
|
||||
long unconsumedAudioMs = 0;
|
||||
|
||||
while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
|
||||
MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
|
||||
mNotFull.await();
|
||||
}
|
||||
|
||||
// Don't bother queueing the buffer if we've stopped. The playback thread
|
||||
// would have woken up when stop() is called (if it was blocked) and will
|
||||
// proceed to leave the write loop since take() will return null when
|
||||
// stopped.
|
||||
if (mStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDataBufferList.add(new ListEntry(buffer));
|
||||
mUnconsumedBytes += buffer.length;
|
||||
mReadReady.signal();
|
||||
} finally {
|
||||
mListLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] take() throws InterruptedException {
|
||||
try {
|
||||
mListLock.lock();
|
||||
|
||||
// Block if there are no available buffers, and stop() has not
|
||||
// been called and done() has not been called.
|
||||
while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
|
||||
mReadReady.await();
|
||||
}
|
||||
|
||||
// If stopped, return null so that we can exit the playback loop
|
||||
// as soon as possible.
|
||||
if (mStopped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the first entry from the queue.
|
||||
ListEntry entry = mDataBufferList.poll();
|
||||
|
||||
// This is the normal playback loop exit case, when done() was
|
||||
// called. (mDone will be true at this point).
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mUnconsumedBytes -= entry.mBytes.length;
|
||||
// Unblock the waiting writer. We use signal() and not signalAll()
|
||||
// because there will only be one thread waiting on this (the
|
||||
// Synthesis thread).
|
||||
mNotFull.signal();
|
||||
|
||||
return entry.mBytes;
|
||||
} finally {
|
||||
mListLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static final class ListEntry {
|
||||
final byte[] mBytes;
|
||||
|
||||
ListEntry(byte[] bytes) {
|
||||
mBytes = bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
}
|
||||
|
||||
// Remove any enqueued audio too.
|
||||
mAudioPlaybackHandler.removePlaybackItems(callerIdentity);
|
||||
mAudioPlaybackHandler.stopForApp(callerIdentity);
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
@@ -378,7 +378,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
// Remove all other items from the queue.
|
||||
removeCallbacksAndMessages(null);
|
||||
// Remove all pending playback as well.
|
||||
mAudioPlaybackHandler.removeAllItems();
|
||||
mAudioPlaybackHandler.stop();
|
||||
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
@@ -694,9 +694,7 @@ public abstract class TextToSpeechService extends Service {
|
||||
}
|
||||
|
||||
private class AudioSpeechItem extends SpeechItem {
|
||||
|
||||
private final BlockingMediaPlayer mPlayer;
|
||||
private AudioMessageParams mToken;
|
||||
|
||||
public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid,
|
||||
Bundle params, Uri uri) {
|
||||
@@ -711,8 +709,8 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
@Override
|
||||
protected int playImpl() {
|
||||
mToken = new AudioMessageParams(this, getCallerIdentity(), mPlayer);
|
||||
mAudioPlaybackHandler.enqueueAudio(mToken);
|
||||
mAudioPlaybackHandler.enqueue(new AudioPlaybackQueueItem(
|
||||
this, getCallerIdentity(), mPlayer));
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@@ -724,7 +722,6 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
private class SilenceSpeechItem extends SpeechItem {
|
||||
private final long mDuration;
|
||||
private SilenceMessageParams mToken;
|
||||
|
||||
public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
|
||||
Bundle params, long duration) {
|
||||
@@ -739,14 +736,14 @@ public abstract class TextToSpeechService extends Service {
|
||||
|
||||
@Override
|
||||
protected int playImpl() {
|
||||
mToken = new SilenceMessageParams(this, getCallerIdentity(), mDuration);
|
||||
mAudioPlaybackHandler.enqueueSilence(mToken);
|
||||
mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
|
||||
this, getCallerIdentity(), mDuration));
|
||||
return TextToSpeech.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopImpl() {
|
||||
// Do nothing.
|
||||
// Do nothing, handled by AudioPlaybackHandler#stopForApp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user