Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4924fe38 authored by Narayan Kamath's avatar Narayan Kamath
Browse files

Implement QUEUE_FLUSH and QUEUE_DESTROY addition modes.

Note that this change is slightly more flexible, it defines
a priority for each playback request. We needn't really throw
away synthesis requests from other apps. We could just play
them after higher priority ones (for e.g from talkback).

Change-Id: I6c7dd9abe4871e87da08d9aaa4c55225a69078e5
parent 6276814a
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -20,8 +20,9 @@ import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;
class AudioMessageParams extends MessageParams {
    private final BlockingMediaPlayer mPlayer;

    AudioMessageParams(UtteranceCompletedDispatcher dispatcher, BlockingMediaPlayer player) {
        super(dispatcher);
    AudioMessageParams(UtteranceCompletedDispatcher dispatcher,
            String callingApp, BlockingMediaPlayer player) {
        super(dispatcher, callingApp);
        mPlayer = player;
    }

+229 −59
Original line number Diff line number Diff line
@@ -17,13 +17,13 @@ package android.speech.tts;

import android.media.AudioFormat;
import android.media.AudioTrack;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.speech.tts.SynthesisMessageParams.ListEntry;
import android.util.Log;

class AudioPlaybackHandler extends Handler {
import java.util.Iterator;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

class AudioPlaybackHandler {
    private static final String TAG = "TTS.AudioPlaybackHandler";
    private static final boolean DBG = false;

@@ -37,33 +37,31 @@ class AudioPlaybackHandler extends Handler {
    private static final int PLAY_AUDIO = 5;
    private static final int PLAY_SILENCE = 6;

    // Accessed by multiple threads, synchronized by "this".
    private MessageParams mCurrentParams;
    private static final int SHUTDOWN = -1;

    private static final int DEFAULT_PRIORITY = 1;
    private static final int HIGH_PRIORITY = 0;

    private final PriorityBlockingQueue<ListEntry> mQueue =
            new PriorityBlockingQueue<ListEntry>();
    private final Thread mHandlerThread;

    private final Object mStateLock = new Object();

    // Accessed by multiple threads, synchronized by "mStateLock".
    private MessageParams mCurrentParams = null;
    // Used only for book keeping and error detection.
    private SynthesisMessageParams mLastSynthesisRequest;
    private volatile SynthesisMessageParams mLastSynthesisRequest = null;
    // Used to order incoming messages in our priority queue.
    private final AtomicLong mSequenceIdCtr = new AtomicLong(0);

    AudioPlaybackHandler(Looper looper) {
        super(looper);
    }

    @Override
    public synchronized void handleMessage(Message msg) {
        if (msg.what == SYNTHESIS_START) {
            mCurrentParams = (SynthesisMessageParams) msg.obj;
            handleSynthesisStart(msg);
        } else if (msg.what == SYNTHESIS_DATA_AVAILABLE) {
            handleSynthesisDataAvailable(msg);
        } else if (msg.what == SYNTHESIS_DONE) {
            handleSynthesisDone(msg);
        } else if (msg.what == SYNTHESIS_COMPLETE_DATA_AVAILABLE) {
            handleSynthesisCompleteDataAvailable(msg);
        } else if (msg.what == PLAY_AUDIO) {
            handleAudio(msg);
        } else if (msg.what == PLAY_SILENCE) {
            handleSilence(msg);
    AudioPlaybackHandler() {
        mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread");
    }

        mCurrentParams = null;
    public void start() {
        mHandlerThread.start();
    }

    /**
@@ -72,63 +70,232 @@ class AudioPlaybackHandler extends Handler {
     * that is not guaranteed.
     */
    synchronized public void stop(MessageParams token) {
        removeCallbacksAndMessages(token);
        if (token == null) {
            return;
        }

        removeMessages(token);

        if (token.getType() == MessageParams.TYPE_SYNTHESIS) {
            sendMessageAtFrontOfQueue(obtainMessage(SYNTHESIS_DONE, token));
        } else if (token == mCurrentParams) {
            mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY));
        } else  {
            MessageParams current = getCurrentParams();

            if (current != null) {
                if (token.getType() == MessageParams.TYPE_AUDIO) {
                ((AudioMessageParams) mCurrentParams).getPlayer().stop();
                    ((AudioMessageParams) current).getPlayer().stop();
                } else if (token.getType() == MessageParams.TYPE_SILENCE) {
                ((SilenceMessageParams) mCurrentParams).getConditionVariable().open();
                    ((SilenceMessageParams) current).getConditionVariable().open();
                }
            }
        }
    }

    synchronized public void removePlaybackItems(String callingApp) {
        removeMessages(callingApp);
        stop(getCurrentParams());
    }

    synchronized public void removeAllItems() {
        removeAllMessages();
        stop(getCurrentParams());
    }

    /**
     * Shut down the audio playback thread.
     */
    synchronized public void quit() {
        if (mCurrentParams != null) {
            stop(mCurrentParams);
        }
        getLooper().quit();
        stop(getCurrentParams());
        mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY));
    }

    void enqueueSynthesisStart(SynthesisMessageParams token) {
        sendMessage(obtainMessage(SYNTHESIS_START, token));
        mQueue.add(new ListEntry(SYNTHESIS_START, token));
    }

    void enqueueSynthesisDataAvailable(SynthesisMessageParams token) {
        sendMessage(obtainMessage(SYNTHESIS_DATA_AVAILABLE, token));
        mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token));
    }

