Home | History | Annotate | Download | only in tts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package android.speech.tts;
     17 
     18 import android.media.AudioTrack;
     19 import android.speech.tts.TextToSpeechService.AudioOutputParams;
     20 import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
     21 import android.util.Log;
     22 
     23 import java.util.LinkedList;
     24 import java.util.concurrent.ConcurrentLinkedQueue;
     25 import java.util.concurrent.atomic.AtomicInteger;
     26 import java.util.concurrent.locks.Condition;
     27 import java.util.concurrent.locks.Lock;
     28 import java.util.concurrent.locks.ReentrantLock;
     29 
     30 /**
     31  * Manages the playback of a list of byte arrays representing audio data that are queued by the
     32  * engine to an audio track.
     33  */
     34 final class SynthesisPlaybackQueueItem extends PlaybackQueueItem
     35         implements AudioTrack.OnPlaybackPositionUpdateListener {
     36     private static final String TAG = "TTS.SynthQueueItem";
     37     private static final boolean DBG = false;
     38 
     39     /**
     40      * Maximum length of audio we leave unconsumed by the audio track.
     41      * Calls to {@link #put(byte[])} will block until we have less than
     42      * this amount of audio left to play back.
     43      */
     44     private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
     45 
     46     /**
     47      * Guards accesses to mDataBufferList and mUnconsumedBytes.
     48      */
     49     private final Lock mListLock = new ReentrantLock();
     50     private final Condition mReadReady = mListLock.newCondition();
     51     private final Condition mNotFull = mListLock.newCondition();
     52 
     53     // Guarded by mListLock.
     54     private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
     55     // Guarded by mListLock.
     56     private int mUnconsumedBytes;
     57 
     58     /*
     59      * While mStopped and mIsError can be written from any thread, mDone is written
     60      * only from the synthesis thread. All three variables are read from the
     61      * audio playback thread.
     62      */
     63     private volatile boolean mStopped;
     64     private volatile boolean mDone;
     65     private volatile int mStatusCode;
     66 
     67     private final BlockingAudioTrack mAudioTrack;
     68     private final AbstractEventLogger mLogger;
     69 
     70     // Stores a queue of markers. When the marker in front is reached the client is informed and we
     71     // wait for the next one.
     72     private ConcurrentLinkedQueue<ProgressMarker> markerList = new ConcurrentLinkedQueue<>();
     73 
     74     private static final int NOT_RUN = 0;
     75     private static final int RUN_CALLED = 1;
     76     private static final int STOP_CALLED = 2;
     77     private final AtomicInteger mRunState = new AtomicInteger(NOT_RUN);
     78 
     79     SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate,
     80             int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher,
     81             Object callerIdentity, AbstractEventLogger logger) {
     82         super(dispatcher, callerIdentity);
     83 
     84         mUnconsumedBytes = 0;
     85 
     86         mStopped = false;
     87         mDone = false;
     88         mStatusCode = TextToSpeech.SUCCESS;
     89 
     90         mAudioTrack = new BlockingAudioTrack(audioParams, sampleRate, audioFormat, channelCount);
     91         mLogger = logger;
     92     }
     93 
     94 
     95     @Override
     96     public void run() {
     97         if (!mRunState.compareAndSet(NOT_RUN, RUN_CALLED)) {
     98             // stop() was already called before run(). Do nothing and just finish.
     99             return;
    100         }
    101 
    102         final UtteranceProgressDispatcher dispatcher = getDispatcher();
    103         dispatcher.dispatchOnStart();
    104 
    105         if (!mAudioTrack.init()) {
    106             dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);
    107             return;
    108         }
    109 
    110         mAudioTrack.setPlaybackPositionUpdateListener(this);
    111         // Ensure we set the first marker if there is one.
    112         updateMarker();
    113 
    114         try {
    115             byte[] buffer = null;
    116 
    117             // take() will block until:
    118             //
    119             // (a) there is a buffer available to tread. In which case
    120             // a non null value is returned.
    121             // OR (b) stop() is called in which case it will return null.
    122             // OR (c) done() is called in which case it will return null.
    123             while ((buffer = take()) != null) {
    124                 mAudioTrack.write(buffer);
    125                 mLogger.onAudioDataWritten();
    126             }
    127 
    128         } catch (InterruptedException ie) {
    129             if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
    130         }
    131 
    132         mAudioTrack.waitAndRelease();
    133 
    134         dispatchEndStatus();
    135     }
    136 
    137     private void dispatchEndStatus() {
    138         final UtteranceProgressDispatcher dispatcher = getDispatcher();
    139 
    140         if (mStatusCode == TextToSpeech.SUCCESS) {
    141             dispatcher.dispatchOnSuccess();
    142         } else if(mStatusCode == TextToSpeech.STOPPED) {
    143             dispatcher.dispatchOnStop();
    144         } else {
    145             dispatcher.dispatchOnError(mStatusCode);
    146         }
    147 
    148         mLogger.onCompleted(mStatusCode);
    149     }
    150 
    151     @Override
    152     void stop(int statusCode) {
    153         try {
    154             mListLock.lock();
    155 
    156             // Update our internal state.
    157             mStopped = true;
    158             mStatusCode = statusCode;
    159 
    160             // Wake up the synthesis thread if it was waiting on put(). Its
    161             // buffers will no longer be copied since mStopped is true. The
    162             // PlaybackSynthesisCallback that this synthesis corresponds to
    163             // would also have been stopped, and so all calls to
    164             // Callback.onDataAvailable( ) will return errors too.
    165             mNotFull.signal();
    166 
    167             if (mRunState.getAndSet(STOP_CALLED) == NOT_RUN) {
    168                 // Dispatch the status code and just finish. Signaling audio
    169                 // playback is not necessary because run() hasn't started.
    170                 dispatchEndStatus();
    171                 return;
    172             }
    173 
    174             // Wake up the audio playback thread if it was waiting on take().
    175             // take() will return null since mStopped was true, and will then
    176             // break out of the data write loop.
    177             mReadReady.signal();
    178         } finally {
    179             mListLock.unlock();
    180         }
    181 
    182         // Stop the underlying audio track. This will stop sending
    183         // data to the mixer and discard any pending buffers that the
    184         // track holds.
    185         mAudioTrack.stop();
    186     }
    187 
    188     void done() {
    189         try {
    190             mListLock.lock();
    191 
    192             // Update state.
    193             mDone = true;
    194 
    195             // Unblocks the audio playback thread if it was waiting on take()
    196             // after having consumed all available buffers. It will then return
    197             // null and leave the write loop.
    198             mReadReady.signal();
    199 
    200             // Just so that engines that try to queue buffers after
    201             // calling done() don't block the synthesis thread forever. Ideally
    202             // this should be called from the same thread as put() is, and hence
    203             // this call should be pointless.
    204             mNotFull.signal();
    205         } finally {
    206             mListLock.unlock();
    207         }
    208     }
    209 
    210     /** Convenience class for passing around TTS markers. */
    211     private class ProgressMarker {
    212         // The index in frames of this marker.
    213         public final int frames;
    214         // The start index in the text of the utterance.
    215         public final int start;
    216         // The end index (exclusive) in the text of the utterance.
    217         public final int end;
    218 
    219         public ProgressMarker(int frames, int start, int end) {
    220             this.frames = frames;
    221             this.start = start;
    222             this.end = end;
    223         }
    224     }
    225 
    226     /** Set a callback for the first marker in the queue. */
    227     void updateMarker() {
    228         ProgressMarker marker = markerList.peek();
    229         if (marker != null) {
    230             // Zero is used to disable the marker. The documentation recommends to use a non-zero
    231             // position near zero such as 1.
    232             int markerInFrames = marker.frames == 0 ? 1 : marker.frames;
    233             mAudioTrack.setNotificationMarkerPosition(markerInFrames);
    234         }
    235     }
    236 
    237     /** Informs us that at markerInFrames, the range between start and end is about to be spoken. */
    238     void rangeStart(int markerInFrames, int start, int end) {
    239         markerList.add(new ProgressMarker(markerInFrames, start, end));
    240         updateMarker();
    241     }
    242 
    243     @Override
    244     public void onMarkerReached(AudioTrack track) {
    245         ProgressMarker marker = markerList.poll();
    246         if (marker == null) {
    247             Log.e(TAG, "onMarkerReached reached called but no marker in queue");
    248             return;
    249         }
    250         // Inform the client.
    251         getDispatcher().dispatchOnRangeStart(marker.start, marker.end, marker.frames);
    252         // Listen for the next marker.
    253         // It's ok if this marker is in the past, in that case onMarkerReached will be called again.
    254         updateMarker();
    255     }
    256 
    257     @Override
    258     public void onPeriodicNotification(AudioTrack track) {}
    259 
    260     void put(byte[] buffer) throws InterruptedException {
    261         try {
    262             mListLock.lock();
    263             long unconsumedAudioMs = 0;
    264 
    265             while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
    266                     MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
    267                 mNotFull.await();
    268             }
    269 
    270             // Don't bother queueing the buffer if we've stopped. The playback thread
    271             // would have woken up when stop() is called (if it was blocked) and will
    272             // proceed to leave the write loop since take() will return null when
    273             // stopped.
    274             if (mStopped) {
    275                 return;
    276             }
    277 
    278             mDataBufferList.add(new ListEntry(buffer));
    279             mUnconsumedBytes += buffer.length;
    280             mReadReady.signal();
    281         } finally {
    282             mListLock.unlock();
    283         }
    284     }
    285 
    286     private byte[] take() throws InterruptedException {
    287         try {
    288             mListLock.lock();
    289 
    290             // Block if there are no available buffers, and stop() has not
    291             // been called and done() has not been called.
    292             while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
    293                 mReadReady.await();
    294             }
    295 
    296             // If stopped, return null so that we can exit the playback loop
    297             // as soon as possible.
    298             if (mStopped) {
    299                 return null;
    300             }
    301 
    302             // Remove the first entry from the queue.
    303             ListEntry entry = mDataBufferList.poll();
    304 
    305             // This is the normal playback loop exit case, when done() was
    306             // called. (mDone will be true at this point).
    307             if (entry == null) {
    308                 return null;
    309             }
    310 
    311             mUnconsumedBytes -= entry.mBytes.length;
    312             // Unblock the waiting writer. We use signal() and not signalAll()
    313             // because there will only be one thread waiting on this (the
    314             // Synthesis thread).
    315             mNotFull.signal();
    316 
    317             return entry.mBytes;
    318         } finally {
    319             mListLock.unlock();
    320         }
    321     }
    322 
    323     static final class ListEntry {
    324         final byte[] mBytes;
    325 
    326         ListEntry(byte[] bytes) {
    327             mBytes = bytes;
    328         }
    329     }
    330 }
    331