Simplify the implementation of AudioPlaybackHandler.

Now uses a queue of runnables, one per synthesis. Also
introduce an abstraction that wraps AudioTrack that implements
the blocking semantics that we desire.

bug:5680699

Change-Id: I34a1248ff05766a7d9b3002055fb5b24aa9f230b
This commit is contained in:
Narayan Kamath
2011-11-30 14:51:00 +00:00
parent 51340e0fb7
commit 67ae6bc83c
11 changed files with 723 additions and 765 deletions

View File

@@ -15,44 +15,20 @@
*/
package android.speech.tts;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.text.TextUtils;
import android.util.Log;
import java.util.Iterator;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.LinkedBlockingQueue;
class AudioPlaybackHandler {
private static final String TAG = "TTS.AudioPlaybackHandler";
private static final boolean DBG_THREADING = false;
private static final boolean DBG = false;
private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
private static final int SYNTHESIS_START = 1;
private static final int SYNTHESIS_DATA_AVAILABLE = 2;
private static final int SYNTHESIS_DONE = 3;
private static final int PLAY_AUDIO = 5;
private static final int PLAY_SILENCE = 6;
private static final int SHUTDOWN = -1;
private static final int DEFAULT_PRIORITY = 1;
private static final int HIGH_PRIORITY = 0;
private final PriorityBlockingQueue<ListEntry> mQueue =
new PriorityBlockingQueue<ListEntry>();
private final LinkedBlockingQueue<PlaybackQueueItem> mQueue =
new LinkedBlockingQueue<PlaybackQueueItem>();
private final Thread mHandlerThread;
private volatile MessageParams mCurrentParams = null;
// Used only for book keeping and error detection.
private volatile SynthesisMessageParams mLastSynthesisRequest = null;
// Used to order incoming messages in our priority queue.
private final AtomicLong mSequenceIdCtr = new AtomicLong(0);
private volatile PlaybackQueueItem mCurrentWorkItem = null;
AudioPlaybackHandler() {
mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread");
@@ -62,82 +38,38 @@ class AudioPlaybackHandler {
mHandlerThread.start();
}
/**
* Stops all synthesis for a given {@code token}. If the current token
* is currently being processed, an effort will be made to stop it but
* that is not guaranteed.
*
* NOTE: This assumes that all other messages in the queue with {@code token}
* have been removed already.
*
* NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}.
*/
private void stop(MessageParams token) {
if (token == null) {
private void stop(PlaybackQueueItem item) {
if (item == null) {
return;
}
if (DBG) Log.d(TAG, "Stopping token : " + token);
item.stop(false);
}
if (token.getType() == MessageParams.TYPE_SYNTHESIS) {
AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack();
if (current != null) {
// Stop the current audio track if it's still playing.
// The audio track is thread safe in this regard. The current
// handleSynthesisDataAvailable call will return soon after this
// call.
current.stop();
}
// This is safe because PlaybackSynthesisCallback#stop would have
// been called before this method, and will no longer enqueue any
// audio for this token.
//
// (Even if it did, all it would result in is a warning message).
mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY));
} else if (token.getType() == MessageParams.TYPE_AUDIO) {
((AudioMessageParams) token).getPlayer().stop();
// No cleanup required for audio messages.
} else if (token.getType() == MessageParams.TYPE_SILENCE) {
((SilenceMessageParams) token).getConditionVariable().open();
// No cleanup required for silence messages.
public void enqueue(PlaybackQueueItem item) {
try {
mQueue.put(item);
} catch (InterruptedException ie) {
// This exception will never be thrown, since we allow our queue
// to be have an unbounded size. put() will therefore never block.
}
}
// -----------------------------------------------------
// Methods that add and remove elements from the queue. These do not
// need to be synchronized strictly speaking, but they make the behaviour
// a lot more predictable. (though it would still be correct without
// synchronization).
// -----------------------------------------------------
public void stopForApp(Object callerIdentity) {
if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity);
removeWorkItemsFor(callerIdentity);
synchronized public void removePlaybackItems(Object callerIdentity) {
if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callerIdentity);
removeMessages(callerIdentity);
final MessageParams current = getCurrentParams();
final PlaybackQueueItem current = mCurrentWorkItem;
if (current != null && (current.getCallerIdentity() == callerIdentity)) {
stop(current);
}
final MessageParams lastSynthesis = mLastSynthesisRequest;
if (lastSynthesis != null && lastSynthesis != current &&
(lastSynthesis.getCallerIdentity() == callerIdentity)) {
stop(lastSynthesis);
}
}
synchronized public void removeAllItems() {
if (DBG_THREADING) Log.d(TAG, "Removing all items");
public void stop() {
if (DBG) Log.d(TAG, "Stopping all items");
removeAllMessages();
final MessageParams current = getCurrentParams();
final MessageParams lastSynthesis = mLastSynthesisRequest;
stop(current);
if (lastSynthesis != null && lastSynthesis != current) {
stop(lastSynthesis);
}
stop(mCurrentWorkItem);
}
/**
@@ -145,51 +77,39 @@ class AudioPlaybackHandler {
* being handled, true otherwise.
*/
public boolean isSpeaking() {
return (mQueue.peek() != null) || (mCurrentParams != null);
return (mQueue.peek() != null) || (mCurrentWorkItem != null);
}
/**
* Shut down the audio playback thread.
*/
synchronized public void quit() {
public void quit() {
removeAllMessages();
stop(getCurrentParams());
mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY));
stop(mCurrentWorkItem);
mHandlerThread.interrupt();
}
synchronized void enqueueSynthesisStart(SynthesisMessageParams token) {
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token);
mQueue.add(new ListEntry(SYNTHESIS_START, token));
/*
* Atomically clear the queue of all messages.
*/
private void removeAllMessages() {
mQueue.clear();
}
synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) {
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token);
mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token));
/*
* Remove all messages that originate from a given calling app.
*/
private void removeWorkItemsFor(Object callerIdentity) {
Iterator<PlaybackQueueItem> it = mQueue.iterator();
while (it.hasNext()) {
final PlaybackQueueItem item = it.next();
if (item.getCallerIdentity() == callerIdentity) {
it.remove();
}
}
}
synchronized void enqueueSynthesisDone(SynthesisMessageParams token) {
if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token);
mQueue.add(new ListEntry(SYNTHESIS_DONE, token));
}
synchronized void enqueueAudio(AudioMessageParams token) {
if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token);
mQueue.add(new ListEntry(PLAY_AUDIO, token));
}
synchronized void enqueueSilence(SilenceMessageParams token) {
if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token);
mQueue.add(new ListEntry(PLAY_SILENCE, token));
}
// -----------------------------------------
// End of public API methods.
// -----------------------------------------
// -----------------------------------------
// Methods for managing the message queue.
// -----------------------------------------
/*
* The MessageLoop is a handler like implementation that
* processes messages from a priority queue.
@@ -198,436 +118,23 @@ class AudioPlaybackHandler {
@Override
public void run() {
while (true) {
ListEntry entry = null;
PlaybackQueueItem item = null;
try {
entry = mQueue.take();
item = mQueue.take();
} catch (InterruptedException ie) {
if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)");
return;
}
if (entry.mWhat == SHUTDOWN) {
if (DBG) Log.d(TAG, "MessageLoop : Shutting down");
return;
}
// If stop() or stopForApp() are called between mQueue.take()
// returning and mCurrentWorkItem being set, the current work item
// will be run anyway.
if (DBG) {
Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat
+ " ,seqId : " + entry.mSequenceId);
}
setCurrentParams(entry.mMessage);
handleMessage(entry);
setCurrentParams(null);
mCurrentWorkItem = item;
item.run();
mCurrentWorkItem = null;
}
}
}
/*
* Atomically clear the queue of all messages.
*/
synchronized private void removeAllMessages() {
mQueue.clear();
}
/*
* Remove all messages that originate from a given calling app.
*/
synchronized private void removeMessages(Object callerIdentity) {
Iterator<ListEntry> it = mQueue.iterator();
while (it.hasNext()) {
final ListEntry current = it.next();
// The null check is to prevent us from removing control messages,
// such as a shutdown message.
if (current.mMessage != null &&
current.mMessage.getCallerIdentity() == callerIdentity) {
it.remove();
}
}
}
/*
* An element of our priority queue of messages. Each message has a priority,
* and a sequence id (defined by the order of enqueue calls). Among messages
* with the same priority, messages that were received earlier win out.
*/
private final class ListEntry implements Comparable<ListEntry> {
final int mWhat;
final MessageParams mMessage;
final int mPriority;
final long mSequenceId;
private ListEntry(int what, MessageParams message) {
this(what, message, DEFAULT_PRIORITY);
}
private ListEntry(int what, MessageParams message, int priority) {
mWhat = what;
mMessage = message;
mPriority = priority;
mSequenceId = mSequenceIdCtr.incrementAndGet();
}
@Override
public int compareTo(ListEntry that) {
if (that == this) {
return 0;
}
// Note that this is always 0, 1 or -1.
int priorityDiff = mPriority - that.mPriority;
if (priorityDiff == 0) {
// The == case cannot occur.
return (mSequenceId < that.mSequenceId) ? -1 : 1;
}
return priorityDiff;
}
}
private void setCurrentParams(MessageParams p) {
if (DBG_THREADING) {
if (p != null) {
Log.d(TAG, "Started handling :" + p);
} else {
Log.d(TAG, "End handling : " + mCurrentParams);
}
}
mCurrentParams = p;
}
private MessageParams getCurrentParams() {
return mCurrentParams;
}
// -----------------------------------------
// Methods for dealing with individual messages, the methods
// below do the actual work.
// -----------------------------------------
private void handleMessage(ListEntry entry) {
final MessageParams msg = entry.mMessage;
if (entry.mWhat == SYNTHESIS_START) {
handleSynthesisStart(msg);
} else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) {
handleSynthesisDataAvailable(msg);
} else if (entry.mWhat == SYNTHESIS_DONE) {
handleSynthesisDone(msg);
} else if (entry.mWhat == PLAY_AUDIO) {
handleAudio(msg);
} else if (entry.mWhat == PLAY_SILENCE) {
handleSilence(msg);
}
}
// Currently implemented as blocking the audio playback thread for the
// specified duration. If a call to stop() is made, the thread
// unblocks.
private void handleSilence(MessageParams msg) {
if (DBG) Log.d(TAG, "handleSilence()");
SilenceMessageParams params = (SilenceMessageParams) msg;
params.getDispatcher().dispatchOnStart();
if (params.getSilenceDurationMs() > 0) {
params.getConditionVariable().block(params.getSilenceDurationMs());
}
params.getDispatcher().dispatchOnDone();
if (DBG) Log.d(TAG, "handleSilence() done.");
}
// Plays back audio from a given URI. No TTS engine involvement here.
private void handleAudio(MessageParams msg) {
if (DBG) Log.d(TAG, "handleAudio()");
AudioMessageParams params = (AudioMessageParams) msg;
params.getDispatcher().dispatchOnStart();
// Note that the BlockingMediaPlayer spawns a separate thread.
//
// TODO: This can be avoided.
params.getPlayer().startAndWait();
params.getDispatcher().dispatchOnDone();
if (DBG) Log.d(TAG, "handleAudio() done.");
}
// Denotes the start of a new synthesis request. We create a new
// audio track, and prepare it for incoming data.
//
// Note that since all TTS synthesis happens on a single thread, we
// should ALWAYS see the following order :
//
// handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone
// OR
// handleSynthesisCompleteDataAvailable.
private void handleSynthesisStart(MessageParams msg) {
if (DBG) Log.d(TAG, "handleSynthesisStart()");
final SynthesisMessageParams param = (SynthesisMessageParams) msg;
// Oops, looks like the engine forgot to call done(). We go through
// extra trouble to clean the data to prevent the AudioTrack resources
// from being leaked.
if (mLastSynthesisRequest != null) {
Log.e(TAG, "Error : Missing call to done() for request : " +
mLastSynthesisRequest);
handleSynthesisDone(mLastSynthesisRequest);
}
mLastSynthesisRequest = param;
// Create the audio track.
final AudioTrack audioTrack = createStreamingAudioTrack(param);
if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]");
param.setAudioTrack(audioTrack);
msg.getDispatcher().dispatchOnStart();
}
// More data available to be flushed to the audio track.
private void handleSynthesisDataAvailable(MessageParams msg) {
final SynthesisMessageParams param = (SynthesisMessageParams) msg;
if (param.getAudioTrack() == null) {
Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param);
return;
}
if (param != mLastSynthesisRequest) {
Log.e(TAG, "Call to dataAvailable without done() / start()");
return;
}
final AudioTrack audioTrack = param.getAudioTrack();
final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer();
if (bufferCopy == null) {
Log.e(TAG, "No buffers available to play.");
return;
}
int playState = audioTrack.getPlayState();
if (playState == AudioTrack.PLAYSTATE_STOPPED) {
if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode());
audioTrack.play();
}
int count = 0;
while (count < bufferCopy.mBytes.length) {
// Note that we don't take bufferCopy.mOffset into account because
// it is guaranteed to be 0.
int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mBytes.length);
if (written <= 0) {
break;
}
count += written;
}
param.mBytesWritten += count;
param.mLogger.onPlaybackStart();
}
// Wait for the audio track to stop playing, and then release its resources.
private void handleSynthesisDone(MessageParams msg) {
final SynthesisMessageParams params = (SynthesisMessageParams) msg;
if (DBG) Log.d(TAG, "handleSynthesisDone()");
final AudioTrack audioTrack = params.getAudioTrack();
if (audioTrack == null) {
// There was already a call to handleSynthesisDone for
// this token.
return;
}
if (params.mBytesWritten < params.mAudioBufferSize) {
if (DBG) Log.d(TAG, "Stopping audio track to flush audio, state was : " +
audioTrack.getPlayState());
params.mIsShortUtterance = true;
audioTrack.stop();
}
if (DBG) Log.d(TAG, "Waiting for audio track to complete : " +
audioTrack.hashCode());
blockUntilDone(params);
if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]");
// The last call to AudioTrack.write( ) will return only after
// all data from the audioTrack has been sent to the mixer, so
// it's safe to release at this point. Make sure release() and the call
// that set the audio track to null are performed atomically.
synchronized (this) {
// Never allow the audioTrack to be observed in a state where
// it is released but non null. The only case this might happen
// is in the various stopFoo methods that call AudioTrack#stop from
// different threads, but they are synchronized on AudioPlayBackHandler#this
// too.
audioTrack.release();
params.setAudioTrack(null);
}
if (params.isError()) {
params.getDispatcher().dispatchOnError();
} else {
params.getDispatcher().dispatchOnDone();
}
mLastSynthesisRequest = null;
params.mLogger.onWriteData();
}
/**
* The minimum increment of time to wait for an audiotrack to finish
* playing.
*/
private static final long MIN_SLEEP_TIME_MS = 20;
/**
* The maximum increment of time to sleep while waiting for an audiotrack
* to finish playing.
*/
private static final long MAX_SLEEP_TIME_MS = 2500;
/**
* The maximum amount of time to wait for an audio track to make progress while
* it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
* could happen in exceptional circumstances like a media_server crash.
*/
private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
private static void blockUntilDone(SynthesisMessageParams params) {
if (params.mAudioTrack == null || params.mBytesWritten <= 0) {
return;
}
if (params.mIsShortUtterance) {
// In this case we would have called AudioTrack#stop() to flush
// buffers to the mixer. This makes the playback head position
// unobservable and notification markers do not work reliably. We
// have no option but to wait until we think the track would finish
// playing and release it after.
//
// This isn't as bad as it looks because (a) We won't end up waiting
// for much longer than we should because even at 4khz mono, a short
// utterance weighs in at about 2 seconds, and (b) such short utterances
// are expected to be relatively infrequent and in a stream of utterances
// this shows up as a slightly longer pause.
blockUntilEstimatedCompletion(params);
} else {
blockUntilCompletion(params);
}
}
private static void blockUntilEstimatedCompletion(SynthesisMessageParams params) {
final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
final long estimatedTimeMs = (lengthInFrames * 1000 / params.mSampleRateInHz);
if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
try {
Thread.sleep(estimatedTimeMs);
} catch (InterruptedException ie) {
// Do nothing.
}
}
private static void blockUntilCompletion(SynthesisMessageParams params) {
final AudioTrack audioTrack = params.mAudioTrack;
final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
int previousPosition = -1;
int currentPosition = 0;
long blockedTimeMs = 0;
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
audioTrack.getSampleRate();
final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
// Check if the audio track has made progress since the last loop
// iteration. We should then add in the amount of time that was
// spent sleeping in the last iteration.
if (currentPosition == previousPosition) {
// This works only because the sleep time that would have been calculated
// would be the same in the previous iteration too.
blockedTimeMs += sleepTimeMs;
// If we've taken too long to make progress, bail.
if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
"for AudioTrack to make progress, Aborting");
break;
}
} else {
blockedTimeMs = 0;
}
previousPosition = currentPosition;
if (DBG) Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
" Playback position : " + currentPosition + ", Length in frames : "
+ lengthInFrames);
try {
Thread.sleep(sleepTimeMs);
} catch (InterruptedException ie) {
break;
}
}
}
private static final long clip(long value, long min, long max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
private static AudioTrack createStreamingAudioTrack(SynthesisMessageParams params) {
final int channelConfig = getChannelConfig(params.mChannelCount);
final int sampleRateInHz = params.mSampleRateInHz;
final int audioFormat = params.mAudioFormat;
int minBufferSizeInBytes
= AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
AudioTrack audioTrack = new AudioTrack(params.mStreamType, sampleRateInHz, channelConfig,
audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
Log.w(TAG, "Unable to create audio track.");
audioTrack.release();
return null;
}
params.mAudioBufferSize = bufferSizeInBytes;
setupVolume(audioTrack, params.mVolume, params.mPan);
return audioTrack;
}
static int getChannelConfig(int channelCount) {
if (channelCount == 1) {
return AudioFormat.CHANNEL_OUT_MONO;
} else if (channelCount == 2){
return AudioFormat.CHANNEL_OUT_STEREO;
}
return 0;
}
private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
float vol = clip(volume, 0.0f, 1.0f);
float panning = clip(pan, -1.0f, 1.0f);
float volLeft = vol;
float volRight = vol;
if (panning > 0.0f) {
volLeft *= (1.0f - panning);
} else if (panning < 0.0f) {
volRight *= (1.0f + panning);
}
if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
Log.e(TAG, "Failed to set volume");
}
}
private static float clip(float value, float min, float max) {
return value > max ? max : (value < min ? min : value);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,338 @@
// Copyright 2011 Google Inc. All Rights Reserved.
package android.speech.tts;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.util.Log;
/**
* Exposes parts of the {@link AudioTrack} API by delegating calls to an
* underlying {@link AudioTrack}. Additionally, provides methods like
* {@link #waitAndRelease()} that will block until all audiotrack
* data has been flushed to the mixer, and is estimated to have completed
* playback.
*/
class BlockingAudioTrack {
private static final String TAG = "TTS.BlockingAudioTrack";
private static final boolean DBG = false;
/**
* The minimum increment of time to wait for an AudioTrack to finish
* playing.
*/
private static final long MIN_SLEEP_TIME_MS = 20;
/**
* The maximum increment of time to sleep while waiting for an AudioTrack
* to finish playing.
*/
private static final long MAX_SLEEP_TIME_MS = 2500;
/**
* The maximum amount of time to wait for an audio track to make progress while
* it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
* could happen in exceptional circumstances like a media_server crash.
*/
private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
/**
* Minimum size of the buffer of the underlying {@link android.media.AudioTrack}
* we create.
*/
private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
private final int mStreamType;
private final int mSampleRateInHz;
private final int mAudioFormat;
private final int mChannelCount;
private final float mVolume;
private final float mPan;
private final int mBytesPerFrame;
/**
* A "short utterance" is one that uses less bytes than the audio
* track buffer size (mAudioBufferSize). In this case, we need to call
* {@link AudioTrack#stop()} to send pending buffers to the mixer, and slightly
* different logic is required to wait for the track to finish.
*
* Not volatile, accessed only from the audio playback thread.
*/
private boolean mIsShortUtterance;
/**
* Will be valid after a call to {@link #init()}.
*/
private int mAudioBufferSize;
private int mBytesWritten = 0;
private AudioTrack mAudioTrack;
private volatile boolean mStopped;
// Locks the initialization / uninitialization of the audio track.
// This is required because stop() will throw an illegal state exception
// if called before init() or after mAudioTrack.release().
private final Object mAudioTrackLock = new Object();
BlockingAudioTrack(int streamType, int sampleRate,
int audioFormat, int channelCount,
float volume, float pan) {
mStreamType = streamType;
mSampleRateInHz = sampleRate;
mAudioFormat = audioFormat;
mChannelCount = channelCount;
mVolume = volume;
mPan = pan;
mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount;
mIsShortUtterance = false;
mAudioBufferSize = 0;
mBytesWritten = 0;
mAudioTrack = null;
mStopped = false;
}
public void init() {
AudioTrack track = createStreamingAudioTrack();
synchronized (mAudioTrackLock) {
mAudioTrack = track;
}
}
public void stop() {
synchronized (mAudioTrackLock) {
if (mAudioTrack != null) {
mAudioTrack.stop();
}
}
mStopped = true;
}
public int write(byte[] data) {
if (mAudioTrack == null || mStopped) {
return -1;
}
final int bytesWritten = writeToAudioTrack(mAudioTrack, data);
mBytesWritten += bytesWritten;
return bytesWritten;
}
public void waitAndRelease() {
// For "small" audio tracks, we have to stop() them to make them mixable,
// else the audio subsystem will wait indefinitely for us to fill the buffer
// before rendering the track mixable.
//
// If mStopped is true, the track would already have been stopped, so not
// much point not doing that again.
if (mBytesWritten < mAudioBufferSize && !mStopped) {
if (DBG) {
Log.d(TAG, "Stopping audio track to flush audio, state was : " +
mAudioTrack.getPlayState() + ",stopped= " + mStopped);
}
mIsShortUtterance = true;
mAudioTrack.stop();
}
// Block until the audio track is done only if we haven't stopped yet.
if (!mStopped) {
if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + mAudioTrack.hashCode());
blockUntilDone(mAudioTrack);
}
// The last call to AudioTrack.write( ) will return only after
// all data from the audioTrack has been sent to the mixer, so
// it's safe to release at this point.
if (DBG) Log.d(TAG, "Releasing audio track [" + mAudioTrack.hashCode() + "]");
synchronized (mAudioTrackLock) {
mAudioTrack.release();
mAudioTrack = null;
}
}
static int getChannelConfig(int channelCount) {
if (channelCount == 1) {
return AudioFormat.CHANNEL_OUT_MONO;
} else if (channelCount == 2){
return AudioFormat.CHANNEL_OUT_STEREO;
}
return 0;
}
long getAudioLengthMs(int numBytes) {
final int unconsumedFrames = numBytes / mBytesPerFrame;
final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz;
return estimatedTimeMs;
}
private static int writeToAudioTrack(AudioTrack audioTrack, byte[] bytes) {
if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
if (DBG) Log.d(TAG, "AudioTrack not playing, restarting : " + audioTrack.hashCode());
audioTrack.play();
}
int count = 0;
while (count < bytes.length) {
// Note that we don't take bufferCopy.mOffset into account because
// it is guaranteed to be 0.
int written = audioTrack.write(bytes, count, bytes.length);
if (written <= 0) {
break;
}
count += written;
}
return count;
}
private AudioTrack createStreamingAudioTrack() {
final int channelConfig = getChannelConfig(mChannelCount);
int minBufferSizeInBytes
= AudioTrack.getMinBufferSize(mSampleRateInHz, channelConfig, mAudioFormat);
int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
AudioTrack audioTrack = new AudioTrack(mStreamType, mSampleRateInHz, channelConfig,
mAudioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
Log.w(TAG, "Unable to create audio track.");
audioTrack.release();
return null;
}
mAudioBufferSize = bufferSizeInBytes;
setupVolume(audioTrack, mVolume, mPan);
return audioTrack;
}
private static int getBytesPerFrame(int audioFormat) {
if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
return 1;
} else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
return 2;
}
return -1;
}
private void blockUntilDone(AudioTrack audioTrack) {
if (mBytesWritten <= 0) {
return;
}
if (mIsShortUtterance) {
// In this case we would have called AudioTrack#stop() to flush
// buffers to the mixer. This makes the playback head position
// unobservable and notification markers do not work reliably. We
// have no option but to wait until we think the track would finish
// playing and release it after.
//
// This isn't as bad as it looks because (a) We won't end up waiting
// for much longer than we should because even at 4khz mono, a short
// utterance weighs in at about 2 seconds, and (b) such short utterances
// are expected to be relatively infrequent and in a stream of utterances
// this shows up as a slightly longer pause.
blockUntilEstimatedCompletion();
} else {
blockUntilCompletion(audioTrack);
}
}
private void blockUntilEstimatedCompletion() {
final int lengthInFrames = mBytesWritten / mBytesPerFrame;
final long estimatedTimeMs = (lengthInFrames * 1000 / mSampleRateInHz);
if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
try {
Thread.sleep(estimatedTimeMs);
} catch (InterruptedException ie) {
// Do nothing.
}
}
private void blockUntilCompletion(AudioTrack audioTrack) {
final int lengthInFrames = mBytesWritten / mBytesPerFrame;
int previousPosition = -1;
int currentPosition = 0;
long blockedTimeMs = 0;
while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && !mStopped) {
final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
audioTrack.getSampleRate();
final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
// Check if the audio track has made progress since the last loop
// iteration. We should then add in the amount of time that was
// spent sleeping in the last iteration.
if (currentPosition == previousPosition) {
// This works only because the sleep time that would have been calculated
// would be the same in the previous iteration too.
blockedTimeMs += sleepTimeMs;
// If we've taken too long to make progress, bail.
if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
"for AudioTrack to make progress, Aborting");
break;
}
} else {
blockedTimeMs = 0;
}
previousPosition = currentPosition;
if (DBG) {
Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
" Playback position : " + currentPosition + ", Length in frames : "
+ lengthInFrames);
}
try {
Thread.sleep(sleepTimeMs);
} catch (InterruptedException ie) {
break;
}
}
}
private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
final float vol = clip(volume, 0.0f, 1.0f);
final float panning = clip(pan, -1.0f, 1.0f);
float volLeft = vol;
float volRight = vol;
if (panning > 0.0f) {
volLeft *= (1.0f - panning);
} else if (panning < 0.0f) {
volRight *= (1.0f + panning);
}
if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
Log.e(TAG, "Failed to set volume");
}
}
private static final long clip(long value, long min, long max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
private static float clip(float value, float min, float max) {
return value > max ? max : (value < min ? min : value);
}
}

