diff options
author | Narayan Kamath <narayan@google.com> | 2011-06-08 02:11:26 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-06-08 02:11:26 -0700 |
commit | 233c56a3509ba2d2446de0f7d9e1112ca82063ea (patch) | |
tree | e21b0172ed81bbd5b0074f336d7e9d55da37aaa4 | |
parent | 7f86827af44eb5267c8d21f355d109ff71b04f10 (diff) | |
parent | 8d1fc2403b8277e68d7816b2bbf05464a4c7a58a (diff) | |
download | frameworks_base-233c56a3509ba2d2446de0f7d9e1112ca82063ea.zip frameworks_base-233c56a3509ba2d2446de0f7d9e1112ca82063ea.tar.gz frameworks_base-233c56a3509ba2d2446de0f7d9e1112ca82063ea.tar.bz2 |
Merge "Fix the threading behaviour of TTS audio requests."
7 files changed, 686 insertions, 240 deletions
diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java new file mode 100644 index 0000000..db4d622 --- /dev/null +++ b/core/java/android/speech/tts/AudioMessageParams.java @@ -0,0 +1,37 @@ +/* + * 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.UtteranceCompletedDispatcher; + +class AudioMessageParams extends MessageParams { + private final BlockingMediaPlayer mPlayer; + + AudioMessageParams(UtteranceCompletedDispatcher dispatcher, BlockingMediaPlayer player) { + super(dispatcher); + mPlayer = player; + } + + BlockingMediaPlayer getPlayer() { + return mPlayer; + } + + @Override + int getType() { + return TYPE_AUDIO; + } + +} diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java new file mode 100644 index 0000000..924bbbc --- /dev/null +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -0,0 +1,358 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.speech.tts.SynthesisMessageParams.ListEntry; +import android.util.Log; + +class AudioPlaybackHandler extends Handler { + private static final String TAG = "TTS.AudioPlaybackHandler"; + 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_COMPLETE_DATA_AVAILABLE = 3; + private static final int SYNTHESIS_DONE = 4; + + private static final int PLAY_AUDIO = 5; + private static final int PLAY_SILENCE = 6; + + // Accessed by multiple threads, synchronized by "this". + private MessageParams mCurrentParams; + // Used only for book keeping and error detection. + private SynthesisMessageParams mLastSynthesisRequest; + + AudioPlaybackHandler(Looper looper) { + super(looper); + } + + @Override + public synchronized void handleMessage(Message msg) { + if (msg.what == SYNTHESIS_START) { + mCurrentParams = (SynthesisMessageParams) msg.obj; + handleSynthesisStart(msg); + } else if (msg.what == SYNTHESIS_DATA_AVAILABLE) { + handleSynthesisDataAvailable(msg); + } else if (msg.what == SYNTHESIS_DONE) { + handleSynthesisDone(msg); + } else if (msg.what == SYNTHESIS_COMPLETE_DATA_AVAILABLE) { + handleSynthesisCompleteDataAvailable(msg); + } else if (msg.what == PLAY_AUDIO) { + handleAudio(msg); + } else if (msg.what == PLAY_SILENCE) { + handleSilence(msg); + } + + mCurrentParams = null; + } + + /** + * 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. + */ + synchronized public void stop(MessageParams token) { + removeCallbacksAndMessages(token); + + if (token.getType() == MessageParams.TYPE_SYNTHESIS) { + sendMessageAtFrontOfQueue(obtainMessage(SYNTHESIS_DONE, token)); + } else if (token == mCurrentParams) { + if (token.getType() == MessageParams.TYPE_AUDIO) { + ((AudioMessageParams) mCurrentParams).getPlayer().stop(); + } else if (token.getType() == MessageParams.TYPE_SILENCE) { + ((SilenceMessageParams) mCurrentParams).getConditionVariable().open(); + } + } + } + + /** + * Shut down the audio playback thread. + */ + synchronized public void quit() { + if (mCurrentParams != null) { + stop(mCurrentParams); + } + getLooper().quit(); + } + + void enqueueSynthesisStart(SynthesisMessageParams token) { + sendMessage(obtainMessage(SYNTHESIS_START, token)); + } + + void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { + sendMessage(obtainMessage(SYNTHESIS_DATA_AVAILABLE, token)); + } + + void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) { + sendMessage(obtainMessage(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token)); + } + + void enqueueSynthesisDone(SynthesisMessageParams token) { + sendMessage(obtainMessage(SYNTHESIS_DONE, token)); + } + + void enqueueAudio(AudioMessageParams token) { + sendMessage(obtainMessage(PLAY_AUDIO, token)); + } + + void enqueueSilence(SilenceMessageParams token) { + sendMessage(obtainMessage(PLAY_SILENCE, token)); + } + + // ----------------------------------------- + // End of public API methods. + // ----------------------------------------- + + // Currently implemented as blocking the audio playback thread for the + // specified duration. If a call to stop() is made, the thread + // unblocks. + private void handleSilence(Message msg) { + if (DBG) Log.d(TAG, "handleSilence()"); + SilenceMessageParams params = (SilenceMessageParams) msg.obj; + if (params.getSilenceDurationMs() > 0) { + params.getConditionVariable().block(params.getSilenceDurationMs()); + } + params.getDispatcher().dispatchUtteranceCompleted(); + if (DBG) Log.d(TAG, "handleSilence() done."); + } + + // Plays back audio from a given URI. No TTS engine involvement here. + private void handleAudio(Message msg) { + if (DBG) Log.d(TAG, "handleAudio()"); + AudioMessageParams params = (AudioMessageParams) msg.obj; + // Note that the BlockingMediaPlayer spawns a separate thread. + // + // TODO: This can be avoided. + params.getPlayer().startAndWait(); + params.getDispatcher().dispatchUtteranceCompleted(); + 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(Message msg) { + if (DBG) Log.d(TAG, "handleSynthesisStart()"); + final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj; + + // 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.w(TAG, "Error : Missing call to done() for request : " + + mLastSynthesisRequest); + handleSynthesisDone(mLastSynthesisRequest); + } + + mLastSynthesisRequest = param; + + // Create the audio track. + final AudioTrack audioTrack = createStreamingAudioTrack( + param.mStreamType, param.mSampleRateInHz, param.mAudioFormat, + param.mChannelCount, param.mVolume, param.mPan); + + param.setAudioTrack(audioTrack); + } + + // More data available to be flushed to the audio track. + private void handleSynthesisDataAvailable(Message msg) { + final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj; + if (param.getAudioTrack() == null) { + Log.w(TAG, "Error : null audio track in handleDataAvailable."); + return; + } + + if (param != mLastSynthesisRequest) { + Log.e(TAG, "Call to dataAvailable without done() / start()"); + return; + } + + final AudioTrack audioTrack = param.getAudioTrack(); + final 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.mLength) { + // 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.mLength); + if (written <= 0) { + break; + } + count += written; + } + } + + private void handleSynthesisDone(Message msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj; + handleSynthesisDone(params); + } + + // Flush all remaining data to the audio track, stop it and release + // all it's resources. + private void handleSynthesisDone(SynthesisMessageParams params) { + if (DBG) Log.d(TAG, "handleSynthesisDone()"); + final AudioTrack audioTrack = params.getAudioTrack(); + + try { + if (audioTrack != null) { + audioTrack.flush(); + audioTrack.stop(); + audioTrack.release(); + } + } finally { + params.setAudioTrack(null); + params.getDispatcher().dispatchUtteranceCompleted(); + mLastSynthesisRequest = null; + } + } + + private void handleSynthesisCompleteDataAvailable(Message msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj; + if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")"); + + // Channel config and bytes per frame are checked before + // this message is sent. + int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount); + int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat); + + ListEntry entry = params.getNextBuffer(); + + if (entry == null) { + Log.w(TAG, "completeDataAvailable : No buffers available to play."); + return; + } + + final AudioTrack audioTrack = new AudioTrack(params.mStreamType, params.mSampleRateInHz, + channelConfig, params.mAudioFormat, entry.mLength, AudioTrack.MODE_STATIC); + + // So that handleDone can access this correctly. + params.mAudioTrack = audioTrack; + + try { + audioTrack.write(entry.mBytes, entry.mOffset, entry.mLength); + setupVolume(audioTrack, params.mVolume, params.mPan); + audioTrack.play(); + blockUntilDone(audioTrack, bytesPerFrame, entry.mLength); + if (DBG) Log.d(TAG, "Wrote data to audio track successfully : " + entry.mLength); + } catch (IllegalStateException ex) { + Log.e(TAG, "Playback error", ex); + } finally { + handleSynthesisDone(msg); + } + } + + + private static void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) { + int lengthInFrames = length / bytesPerFrame; + int currentPosition = 0; + while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) { + long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / + audioTrack.getSampleRate(); + audioTrack.getPlayState(); + if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," + + " Playback position : " + currentPosition); + try { + Thread.sleep(estimatedTimeMs); + } catch (InterruptedException ie) { + break; + } + } + } + + private static AudioTrack createStreamingAudioTrack(int streamType, int sampleRateInHz, + int audioFormat, int channelCount, float volume, float pan) { + int channelConfig = getChannelConfig(channelCount); + + int minBufferSizeInBytes + = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); + int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); + + AudioTrack audioTrack = new AudioTrack(streamType, 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; + } + + setupVolume(audioTrack, volume, pan); + 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; + } + + 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 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); + } + +} diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java new file mode 100644 index 0000000..2d96df4 --- /dev/null +++ b/core/java/android/speech/tts/MessageParams.java @@ -0,0 +1,36 @@ +/* + * 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.UtteranceCompletedDispatcher; + +abstract class MessageParams { + private final UtteranceCompletedDispatcher mDispatcher; + + static final int TYPE_SYNTHESIS = 1; + static final int TYPE_AUDIO = 2; + static final int TYPE_SILENCE = 3; + + MessageParams(UtteranceCompletedDispatcher dispatcher) { + mDispatcher = dispatcher; + } + + UtteranceCompletedDispatcher getDispatcher() { + return mDispatcher; + } + + abstract int getType(); +} diff --git a/core/java/android/speech/tts/PlaybackSynthesisRequest.java b/core/java/android/speech/tts/PlaybackSynthesisRequest.java index d698b54..34b263c 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisRequest.java +++ b/core/java/android/speech/tts/PlaybackSynthesisRequest.java @@ -15,10 +15,8 @@ */ package android.speech.tts; -import android.media.AudioFormat; -import android.media.AudioTrack; import android.os.Bundle; -import android.os.Handler; +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; import android.util.Log; /** @@ -49,54 +47,48 @@ class PlaybackSynthesisRequest extends SynthesisRequest { */ private final float mPan; + /** + * Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}. + */ private final Object mStateLock = new Object(); - private final Handler mAudioTrackHandler; - private volatile AudioTrack mAudioTrack = null; + + // Handler associated with a thread that plays back audio requests. + private final AudioPlaybackHandler mAudioTrackHandler; + // A request "token", which will be non null after start() or + // completeAudioAvailable() have been called. + private SynthesisMessageParams mToken = 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. private boolean mStopped = false; - private boolean mDone = false; - private volatile boolean mWriteErrorOccured; - PlaybackSynthesisRequest(String text, Bundle params, - int streamType, float volume, float pan, Handler audioTrackHandler) { + private volatile boolean mDone = false; + + private final UtteranceCompletedDispatcher mDispatcher; + + PlaybackSynthesisRequest(String text, Bundle params, int streamType, float volume, float pan, + AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher) { super(text, params); mStreamType = streamType; mVolume = volume; mPan = pan; mAudioTrackHandler = audioTrackHandler; - mWriteErrorOccured = false; + mDispatcher = dispatcher; } @Override void stop() { if (DBG) Log.d(TAG, "stop()"); + synchronized (mStateLock) { + if (mToken == null || mStopped) { + Log.w(TAG, "stop() called twice, before start(), or after done()"); + return; + } + mAudioTrackHandler.stop(mToken); + mToken = null; mStopped = true; - cleanUp(); - } - } - - // Always guarded by mStateLock. - private void cleanUp() { - if (DBG) Log.d(TAG, "cleanUp()"); - if (mAudioTrack == null) { - return; } - - final AudioTrack audioTrack = mAudioTrack; - mAudioTrack = null; - - // Clean up on the audiotrack handler thread. - // - // NOTE: It isn't very clear whether AudioTrack is thread safe. - // If it is we can clean up on the current (synthesis) thread. - mAudioTrackHandler.post(new Runnable() { - @Override - public void run() { - audioTrack.flush(); - audioTrack.stop(); - audioTrack.release(); - } - }); } @Override @@ -111,7 +103,6 @@ class PlaybackSynthesisRequest extends SynthesisRequest { return mDone; } - // TODO: add a thread that writes to the AudioTrack? @Override public int start(int sampleRateInHz, int audioFormat, int channelCount) { if (DBG) { @@ -119,45 +110,28 @@ class PlaybackSynthesisRequest extends SynthesisRequest { + "," + channelCount + ")"); } + int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + return TextToSpeech.ERROR; + } + synchronized (mStateLock) { if (mStopped) { - if (DBG) Log.d(TAG, "Request has been aborted."); - return TextToSpeech.ERROR; - } - if (mAudioTrack != null) { - Log.e(TAG, "start() called twice"); - cleanUp(); + if (DBG) Log.d(TAG, "stop() called before start(), returning."); return TextToSpeech.ERROR; } + SynthesisMessageParams params = new SynthesisMessageParams( + mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, + mDispatcher); + mAudioTrackHandler.enqueueSynthesisStart(params); - mAudioTrack = createStreamingAudioTrack(sampleRateInHz, audioFormat, channelCount); - if (mAudioTrack == null) { - return TextToSpeech.ERROR; - } + mToken = params; } return TextToSpeech.SUCCESS; } - private 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 float clip(float value, float min, float max) { - return value > max ? max : (value < min ? min : value); - } @Override public int audioAvailable(byte[] buffer, int offset, int length) { @@ -169,195 +143,78 @@ class PlaybackSynthesisRequest extends SynthesisRequest { throw new IllegalArgumentException("buffer is too large or of zero length (" + + length + " bytes)"); } + synchronized (mStateLock) { - if (mWriteErrorOccured) { - if (DBG) Log.d(TAG, "Error writing to audio track, count < 0"); - return TextToSpeech.ERROR; - } - if (mStopped) { - if (DBG) Log.d(TAG, "Request has been aborted."); + if (mToken == null) { return TextToSpeech.ERROR; } - if (mAudioTrack == null) { - Log.e(TAG, "audioAvailable(): Not started"); - return TextToSpeech.ERROR; - } - final AudioTrack audioTrack = mAudioTrack; + // Sigh, another copy. final byte[] bufferCopy = new byte[length]; System.arraycopy(buffer, offset, bufferCopy, 0, length); - - mAudioTrackHandler.post(new Runnable() { - @Override - public void run() { - int playState = audioTrack.getPlayState(); - if (playState == AudioTrack.PLAYSTATE_STOPPED) { - if (DBG) Log.d(TAG, "AudioTrack stopped, restarting"); - audioTrack.play(); - } - // TODO: loop until all data is written? - if (DBG) Log.d(TAG, "AudioTrack.write()"); - int count = audioTrack.write(bufferCopy, 0, bufferCopy.length); - // The semantics of this change very slightly. Earlier, we would - // report an error immediately, Now we will return an error on - // the next API call, usually done( ) or another audioAvailable( ) - // call. - if (count < 0) { - mWriteErrorOccured = true; - } - } - }); - - return TextToSpeech.SUCCESS; + mToken.addBuffer(bufferCopy); + mAudioTrackHandler.enqueueSynthesisDataAvailable(mToken); } + + return TextToSpeech.SUCCESS; } @Override public int done() { if (DBG) Log.d(TAG, "done()"); + synchronized (mStateLock) { - if (mWriteErrorOccured) { - if (DBG) Log.d(TAG, "Error writing to audio track, count < 0"); - return TextToSpeech.ERROR; - } - if (mStopped) { - if (DBG) Log.d(TAG, "Request has been aborted."); + if (mDone) { + Log.w(TAG, "Duplicate call to done()"); return TextToSpeech.ERROR; } - if (mAudioTrack == null) { - Log.e(TAG, "done(): Not started"); + + mDone = true; + + if (mToken == null) { return TextToSpeech.ERROR; } - mDone = true; - cleanUp(); + + mAudioTrackHandler.enqueueSynthesisDone(mToken); } return TextToSpeech.SUCCESS; } @Override public void error() { - if (DBG) Log.d(TAG, "error()"); - synchronized (mStateLock) { - cleanUp(); - } + if (DBG) Log.d(TAG, "error() [will call stop]"); + stop(); } @Override public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, byte[] buffer, int offset, int length) { - if (DBG) { - Log.d(TAG, "completeAudioAvailable(" + sampleRateInHz + "," + audioFormat - + "," + channelCount + "byte[" + buffer.length + "]," - + offset + "," + length + ")"); + int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + return TextToSpeech.ERROR; + } + + int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(audioFormat); + if (bytesPerFrame < 0) { + Log.e(TAG, "Unsupported audio format :" + audioFormat); + return TextToSpeech.ERROR; } synchronized (mStateLock) { if (mStopped) { - if (DBG) Log.d(TAG, "Request has been aborted."); - return TextToSpeech.ERROR; - } - if (mAudioTrack != null) { - Log.e(TAG, "start() called before completeAudioAvailable()"); - cleanUp(); return TextToSpeech.ERROR; } + SynthesisMessageParams params = new SynthesisMessageParams( + mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, + mDispatcher); + params.addBuffer(buffer, offset, length); - int channelConfig = getChannelConfig(channelCount); - if (channelConfig < 0) { - Log.e(TAG, "Unsupported number of channels :" + channelCount); - cleanUp(); - return TextToSpeech.ERROR; - } - int bytesPerFrame = getBytesPerFrame(audioFormat); - if (bytesPerFrame < 0) { - Log.e(TAG, "Unsupported audio format :" + audioFormat); - cleanUp(); - return TextToSpeech.ERROR; - } - - mAudioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig, - audioFormat, buffer.length, AudioTrack.MODE_STATIC); - if (mAudioTrack == null) { - return TextToSpeech.ERROR; - } - - try { - mAudioTrack.write(buffer, offset, length); - setupVolume(mAudioTrack, mVolume, mPan); - mAudioTrack.play(); - blockUntilDone(mAudioTrack, bytesPerFrame, length); - mDone = true; - if (DBG) Log.d(TAG, "Wrote data to audio track succesfully : " + length); - } catch (IllegalStateException ex) { - Log.e(TAG, "Playback error", ex); - return TextToSpeech.ERROR; - } finally { - cleanUp(); - } + mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params); + mToken = params; } return TextToSpeech.SUCCESS; } - private void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) { - int lengthInFrames = length / bytesPerFrame; - int currentPosition = 0; - while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) { - long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / - audioTrack.getSampleRate(); - if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," + - " Playback position : " + currentPosition); - try { - Thread.sleep(estimatedTimeMs); - } catch (InterruptedException ie) { - break; - } - } - } - - private int getBytesPerFrame(int audioFormat) { - if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { - return 1; - } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { - return 2; - } - - return -1; - } - - private int getChannelConfig(int channelCount) { - if (channelCount == 1) { - return AudioFormat.CHANNEL_OUT_MONO; - } else if (channelCount == 2){ - return AudioFormat.CHANNEL_OUT_STEREO; - } - - return -1; - } - - private AudioTrack createStreamingAudioTrack(int sampleRateInHz, int audioFormat, - int channelCount) { - int channelConfig = getChannelConfig(channelCount); - - if (channelConfig < 0) { - Log.e(TAG, "Unsupported number of channels : " + channelCount); - return null; - } - - int minBufferSizeInBytes - = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); - int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); - AudioTrack audioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig, - audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); - if (audioTrack == null) { - return null; - } - - if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - audioTrack.release(); - return null; - } - setupVolume(audioTrack, mVolume, mPan); - return audioTrack; - } } diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilenceMessageParams.java new file mode 100644 index 0000000..eee8b68 --- /dev/null +++ b/core/java/android/speech/tts/SilenceMessageParams.java @@ -0,0 +1,43 @@ +/* + * 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.os.ConditionVariable; +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +class SilenceMessageParams extends MessageParams { + private final ConditionVariable mCondVar = new ConditionVariable(); + private final long mSilenceDurationMs; + + SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, long silenceDurationMs) { + super(dispatcher); + mSilenceDurationMs = silenceDurationMs; + } + + long getSilenceDurationMs() { + return mSilenceDurationMs; + } + + @Override + int getType() { + return TYPE_SILENCE; + } + + ConditionVariable getConditionVariable() { + return mCondVar; + } + +} diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java new file mode 100644 index 0000000..aabaa5a --- /dev/null +++ b/core/java/android/speech/tts/SynthesisMessageParams.java @@ -0,0 +1,92 @@ +/* + * 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.AudioTrack; +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +import java.util.LinkedList; + +/** + * Params required to play back a synthesis request. + */ +final class SynthesisMessageParams extends MessageParams { + final int mStreamType; + final int mSampleRateInHz; + final int mAudioFormat; + final int mChannelCount; + final float mVolume; + final float mPan; + + public volatile AudioTrack mAudioTrack; + + private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); + + SynthesisMessageParams(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan, UtteranceCompletedDispatcher dispatcher) { + super(dispatcher); + + mStreamType = streamType; + mSampleRateInHz = sampleRate; + mAudioFormat = audioFormat; + mChannelCount = channelCount; + mVolume = volume; + mPan = pan; + + // initially null. + mAudioTrack = null; + } + + @Override + int getType() { + return TYPE_SYNTHESIS; + } + + synchronized void addBuffer(byte[] buffer, int offset, int length) { + mDataBufferList.add(new ListEntry(buffer, offset, length)); + } + + synchronized void addBuffer(byte[] buffer) { + mDataBufferList.add(new ListEntry(buffer, 0, buffer.length)); + } + + synchronized ListEntry getNextBuffer() { + return mDataBufferList.poll(); + } + + + void setAudioTrack(AudioTrack audioTrack) { + mAudioTrack = audioTrack; + } + + AudioTrack getAudioTrack() { + return mAudioTrack; + } + + static final class ListEntry { + final byte[] mBytes; + final int mOffset; + final int mLength; + + ListEntry(byte[] bytes, int offset, int length) { + mBytes = bytes; + mOffset = offset; + mLength = length; + } + } +} + diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index ddd3252..e553f77 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -51,7 +51,10 @@ public abstract class TextToSpeechService extends Service { private static final String SYNTH_THREAD_NAME = "SynthThread"; private SynthHandler mSynthHandler; - private Handler mAudioTrackHandler; + // A thread and it's associated handler for playing back any audio + // associated with this TTS engine. Will handle all requests except synthesis + // to file requests, which occur on the synthesis thread. + private AudioPlaybackHandler mAudioPlaybackHandler; private CallbackMap mCallbacks; @@ -68,7 +71,7 @@ public abstract class TextToSpeechService extends Service { HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread"); audioTrackThread.start(); - mAudioTrackHandler = new Handler(audioTrackThread.getLooper()); + mAudioPlaybackHandler = new AudioPlaybackHandler(audioTrackThread.getLooper()); mCallbacks = new CallbackMap(); @@ -83,8 +86,8 @@ public abstract class TextToSpeechService extends Service { // Tell the synthesizer to stop mSynthHandler.quit(); - mAudioTrackHandler.getLooper().quit(); - + // Tell the audio playback thread to stop. + mAudioPlaybackHandler.quit(); // Unregister all callbacks. mCallbacks.kill(); @@ -236,13 +239,6 @@ public abstract class TextToSpeechService extends Service { super(looper); } - private void dispatchUtteranceCompleted(SpeechItem item) { - String utteranceId = item.getUtteranceId(); - if (!TextUtils.isEmpty(utteranceId)) { - mCallbacks.dispatchUtteranceCompleted(item.getCallingApp(), utteranceId); - } - } - private synchronized SpeechItem getCurrentSpeechItem() { return mCurrentSpeechItem; } @@ -286,9 +282,7 @@ public abstract class TextToSpeechService extends Service { @Override public void run() { setCurrentSpeechItem(speechItem); - if (speechItem.play() == TextToSpeech.SUCCESS) { - dispatchUtteranceCompleted(speechItem); - } + speechItem.play(); setCurrentSpeechItem(null); } }; @@ -318,14 +312,19 @@ public abstract class TextToSpeechService extends Service { if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { current.stop(); } + return TextToSpeech.SUCCESS; } } + interface UtteranceCompletedDispatcher { + public void dispatchUtteranceCompleted(); + } + /** * An item in the synth thread queue. */ - private static abstract class SpeechItem { + private abstract class SpeechItem implements UtteranceCompletedDispatcher { private final String mCallingApp; protected final Bundle mParams; private boolean mStarted = false; @@ -380,6 +379,13 @@ public abstract class TextToSpeechService extends Service { stopImpl(); } + public void dispatchUtteranceCompleted() { + final String utteranceId = getUtteranceId(); + if (!TextUtils.isEmpty(utteranceId)) { + mCallbacks.dispatchUtteranceCompleted(getCallingApp(), utteranceId); + } + } + protected abstract int playImpl(); protected abstract void stopImpl(); @@ -413,7 +419,7 @@ public abstract class TextToSpeechService extends Service { } } - private class SynthesisSpeechItem extends SpeechItem { + class SynthesisSpeechItem extends SpeechItem { private final String mText; private SynthesisRequest mSynthesisRequest; @@ -453,7 +459,8 @@ public abstract class TextToSpeechService extends Service { protected SynthesisRequest createSynthesisRequest() { return new PlaybackSynthesisRequest(mText, mParams, - getStreamType(), getVolume(), getPan(), mAudioTrackHandler); + getStreamType(), getVolume(), getPan(), mAudioPlaybackHandler, + this); } private void setRequestParams(SynthesisRequest request) { @@ -526,6 +533,15 @@ public abstract class TextToSpeechService extends Service { return new FileSynthesisRequest(getText(), mParams, mFile); } + @Override + protected int playImpl() { + int status = super.playImpl(); + if (status == TextToSpeech.SUCCESS) { + dispatchUtteranceCompleted(); + } + return status; + } + /** * Checks that the given file can be used for synthesis output. */ @@ -557,6 +573,7 @@ public abstract class TextToSpeechService extends Service { private class AudioSpeechItem extends SpeechItem { private final BlockingMediaPlayer mPlayer; + private AudioMessageParams mToken; public AudioSpeechItem(String callingApp, Bundle params, Uri uri) { super(callingApp, params); @@ -570,23 +587,26 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - return mPlayer.startAndWait() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + mToken = new AudioMessageParams(this, mPlayer); + mAudioPlaybackHandler.enqueueAudio(mToken); + return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - mPlayer.stop(); + if (mToken != null) { + mAudioPlaybackHandler.stop(mToken); + } } } private class SilenceSpeechItem extends SpeechItem { private final long mDuration; - private final ConditionVariable mDone; + private SilenceMessageParams mToken; public SilenceSpeechItem(String callingApp, Bundle params, long duration) { super(callingApp, params); mDuration = duration; - mDone = new ConditionVariable(); } @Override @@ -596,13 +616,16 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - boolean aborted = mDone.block(mDuration); - return aborted ? TextToSpeech.ERROR : TextToSpeech.SUCCESS; + mToken = new SilenceMessageParams(this, mDuration); + mAudioPlaybackHandler.enqueueSilence(mToken); + return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - mDone.open(); + if (mToken != null) { + mAudioPlaybackHandler.stop(mToken); + } } } |