    void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) {
        sendMessage(obtainMessage(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token));
        mQueue.add(new ListEntry(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token));
    }

    void enqueueSynthesisDone(SynthesisMessageParams token) {
        sendMessage(obtainMessage(SYNTHESIS_DONE, token));
        mQueue.add(new ListEntry(SYNTHESIS_DONE, token));
    }

    void enqueueAudio(AudioMessageParams token) {
        sendMessage(obtainMessage(PLAY_AUDIO, token));
        mQueue.add(new ListEntry(PLAY_AUDIO, token));
    }

    void enqueueSilence(SilenceMessageParams token) {
        sendMessage(obtainMessage(PLAY_SILENCE, token));
        mQueue.add(new ListEntry(PLAY_SILENCE, token));
    }

    // -----------------------------------------
    // End of public API methods.
    // -----------------------------------------

    // -----------------------------------------
    // Methods for managing the message queue.
    // -----------------------------------------

    /*
     * The MessageLoop is a handler like implementation that
     * processes messages from a priority queue.
     */
    private final class MessageLoop implements Runnable {
        @Override
        public void run() {
            while (true) {
                ListEntry entry = null;
                try {
                    entry = mQueue.take();
                } catch (InterruptedException ie) {
                    return;
                }

                if (entry.mWhat == SHUTDOWN) {
                    if (DBG) Log.d(TAG, "MessageLoop : Shutting down");
                    return;
                }

                if (DBG) {
                    Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat
                            + " ,seqId : " + entry.mSequenceId);
                }

                setCurrentParams(entry.mMessage);
                handleMessage(entry);
                setCurrentParams(null);
            }
        }
    }

    /*
     * Remove all messages from the queue that contain the supplied token.
     * Note that the Iterator is thread safe, and other methods can safely
     * continue adding to the queue at this point.
     */
    synchronized private void removeMessages(MessageParams token) {
        if (token == null) {
            return;
        }

        Iterator<ListEntry> it = mQueue.iterator();

        while (it.hasNext()) {
            final ListEntry current = it.next();
            if (current.mMessage == token) {
                it.remove();
            }
        }
    }

    /*
     * Atomically clear the queue of all messages.
     */
    synchronized private void removeAllMessages() {
        mQueue.clear();
    }

    /*
     * Remove all messages that originate from a given calling app.
     */
    synchronized private void removeMessages(String callingApp) {
        Iterator<ListEntry> it = mQueue.iterator();

        while (it.hasNext()) {
            final ListEntry current = it.next();
            // The null check is to prevent us from removing control messages,
            // such as a shutdown message.
            if (current.mMessage != null &&
                    callingApp.equals(current.mMessage.getCallingApp())) {
                it.remove();
            }
        }
    }

    /*
     * An element of our priority queue of messages. Each message has a priority,
     * and a sequence id (defined by the order of enqueue calls). Among messages
     * with the same priority, messages that were received earlier win out.
     */
    private final class ListEntry implements Comparable<ListEntry> {
        final int mWhat;
        final MessageParams mMessage;
        final int mPriority;
        final long mSequenceId;

        private ListEntry(int what, MessageParams message) {
            this(what, message, DEFAULT_PRIORITY);
        }

        private ListEntry(int what, MessageParams message, int priority) {
            mWhat = what;
            mMessage = message;
            mPriority = priority;
            mSequenceId = mSequenceIdCtr.incrementAndGet();
        }

        @Override
        public int compareTo(ListEntry that) {
            if (that == this) {
                return 0;
            }

            // Note that this is always 0, 1 or -1.
            int priorityDiff = mPriority - that.mPriority;
            if (priorityDiff == 0) {
                // The == case cannot occur.
                return (mSequenceId < that.mSequenceId) ? -1 : 1;
            }

            return priorityDiff;
        }
    }

    private void setCurrentParams(MessageParams p) {
        synchronized (mStateLock) {
            mCurrentParams = p;
        }
    }

    private MessageParams getCurrentParams() {
        synchronized (mStateLock) {
            return mCurrentParams;
        }
    }

    // -----------------------------------------
    // Methods for dealing with individual messages, the methods
    // below do the actual work.
    // -----------------------------------------

    private void handleMessage(ListEntry entry) {
        final MessageParams msg = entry.mMessage;
        if (entry.mWhat == SYNTHESIS_START) {
            handleSynthesisStart(msg);
        } else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) {
            handleSynthesisDataAvailable(msg);
        } else if (entry.mWhat == SYNTHESIS_DONE) {
            handleSynthesisDone(msg);
        } else if (entry.mWhat == SYNTHESIS_COMPLETE_DATA_AVAILABLE) {
            handleSynthesisCompleteDataAvailable(msg);
        } else if (entry.mWhat == PLAY_AUDIO) {
            handleAudio(msg);
        } else if (entry.mWhat == PLAY_SILENCE) {
            handleSilence(msg);
        }
    }

    // Currently implemented as blocking the audio playback thread for the
    // specified duration. If a call to stop() is made, the thread
    // unblocks.
    private void handleSilence(Message msg) {
    private void handleSilence(MessageParams msg) {
        if (DBG) Log.d(TAG, "handleSilence()");
        SilenceMessageParams params = (SilenceMessageParams) msg.obj;
        SilenceMessageParams params = (SilenceMessageParams) msg;
        if (params.getSilenceDurationMs() > 0) {
            params.getConditionVariable().block(params.getSilenceDurationMs());
        }
@@ -137,9 +304,9 @@ class AudioPlaybackHandler extends Handler {
    }

    // Plays back audio from a given URI. No TTS engine involvement here.
    private void handleAudio(Message msg) {
    private void handleAudio(MessageParams msg) {
        if (DBG) Log.d(TAG, "handleAudio()");
        AudioMessageParams params = (AudioMessageParams) msg.obj;
        AudioMessageParams params = (AudioMessageParams) msg;
        // Note that the BlockingMediaPlayer spawns a separate thread.
        //
        // TODO: This can be avoided.
@@ -157,9 +324,9 @@ class AudioPlaybackHandler extends Handler {
    // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone
    // OR
    // handleSynthesisCompleteDataAvailable.
    private void handleSynthesisStart(Message msg) {
    private void handleSynthesisStart(MessageParams msg) {
        if (DBG) Log.d(TAG, "handleSynthesisStart()");
        final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj;
        final SynthesisMessageParams param = (SynthesisMessageParams) msg;

        // Oops, looks like the engine forgot to call done(). We go through
        // extra trouble to clean the data to prevent the AudioTrack resources
@@ -177,12 +344,14 @@ class AudioPlaybackHandler extends Handler {
                param.mStreamType, param.mSampleRateInHz, param.mAudioFormat,
                param.mChannelCount, param.mVolume, param.mPan);

        if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]");

        param.setAudioTrack(audioTrack);
    }

    // More data available to be flushed to the audio track.
    private void handleSynthesisDataAvailable(Message msg) {
        final SynthesisMessageParams param = (SynthesisMessageParams) msg.obj;
    private void handleSynthesisDataAvailable(MessageParams msg) {
        final SynthesisMessageParams param = (SynthesisMessageParams) msg;
        if (param.getAudioTrack() == null) {
            Log.w(TAG, "Error : null audio track in handleDataAvailable.");
            return;
@@ -194,7 +363,7 @@ class AudioPlaybackHandler extends Handler {
        }

        final AudioTrack audioTrack = param.getAudioTrack();
        final ListEntry bufferCopy = param.getNextBuffer();
        final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer();

        if (bufferCopy == null) {
            Log.e(TAG, "No buffers available to play.");
@@ -218,8 +387,8 @@ class AudioPlaybackHandler extends Handler {
        }
    }

    private void handleSynthesisDone(Message msg) {
        final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj;
    private void handleSynthesisDone(MessageParams msg) {
        final SynthesisMessageParams params = (SynthesisMessageParams) msg;
        handleSynthesisDone(params);
    }

@@ -233,6 +402,7 @@ class AudioPlaybackHandler extends Handler {
            if (audioTrack != null) {
                audioTrack.flush();
                audioTrack.stop();
                if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]");
                audioTrack.release();
            }
        } finally {
@@ -242,8 +412,8 @@ class AudioPlaybackHandler extends Handler {
        }
    }

    private void handleSynthesisCompleteDataAvailable(Message msg) {
        final SynthesisMessageParams params = (SynthesisMessageParams) msg.obj;
    private void handleSynthesisCompleteDataAvailable(MessageParams msg) {
        final SynthesisMessageParams params = (SynthesisMessageParams) msg;
        if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")");

        // Channel config and bytes per frame are checked before
@@ -251,7 +421,7 @@ class AudioPlaybackHandler extends Handler {
        int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount);
        int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat);

        ListEntry entry = params.getNextBuffer();
        SynthesisMessageParams.ListEntry entry = params.getNextBuffer();

        if (entry == null) {
            Log.w(TAG, "completeDataAvailable : No buffers available to play.");
+9 −3
Original line number Diff line number Diff line
@@ -18,19 +18,25 @@ package android.speech.tts;
import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher;

abstract class MessageParams {
    private final UtteranceCompletedDispatcher mDispatcher;

    static final int TYPE_SYNTHESIS = 1;
    static final int TYPE_AUDIO = 2;
    static final int TYPE_SILENCE = 3;

    MessageParams(UtteranceCompletedDispatcher dispatcher) {
    private final UtteranceCompletedDispatcher mDispatcher;
    private final String mCallingApp;

    MessageParams(UtteranceCompletedDispatcher dispatcher, String callingApp) {
        mDispatcher = dispatcher;
        mCallingApp = callingApp;
    }

    UtteranceCompletedDispatcher getDispatcher() {
        return mDispatcher;
    }

    String getCallingApp() {
        return mCallingApp;
    }

    abstract int getType();
}
+6 −4
Original line number Diff line number Diff line
@@ -64,15 +64,17 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
    private volatile boolean mDone = false;

    private final UtteranceCompletedDispatcher mDispatcher;
    private final String mCallingApp;

    PlaybackSynthesisCallback(int streamType, float volume, float pan,
            AudioPlaybackHandler audioTrackHandler,
            UtteranceCompletedDispatcher dispatcher) {
            AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher,
            String callingApp) {
        mStreamType = streamType;
        mVolume = volume;
        mPan = pan;
        mAudioTrackHandler = audioTrackHandler;
        mDispatcher = dispatcher;
        mCallingApp = callingApp;
    }

    @Override
@@ -122,7 +124,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
            }
            SynthesisMessageParams params = new SynthesisMessageParams(
                    mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
                    mDispatcher);
                    mDispatcher, mCallingApp);
            mAudioTrackHandler.enqueueSynthesisStart(params);

            mToken = params;
@@ -206,7 +208,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
            }
            SynthesisMessageParams params = new SynthesisMessageParams(
                    mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
                    mDispatcher);
                    mDispatcher, mCallingApp);
            params.addBuffer(buffer, offset, length);

            mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params);
+3 −2
Original line number Diff line number Diff line
@@ -22,8 +22,9 @@ class SilenceMessageParams extends MessageParams {
    private final ConditionVariable mCondVar = new ConditionVariable();
    private final long mSilenceDurationMs;

    SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, long silenceDurationMs) {
        super(dispatcher);
    SilenceMessageParams(UtteranceCompletedDispatcher dispatcher,
            String callingApp, long silenceDurationMs) {
        super(dispatcher, callingApp);
        mSilenceDurationMs = silenceDurationMs;
    }

Loading