View File

@@ -54,7 +54,6 @@ class BlockingMediaPlayer {
mUri = uri;
mStreamType = streamType;
mDone = new ConditionVariable();
}
/**

View File

@@ -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(),

View File

@@ -0,0 +1,27 @@
// Copyright 2011 Google Inc. All Rights Reserved.
package android.speech.tts;
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
abstract class PlaybackQueueItem implements Runnable {
private final UtteranceProgressDispatcher mDispatcher;
private final Object mCallerIdentity;
PlaybackQueueItem(TextToSpeechService.UtteranceProgressDispatcher dispatcher,
Object callerIdentity) {
mDispatcher = dispatcher;
mCallerIdentity = callerIdentity;
}
Object getCallerIdentity() {
return mCallerIdentity;
}
protected UtteranceProgressDispatcher getDispatcher() {
return mDispatcher;
}
public abstract void run();
abstract void stop(boolean isError);
}

View File

@@ -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;

View File

@@ -17,28 +17,29 @@ package android.speech.tts;
import android.os.ConditionVariable;
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
import android.util.Log;
class SilenceMessageParams extends MessageParams {
class SilencePlaybackQueueItem extends PlaybackQueueItem {
private final ConditionVariable mCondVar = new ConditionVariable();
private final long mSilenceDurationMs;
SilenceMessageParams(UtteranceProgressDispatcher dispatcher,
SilencePlaybackQueueItem(UtteranceProgressDispatcher dispatcher,
Object callerIdentity, long silenceDurationMs) {
super(dispatcher, callerIdentity);
mSilenceDurationMs = silenceDurationMs;
}
long getSilenceDurationMs() {
return mSilenceDurationMs;
@Override
public void run() {
getDispatcher().dispatchOnStart();
if (mSilenceDurationMs > 0) {
mCondVar.block(mSilenceDurationMs);
}
getDispatcher().dispatchOnDone();
}
@Override
int getType() {
return TYPE_SILENCE;
void stop(boolean isError) {
mCondVar.open();
}
ConditionVariable getConditionVariable() {
return mCondVar;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,245 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package android.speech.tts;
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
import android.util.Log;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Manages the playback of a list of byte arrays representing audio data
* that are queued by the engine to an audio track.
*/
final class SynthesisPlaybackQueueItem extends PlaybackQueueItem {
private static final String TAG = "TTS.SynthQueueItem";
private static final boolean DBG = false;
/**
* Maximum length of audio we leave unconsumed by the audio track.
* Calls to {@link #put(byte[])} will block until we have less than
* this amount of audio left to play back.
*/
private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
/**
* Guards accesses to mDataBufferList and mUnconsumedBytes.
*/
private final Lock mListLock = new ReentrantLock();
private final Condition mReadReady = mListLock.newCondition();
private final Condition mNotFull = mListLock.newCondition();
// Guarded by mListLock.
private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
// Guarded by mListLock.
private int mUnconsumedBytes;
/*
* While mStopped and mIsError can be written from any thread, mDone is written
* only from the synthesis thread. All three variables are read from the
* audio playback thread.
*/
private volatile boolean mStopped;
private volatile boolean mDone;
private volatile boolean mIsError;
private final BlockingAudioTrack mAudioTrack;
private final EventLogger mLogger;
SynthesisPlaybackQueueItem(int streamType, int sampleRate,
int audioFormat, int channelCount,
float volume, float pan, UtteranceProgressDispatcher dispatcher,
Object callerIdentity, EventLogger logger) {
super(dispatcher, callerIdentity);
mUnconsumedBytes = 0;
mStopped = false;
mDone = false;
mIsError = false;
mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat,
channelCount, volume, pan);
mLogger = logger;
}
@Override
public void run() {
final UtteranceProgressDispatcher dispatcher = getDispatcher();
dispatcher.dispatchOnStart();
mAudioTrack.init();
try {
byte[] buffer = null;
// take() will block until:
//
// (a) there is a buffer available to tread. In which case
// a non null value is returned.
// OR (b) stop() is called in which case it will return null.
// OR (c) done() is called in which case it will return null.
while ((buffer = take()) != null) {
mAudioTrack.write(buffer);
mLogger.onAudioDataWritten();
}
} catch (InterruptedException ie) {
if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
}
mAudioTrack.waitAndRelease();
if (mIsError) {
dispatcher.dispatchOnError();
} else {
dispatcher.dispatchOnDone();
}
mLogger.onWriteData();
}
@Override
void stop(boolean isError) {
try {
mListLock.lock();
// Update our internal state.
mStopped = true;
mIsError = isError;
// Wake up the audio playback thread if it was waiting on take().
// take() will return null since mStopped was true, and will then
// break out of the data write loop.
mReadReady.signal();
// Wake up the synthesis thread if it was waiting on put(). Its
// buffers will no longer be copied since mStopped is true. The
// PlaybackSynthesisCallback that this synthesis corresponds to
// would also have been stopped, and so all calls to
// Callback.onDataAvailable( ) will return errors too.
mNotFull.signal();
} finally {
mListLock.unlock();
}
// Stop the underlying audio track. This will stop sending
// data to the mixer and discard any pending buffers that the
// track holds.
mAudioTrack.stop();
}
void done() {
try {
mListLock.lock();
// Update state.
mDone = true;
// Unblocks the audio playback thread if it was waiting on take()
// after having consumed all available buffers. It will then return
// null and leave the write loop.
mReadReady.signal();
// Just so that engines that try to queue buffers after
// calling done() don't block the synthesis thread forever. Ideally
// this should be called from the same thread as put() is, and hence
// this call should be pointless.
mNotFull.signal();
} finally {
mListLock.unlock();
}
}
void put(byte[] buffer) throws InterruptedException {
try {
mListLock.lock();
long unconsumedAudioMs = 0;
while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
mNotFull.await();
}
// Don't bother queueing the buffer if we've stopped. The playback thread
// would have woken up when stop() is called (if it was blocked) and will
// proceed to leave the write loop since take() will return null when
// stopped.
if (mStopped) {
return;
}
mDataBufferList.add(new ListEntry(buffer));
mUnconsumedBytes += buffer.length;
mReadReady.signal();
} finally {
mListLock.unlock();
}
}
private byte[] take() throws InterruptedException {
try {
mListLock.lock();
// Block if there are no available buffers, and stop() has not
// been called and done() has not been called.
while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
mReadReady.await();
}
// If stopped, return null so that we can exit the playback loop
// as soon as possible.
if (mStopped) {
return null;
}
// Remove the first entry from the queue.
ListEntry entry = mDataBufferList.poll();
// This is the normal playback loop exit case, when done() was
// called. (mDone will be true at this point).
if (entry == null) {
return null;
}
mUnconsumedBytes -= entry.mBytes.length;
// Unblock the waiting writer. We use signal() and not signalAll()
// because there will only be one thread waiting on this (the
// Synthesis thread).
mNotFull.signal();
return entry.mBytes;
} finally {
mListLock.unlock();
}
}
static final class ListEntry {
final byte[] mBytes;
ListEntry(byte[] bytes) {
mBytes = bytes;
}
}
}

View File

@@ -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
}
}