content: /*
   * 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.annotation.NonNull;
  import android.media.AudioFormat;
  import android.speech.tts.TextToSpeechService.AudioOutputParams;
  import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
  import android.util.Log;
  
  /**
   * Speech synthesis request that plays the audio as it is received.
   */
  class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
  
      private static final String TAG = "PlaybackSynthesisRequest";
      private static final boolean DBG = false;
  
      private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
  
      private final AudioOutputParams mAudioParams;
  
      /**
       * 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 SynthesisPlaybackQueueItem mItem = null;
  
      private volatile boolean mDone = false;
  
      /** Status code of synthesis */
      protected int mStatusCode;
  
      private final UtteranceProgressDispatcher mDispatcher;
      private final Object mCallerIdentity;
      private final AbstractEventLogger mLogger;
  
      PlaybackSynthesisCallback(@NonNull AudioOutputParams audioParams,
              @NonNull AudioPlaybackHandler audioTrackHandler,
              @NonNull UtteranceProgressDispatcher dispatcher, @NonNull Object callerIdentity,
              @NonNull AbstractEventLogger logger, boolean clientIsUsingV2) {
          super(clientIsUsingV2);
          mAudioParams = audioParams;
          mAudioTrackHandler = audioTrackHandler;
          mDispatcher = dispatcher;
          mCallerIdentity = callerIdentity;
          mLogger = logger;
          mStatusCode = TextToSpeech.SUCCESS;
      }
  
      @Override
      void stop() {
          if (DBG) Log.d(TAG, "stop()");
  
          SynthesisPlaybackQueueItem item;
          synchronized (mStateLock) {
              if (mDone) {
                  return;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  Log.w(TAG, "stop() called twice");
                  return;
              }
  
              item = mItem;
              mStatusCode = TextToSpeech.STOPPED;
          }
  
          if (item != null) {
              // This might result in the synthesis thread being woken up, at which
              // 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).
              item.stop(TextToSpeech.STOPPED);
          } else {
              // This happens when stop() or error() were called before start() was.
  
              // In all other cases, mAudioTrackHandler.stop() will
              // result in onSynthesisDone being called, and we will
              // write data there.
              mLogger.onCompleted(TextToSpeech.STOPPED);
              mDispatcher.dispatchOnStop();
          }
      }
  
      @Override
      public int getMaxBufferSize() {
          // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be
          // a safe buffer size to pass in.
          return MIN_AUDIO_BUFFER_SIZE;
      }
  
      @Override
      public boolean hasStarted() {
          synchronized (mStateLock) {
              return mItem != null;
          }
      }
  
      @Override
      public boolean hasFinished() {
          synchronized (mStateLock) {
              return mDone;
          }
      }
  
      @Override
      public int start(int sampleRateInHz, int audioFormat, int channelCount) {
          if (DBG) Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + "," + channelCount
                  + ")");
          if (audioFormat != AudioFormat.ENCODING_PCM_8BIT &&
              audioFormat != AudioFormat.ENCODING_PCM_16BIT &&
              audioFormat != AudioFormat.ENCODING_PCM_FLOAT) {
              Log.w(TAG, "Audio format encoding " + audioFormat + " not supported. Please use one " +
                         "of AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT or " +
                         "AudioFormat.ENCODING_PCM_FLOAT");
          }
          mDispatcher.dispatchOnBeginSynthesis(sampleRateInHz, audioFormat, channelCount);
  
          int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
  
          synchronized (mStateLock) {
              if (channelConfig == 0) {
                  Log.e(TAG, "Unsupported number of channels :" + channelCount);
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
                  return TextToSpeech.ERROR;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  if (DBG) Log.d(TAG, "stop() called before start(), returning.");
                  return errorCodeOnStop();
              }
              if (mStatusCode != TextToSpeech.SUCCESS) {
                  if (DBG) Log.d(TAG, "Error was raised");
                  return TextToSpeech.ERROR;
              }
              if (mItem != null) {
                  Log.e(TAG, "Start called twice");
                  return TextToSpeech.ERROR;
              }
              SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
                      mAudioParams, sampleRateInHz, audioFormat, channelCount,
                      mDispatcher, mCallerIdentity, mLogger);
              mAudioTrackHandler.enqueue(item);
              mItem = item;
          }
  
          return TextToSpeech.SUCCESS;
      }
  
      @Override
      public int audioAvailable(byte[] buffer, int offset, int length) {
          if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length
                  + ")");
  
          if (length > getMaxBufferSize() || length <= 0) {
              throw new IllegalArgumentException("buffer is too large or of zero length (" +
                      + length + " bytes)");
          }
  
          SynthesisPlaybackQueueItem item = null;
          synchronized (mStateLock) {
              if (mItem == null) {
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
                  return TextToSpeech.ERROR;
              }
              if (mStatusCode != TextToSpeech.SUCCESS) {
                  if (DBG) Log.d(TAG, "Error was raised");
                  return TextToSpeech.ERROR;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  return errorCodeOnStop();
              }
              item = mItem;
          }
  
          // Sigh, another copy.
          final byte[] bufferCopy = new byte[length];
          System.arraycopy(buffer, offset, bufferCopy, 0, length);
          mDispatcher.dispatchOnAudioAvailable(bufferCopy);
  
          // Might block on mItem.this, if there are too many buffers waiting to
          // be consumed.
          try {
              item.put(bufferCopy);
          } catch (InterruptedException ie) {
              synchronized (mStateLock) {
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
                  return TextToSpeech.ERROR;
              }
          }
  
          mLogger.onEngineDataReceived();
          return TextToSpeech.SUCCESS;
      }
  
      @Override
      public int done() {
          if (DBG) Log.d(TAG, "done()");
  
          int statusCode = 0;
          SynthesisPlaybackQueueItem item = null;
          synchronized (mStateLock) {
              if (mDone) {
                  Log.w(TAG, "Duplicate call to done()");
                  // Not an error that would prevent synthesis. Hence no
                  // setStatusCode
                  return TextToSpeech.ERROR;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  if (DBG) Log.d(TAG, "Request has been aborted.");
                  return errorCodeOnStop();
              }
              mDone = true;
  
              if (mItem == null) {
                  // .done() was called before .start. Treat it as successful synthesis
                  // for a client, despite service bad implementation.
                  Log.w(TAG, "done() was called before start() call");
                  if (mStatusCode == TextToSpeech.SUCCESS) {
                      mDispatcher.dispatchOnSuccess();
                  } else {
                      mDispatcher.dispatchOnError(mStatusCode);
                  }
                  mLogger.onEngineComplete();
                  return TextToSpeech.ERROR;
              }
  
              item = mItem;
              statusCode = mStatusCode;
          }
  
          // Signal done or error to item
          if (statusCode == TextToSpeech.SUCCESS) {
              item.done();
          } else {
              item.stop(statusCode);
          }
          mLogger.onEngineComplete();
          return TextToSpeech.SUCCESS;
      }
  
      @Override
      public void error() {
          error(TextToSpeech.ERROR_SYNTHESIS);
      }
  
      @Override
      public void error(int errorCode) {
          if (DBG) Log.d(TAG, "error() [will call stop]");
          synchronized (mStateLock) {
              if (mDone) {
                  return;
              }
              mStatusCode = errorCode;
          }
      }
  
      @Override
      public void rangeStart(int markerInFrames, int start, int end) {
          if (mItem == null) {
              Log.e(TAG, "mItem is null");
              return;
          }
          mItem.rangeStart(markerInFrames, start, end);
      }
  }
  