diff options
author | Narayan Kamath <narayan@google.com> | 2011-06-09 11:35:13 +0100 |
---|---|---|
committer | Narayan Kamath <narayan@google.com> | 2011-06-09 16:42:16 +0100 |
commit | 4924fe38675f0bf69bb0c16fc059ffa1606807ce (patch) | |
tree | baeb74c7276d9ff6ae6f599841b4780ee41233b0 /core/java/android/speech | |
parent | 6276814a67e633c342acc7bf3d982b091bfe9f08 (diff) | |
download | frameworks_base-4924fe38675f0bf69bb0c16fc059ffa1606807ce.zip frameworks_base-4924fe38675f0bf69bb0c16fc059ffa1606807ce.tar.gz frameworks_base-4924fe38675f0bf69bb0c16fc059ffa1606807ce.tar.bz2 |
Implement QUEUE_FLUSH and QUEUE_DESTROY addition modes.
Note that this change is slightly more flexible, it defines
a priority for each playback request. We needn't really throw
away synthesis requests from other apps. We could just play
them after higher priority ones (for e.g from talkback).
Change-Id: I6c7dd9abe4871e87da08d9aaa4c55225a69078e5
Diffstat (limited to 'core/java/android/speech')
7 files changed, 270 insertions, 81 deletions
diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java index db4d622..68d8738 100644 --- a/core/java/android/speech/tts/AudioMessageParams.java +++ b/core/java/android/speech/tts/AudioMessageParams.java @@ -20,8 +20,9 @@ import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; class AudioMessageParams extends MessageParams { private final BlockingMediaPlayer mPlayer; - AudioMessageParams(UtteranceCompletedDispatcher dispatcher, BlockingMediaPlayer player) { - super(dispatcher); + AudioMessageParams(UtteranceCompletedDispatcher dispatcher, + String callingApp, BlockingMediaPlayer player) { + super(dispatcher, callingApp); mPlayer = player; } diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index 924bbbc..c068b75 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -17,13 +17,13 @@ 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 { +import java.util.Iterator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; + +class AudioPlaybackHandler { private static final String TAG = "TTS.AudioPlaybackHandler"; private static final boolean DBG = false; @@ -37,33 +37,31 @@ class AudioPlaybackHandler extends Handler { private static final int PLAY_AUDIO = 5; private static final int PLAY_SILENCE = 6; - // Accessed by multiple threads, synchronized by "this". - private MessageParams mCurrentParams; + 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 Thread mHandlerThread; + + private final Object mStateLock = new Object(); + + // Accessed by multiple threads, synchronized by "mStateLock". + private MessageParams mCurrentParams = null; // Used only for book keeping and error detection. - private SynthesisMessageParams mLastSynthesisRequest; + private volatile SynthesisMessageParams mLastSynthesisRequest = null; + // Used to order incoming messages in our priority queue. + private final AtomicLong mSequenceIdCtr = new AtomicLong(0); - 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); - } + AudioPlaybackHandler() { + mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread"); + } - mCurrentParams = null; + public void start() { + mHandlerThread.start(); } /** @@ -72,63 +70,232 @@ class AudioPlaybackHandler extends Handler { * that is not guaranteed. */ synchronized public void stop(MessageParams token) { - removeCallbacksAndMessages(token); + if (token == null) { + return; + } + + removeMessages(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(); + mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY)); + } else { + MessageParams current = getCurrentParams(); + + if (current != null) { + if (token.getType() == MessageParams.TYPE_AUDIO) { + ((AudioMessageParams) current).getPlayer().stop(); + } else if (token.getType() == MessageParams.TYPE_SILENCE) { + ((SilenceMessageParams) current).getConditionVariable().open(); + } } } } + synchronized public void removePlaybackItems(String callingApp) { + removeMessages(callingApp); + stop(getCurrentParams()); + } + + synchronized public void removeAllItems() { + removeAllMessages(); + stop(getCurrentParams()); + } + /** * Shut down the audio playback thread. */ synchronized public void quit() { - if (mCurrentParams != null) { - stop(mCurrentParams); - } - getLooper().quit(); + stop(getCurrentParams()); + mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY)); } void enqueueSynthesisStart(SynthesisMessageParams token) { - sendMessage(obtainMessage(SYNTHESIS_START, token)); + mQueue.add(new ListEntry(SYNTHESIS_START, token)); } void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { - sendMessage(obtainMessage(SYNTHESIS_DATA_AVAILABLE, token)); + mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); } void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) { - sendMessage(obtainMessage(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token)); + mQueue.add(new ListEntry(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token)); } void enqueueSynthesisDone(SynthesisMessageParams token) { - sendMessage(obtainMessage(SYNTHESIS_DONE, token)); + mQueue.add(new ListEntry(SYNTHESIS_DONE, token)); } void enqueueAudio(AudioMessageParams token) { - sendMessage(obtainMessage(PLAY_AUDIO, token)); + mQueue.add(new ListEntry(PLAY_AUDIO, token)); } void enqueueSilence(SilenceMessageParams token) { - sendMessage(obtainMessage(PLAY_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. + */ + private final class MessageLoop implements Runnable { + @Override + public void run() { + while (true) { + ListEntry entry = null; + try { + entry = mQueue.take(); + } catch (InterruptedException ie) { + return; + } + + if (entry.mWhat == SHUTDOWN) { + if (DBG) Log.d(TAG, "MessageLoop : Shutting down"); + return; + } + + if (DBG) { + Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat + + " ,seqId : " + entry.mSequenceId); + } + + setCurrentParams(entry.mMessage); + handleMessage(entry); + setCurrentParams(null); + } + } + } + + /* + * Remove all messages from the queue that contain the supplied token. + * Note that the Iterator is thread safe, and other methods can safely + * continue adding to the queue at this point. + */ + synchronized private void removeMessages(MessageParams token) { + if (token == null) { + return; + } + + Iterator<ListEntry> it = mQueue.iterator(); + + while (it.hasNext()) { + final ListEntry current = it.next(); + if (current.mMessage == token) { + it.remove(); + } + } + } + + /* + * 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(String callingApp) { + 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 && + callingApp.equals(current.mMessage.getCallingApp())) { + 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) { + synchronized (mStateLock) { + mCurrentParams = p; + } + } + + private MessageParams getCurrentParams() { + synchronized (mStateLock) { + 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 == SYNTHESIS_COMPLETE_DATA_AVAILABLE) { + handleSynthesisCompleteDataAvailable(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(Message msg) { + private void handleSilence(MessageParams msg) { if (DBG) Log.d(TAG, "handleSilence()"); - SilenceMessageParams params = (SilenceMessageParams) msg.obj; + SilenceMessageParams params = (SilenceMessageParams) msg; if (params.getSilenceDurationMs() > 0) { params.getConditionVariable().block(params.getSilenceDurationMs()); } @@ -137,9 +304,9 @@ class AudioPlaybackHandler extends Handler { } // Plays back audio from a given URI. No TTS engine involvement here. - private void handleAudio(Message msg) { + private void handleAudio(MessageParams msg) { if (DBG) Log.d(TAG, "handleAudio()"); - AudioMessageParams params = (AudioMessageParams) msg.obj; + AudioMessageParams params = (AudioMessageParams) msg; // Note that the BlockingMediaPlayer spawns a separate thread. // // TODO: This can be avoided. @@ -157,9 +324,9 @@ class AudioPlaybackHandler extends Handler { // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone // OR // handleSynthesisCompleteDataAvailable. - private void handleSynthesisStart(Message msg) { + private void handleSynthesisStart(MessageParams msg) { if (DBG) Log.d(TAG, "handleSynthesisStart()"); - final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj; + 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 @@ -177,12 +344,14 @@ class AudioPlaybackHandler extends Handler { param.mStreamType, param.mSampleRateInHz, param.mAudioFormat, param.mChannelCount, param.mVolume, param.mPan); + if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]"); + param.setAudioTrack(audioTrack); } // More data available to be flushed to the audio track. - private void handleSynthesisDataAvailable(Message msg) { - final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj; + private void handleSynthesisDataAvailable(MessageParams msg) { + final SynthesisMessageParams param = (SynthesisMessageParams) msg; if (param.getAudioTrack() == null) { Log.w(TAG, "Error : null audio track in handleDataAvailable."); return; @@ -194,7 +363,7 @@ class AudioPlaybackHandler extends Handler { } final AudioTrack audioTrack = param.getAudioTrack(); - final ListEntry bufferCopy = param.getNextBuffer(); + final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer(); if (bufferCopy == null) { Log.e(TAG, "No buffers available to play."); @@ -218,8 +387,8 @@ class AudioPlaybackHandler extends Handler { } } - private void handleSynthesisDone(Message msg) { - final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj; + private void handleSynthesisDone(MessageParams msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg; handleSynthesisDone(params); } @@ -233,6 +402,7 @@ class AudioPlaybackHandler extends Handler { if (audioTrack != null) { audioTrack.flush(); audioTrack.stop(); + if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]"); audioTrack.release(); } } finally { @@ -242,8 +412,8 @@ class AudioPlaybackHandler extends Handler { } } - private void handleSynthesisCompleteDataAvailable(Message msg) { - final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj; + private void handleSynthesisCompleteDataAvailable(MessageParams msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg; if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")"); // Channel config and bytes per frame are checked before @@ -251,7 +421,7 @@ class AudioPlaybackHandler extends Handler { int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount); int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat); - ListEntry entry = params.getNextBuffer(); + SynthesisMessageParams.ListEntry entry = params.getNextBuffer(); if (entry == null) { Log.w(TAG, "completeDataAvailable : No buffers available to play."); diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java index 2d96df4..4c1b6d2 100644 --- a/core/java/android/speech/tts/MessageParams.java +++ b/core/java/android/speech/tts/MessageParams.java @@ -18,19 +18,25 @@ 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) { + private final UtteranceCompletedDispatcher mDispatcher; + private final String mCallingApp; + + MessageParams(UtteranceCompletedDispatcher dispatcher, String callingApp) { mDispatcher = dispatcher; + mCallingApp = callingApp; } UtteranceCompletedDispatcher getDispatcher() { return mDispatcher; } + String getCallingApp() { + return mCallingApp; + } + abstract int getType(); } diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index 1888dda..bdaa1b8 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -64,15 +64,17 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private volatile boolean mDone = false; private final UtteranceCompletedDispatcher mDispatcher; + private final String mCallingApp; PlaybackSynthesisCallback(int streamType, float volume, float pan, - AudioPlaybackHandler audioTrackHandler, - UtteranceCompletedDispatcher dispatcher) { + AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher, + String callingApp) { mStreamType = streamType; mVolume = volume; mPan = pan; mAudioTrackHandler = audioTrackHandler; mDispatcher = dispatcher; + mCallingApp = callingApp; } @Override @@ -122,7 +124,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } SynthesisMessageParams params = new SynthesisMessageParams( mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher); + mDispatcher, mCallingApp); mAudioTrackHandler.enqueueSynthesisStart(params); mToken = params; @@ -206,7 +208,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } SynthesisMessageParams params = new SynthesisMessageParams( mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher); + mDispatcher, mCallingApp); params.addBuffer(buffer, offset, length); mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params); diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilenceMessageParams.java index eee8b68..7a4ff1c 100644 --- a/core/java/android/speech/tts/SilenceMessageParams.java +++ b/core/java/android/speech/tts/SilenceMessageParams.java @@ -22,8 +22,9 @@ class SilenceMessageParams extends MessageParams { private final ConditionVariable mCondVar = new ConditionVariable(); private final long mSilenceDurationMs; - SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, long silenceDurationMs) { - super(dispatcher); + SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, + String callingApp, long silenceDurationMs) { + super(dispatcher, callingApp); mSilenceDurationMs = silenceDurationMs; } diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java index aabaa5a..51f3d2e 100644 --- a/core/java/android/speech/tts/SynthesisMessageParams.java +++ b/core/java/android/speech/tts/SynthesisMessageParams.java @@ -37,8 +37,9 @@ final class SynthesisMessageParams extends MessageParams { SynthesisMessageParams(int streamType, int sampleRate, int audioFormat, int channelCount, - float volume, float pan, UtteranceCompletedDispatcher dispatcher) { - super(dispatcher); + float volume, float pan, UtteranceCompletedDispatcher dispatcher, + String callingApp) { + super(dispatcher, callingApp); mStreamType = streamType; mSampleRateInHz = sampleRate; diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 55cfea3..4898f0e 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -94,9 +94,8 @@ public abstract class TextToSpeechService extends Service { synthThread.start(); mSynthHandler = new SynthHandler(synthThread.getLooper()); - HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread"); - audioTrackThread.start(); - mAudioPlaybackHandler = new AudioPlaybackHandler(audioTrackThread.getLooper()); + mAudioPlaybackHandler = new AudioPlaybackHandler(); + mAudioPlaybackHandler.start(); mCallbacks = new CallbackMap(); @@ -299,11 +298,16 @@ public abstract class TextToSpeechService extends Service { if (!speechItem.isValid()) { return TextToSpeech.ERROR; } - // TODO: The old code also supported the undocumented queueMode == 2, - // which clears out all pending items from the calling app, as well as all - // non-file items from other apps. + if (queueMode == TextToSpeech.QUEUE_FLUSH) { stop(speechItem.getCallingApp()); + } else if (queueMode == 2) { + // Stop the current speech item. + stop(speechItem.getCallingApp()); + // Remove all other items from the queue. + removeCallbacksAndMessages(null); + // Remove all pending playback as well. + mAudioPlaybackHandler.removeAllItems(); } Runnable runnable = new Runnable() { @Override @@ -334,12 +338,16 @@ public abstract class TextToSpeechService extends Service { if (TextUtils.isEmpty(callingApp)) { return TextToSpeech.ERROR; } + removeCallbacksAndMessages(callingApp); SpeechItem current = setCurrentSpeechItem(null); if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { current.stop(); } + // Remove any enqueued audio too. + mAudioPlaybackHandler.removePlaybackItems(callingApp); + return TextToSpeech.SUCCESS; } } @@ -490,7 +498,7 @@ public abstract class TextToSpeechService extends Service { protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this); + mAudioPlaybackHandler, this, getCallingApp()); } private void setRequestParams(SynthesisRequest request) { @@ -617,7 +625,7 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new AudioMessageParams(this, mPlayer); + mToken = new AudioMessageParams(this, getCallingApp(), mPlayer); mAudioPlaybackHandler.enqueueAudio(mToken); return TextToSpeech.SUCCESS; } @@ -646,7 +654,7 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new SilenceMessageParams(this, mDuration); + mToken = new SilenceMessageParams(this, getCallingApp(), mDuration); mAudioPlaybackHandler.enqueueSilence(mToken); return TextToSpeech.SUCCESS; } |