summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/speech/tts/AudioPlaybackHandler.java589
-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.java338
-rw-r--r--core/java/android/speech/tts/BlockingMediaPlayer.java1
-rw-r--r--core/java/android/speech/tts/EventLogger.java10
-rw-r--r--core/java/android/speech/tts/PlaybackQueueItem.java27
-rw-r--r--core/java/android/speech/tts/PlaybackSynthesisCallback.java54
-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.java159
-rw-r--r--core/java/android/speech/tts/SynthesisPlaybackQueueItem.java245
-rw-r--r--core/java/android/speech/tts/TextToSpeechService.java17
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
}
}