diff options
| -rw-r--r-- | core/java/android/speech/tts/AudioPlaybackHandler.java | 589 | ||||
| -rw-r--r-- | core/java/android/speech/tts/AudioPlaybackQueueItem.java (renamed from core/java/android/speech/tts/AudioMessageParams.java) | 19 | ||||
| -rw-r--r-- | core/java/android/speech/tts/BlockingAudioTrack.java | 338 | ||||
| -rw-r--r-- | core/java/android/speech/tts/BlockingMediaPlayer.java | 1 | ||||
| -rw-r--r-- | core/java/android/speech/tts/EventLogger.java | 10 | ||||
| -rw-r--r-- | core/java/android/speech/tts/PlaybackQueueItem.java | 27 | ||||
| -rw-r--r-- | core/java/android/speech/tts/PlaybackSynthesisCallback.java | 54 | ||||
| -rw-r--r-- | core/java/android/speech/tts/SilencePlaybackQueueItem.java (renamed from core/java/android/speech/tts/SilenceMessageParams.java) | 23 | ||||
| -rw-r--r-- | core/java/android/speech/tts/SynthesisMessageParams.java | 159 | ||||
| -rw-r--r-- | core/java/android/speech/tts/SynthesisPlaybackQueueItem.java | 245 | ||||
| -rw-r--r-- | core/java/android/speech/tts/TextToSpeechService.java | 17 |
11 files changed, 720 insertions, 762 deletions
diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index 518c937f250f..d63f605df764 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -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). - // ----------------------------------------------------- - - synchronized public void removePlaybackItems(Object callerIdentity) { - if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callerIdentity); - removeMessages(callerIdentity); + public void stopForApp(Object callerIdentity) { + if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity); + removeWorkItemsFor(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,489 +77,64 @@ 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)); - } - - synchronized void enqueueSynthesisStart(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token); - mQueue.add(new ListEntry(SYNTHESIS_START, token)); - } - - synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token); - mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); - } - - 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. - */ - 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); - } - } + stop(mCurrentWorkItem); + mHandlerThread.interrupt(); } /* * Atomically clear the queue of all messages. */ - synchronized private void removeAllMessages() { + 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(); + private void removeWorkItemsFor(Object callerIdentity) { + Iterator<PlaybackQueueItem> 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) { + final PlaybackQueueItem item = it.next(); + if (item.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. + * The MessageLoop is a handler like implementation that + * processes messages from a priority queue. */ - 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(); - } - + private final class MessageLoop implements Runnable { @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; + public void run() { + while (true) { + PlaybackQueueItem item = null; + try { + item = mQueue.take(); + } catch (InterruptedException ie) { + if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)"); + return; } - } 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; + // If stop() or stopForApp() are called between mQueue.take() + // returning and mCurrentWorkItem being set, the current work item + // will be run anyway. - 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"); + mCurrentWorkItem = item; + item.run(); + mCurrentWorkItem = null; + } } } - private static float clip(float value, float min, float max) { - return value > max ? max : (value < min ? min : value); - } - } diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index a2248a2633a1..668b45983dc8 100644 --- a/core/java/android/speech/tts/AudioMessageParams.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -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(); } - } diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java new file mode 100644 index 000000000000..fcadad7dfbe8 --- /dev/null +++ b/core/java/android/speech/tts/BlockingAudioTrack.java @@ -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); + } + +} diff --git a/core/java/android/speech/tts/BlockingMediaPlayer.java b/core/java/android/speech/tts/BlockingMediaPlayer.java index 3cf60dd8f992..1ccc6e4fb9a7 100644 --- a/core/java/android/speech/tts/BlockingMediaPlayer.java +++ b/core/java/android/speech/tts/BlockingMediaPlayer.java @@ -54,7 +54,6 @@ class BlockingMediaPlayer { mUri = uri; mStreamType = streamType; mDone = new ConditionVariable(); - } /** diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java index 3c93e18fef3b..82ed4dd56ae4 100644 --- a/core/java/android/speech/tts/EventLogger.java +++ b/core/java/android/speech/tts/EventLogger.java @@ -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(), diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java new file mode 100644 index 000000000000..d0957ff90a9b --- /dev/null +++ b/core/java/android/speech/tts/PlaybackQueueItem.java @@ -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); +} diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index 8634506d067c..c99f2011f303 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -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; diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java index 2431808e7e4f..a5e47aea8570 100644 --- a/core/java/android/speech/tts/SilenceMessageParams.java +++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java @@ -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 - int getType() { - return TYPE_SILENCE; + public void run() { + getDispatcher().dispatchOnStart(); + if (mSilenceDurationMs > 0) { + mCondVar.block(mSilenceDurationMs); + } + getDispatcher().dispatchOnDone(); } - ConditionVariable getConditionVariable() { - return mCondVar; + @Override + void stop(boolean isError) { + mCondVar.open(); } - } diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java deleted file mode 100644 index ef73d305d3e5..000000000000 --- a/core/java/android/speech/tts/SynthesisMessageParams.java +++ /dev/null @@ -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; - } - } -} - diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java new file mode 100644 index 000000000000..d299d70421f5 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -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; + } + } +} + diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 2f62d39b2df7..ba8485a9334f 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -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 } } |