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.UtteranceProgressDispatcher;
  import android.util.Log;
  
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.nio.channels.FileChannel;
  
  /**
   * Speech synthesis request that writes the audio to a WAV file.
   */
  class FileSynthesisCallback extends AbstractSynthesisCallback {
  
      private static final String TAG = "FileSynthesisRequest";
      private static final boolean DBG = false;
  
      private static final int MAX_AUDIO_BUFFER_SIZE = 8192;
  
      private static final int WAV_HEADER_LENGTH = 44;
      private static final short WAV_FORMAT_PCM = 0x0001;
  
      private final Object mStateLock = new Object();
  
      private int mSampleRateInHz;
      private int mAudioFormat;
      private int mChannelCount;
  
      private FileChannel mFileChannel;
  
      private final UtteranceProgressDispatcher mDispatcher;
  
      private boolean mStarted = false;
      private boolean mDone = false;
  
      /** Status code of synthesis */
      protected int mStatusCode;
  
      FileSynthesisCallback(@NonNull FileChannel fileChannel,
              @NonNull UtteranceProgressDispatcher dispatcher, boolean clientIsUsingV2) {
          super(clientIsUsingV2);
          mFileChannel = fileChannel;
          mDispatcher = dispatcher;
          mStatusCode = TextToSpeech.SUCCESS;
      }
  
      @Override
      void stop() {
          synchronized (mStateLock) {
              if (mDone) {
                  return;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  return;
              }
  
              mStatusCode = TextToSpeech.STOPPED;
              cleanUp();
              mDispatcher.dispatchOnStop();
          }
      }
  
      /**
       * Must be called while holding the monitor on {@link #mStateLock}.
       */
      private void cleanUp() {
          closeFile();
      }
  
      /**
       * Must be called while holding the monitor on {@link #mStateLock}.
       */
      private void closeFile() {
          // File will be closed by the SpeechItem in the speech service.
          mFileChannel = null;
      }
  
      @Override
      public int getMaxBufferSize() {
          return MAX_AUDIO_BUFFER_SIZE;
      }
  
      @Override
      public int start(int sampleRateInHz, int audioFormat, int channelCount) {
          if (DBG) {
              Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat
                      + "," + channelCount + ")");
          }
          if (audioFormat != AudioFormat.ENCODING_PCM_8BIT &&
              audioFormat != AudioFormat.ENCODING_PCM_16BIT &&
              audioFormat != AudioFormat.ENCODING_PCM_FLOAT) {
              Log.e(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);
  
          FileChannel fileChannel = null;
          synchronized (mStateLock) {
              if (mStatusCode == TextToSpeech.STOPPED) {
                  if (DBG) Log.d(TAG, "Request has been aborted.");
                  return errorCodeOnStop();
              }
              if (mStatusCode != TextToSpeech.SUCCESS) {
                  if (DBG) Log.d(TAG, "Error was raised");
                  return TextToSpeech.ERROR;
              }
              if (mStarted) {
                  Log.e(TAG, "Start called twice");
                  return TextToSpeech.ERROR;
              }
              mStarted = true;
              mSampleRateInHz = sampleRateInHz;
              mAudioFormat = audioFormat;
              mChannelCount = channelCount;
  
              mDispatcher.dispatchOnStart();
              fileChannel = mFileChannel;
          }
  
          try {
              fileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH));
                  return TextToSpeech.SUCCESS;
          } catch (IOException ex) {
              Log.e(TAG, "Failed to write wav header to output file descriptor", ex);
              synchronized (mStateLock) {
                  cleanUp();
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
              }
              return TextToSpeech.ERROR;
          }
      }
  
      @Override
      public int audioAvailable(byte[] buffer, int offset, int length) {
          if (DBG) {
              Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset
                      + "," + length + ")");
          }
          FileChannel fileChannel = null;
          synchronized (mStateLock) {
              if (mStatusCode == TextToSpeech.STOPPED) {
                  if (DBG) Log.d(TAG, "Request has been aborted.");
                  return errorCodeOnStop();
              }
              if (mStatusCode != TextToSpeech.SUCCESS) {
                  if (DBG) Log.d(TAG, "Error was raised");
                  return TextToSpeech.ERROR;
              }
              if (mFileChannel == null) {
                  Log.e(TAG, "File not open");
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
                  return TextToSpeech.ERROR;
              }
              if (!mStarted) {
                  Log.e(TAG, "Start method was not called");
                  return TextToSpeech.ERROR;
              }
              fileChannel = mFileChannel;
          }
  
          final byte[] bufferCopy = new byte[length];
          System.arraycopy(buffer, offset, bufferCopy, 0, length);
          mDispatcher.dispatchOnAudioAvailable(bufferCopy);
  
          try {
              fileChannel.write(ByteBuffer.wrap(buffer,  offset,  length));
              return TextToSpeech.SUCCESS;
          } catch (IOException ex) {
              Log.e(TAG, "Failed to write to output file descriptor", ex);
              synchronized (mStateLock) {
                  cleanUp();
                  mStatusCode = TextToSpeech.ERROR_OUTPUT;
              }
              return TextToSpeech.ERROR;
          }
      }
  
      @Override
      public int done() {
          if (DBG) Log.d(TAG, "FileSynthesisRequest.done()");
          FileChannel fileChannel = null;
  
          int sampleRateInHz = 0;
          int audioFormat = 0;
          int channelCount = 0;
  
          synchronized (mStateLock) {
              if (mDone) {
                  Log.w(TAG, "Duplicate call to done()");
                  // This is not an error that would prevent synthesis. Hence no
                  // setStatusCode is set.
                  return TextToSpeech.ERROR;
              }
              if (mStatusCode == TextToSpeech.STOPPED) {
                  if (DBG) Log.d(TAG, "Request has been aborted.");
                  return errorCodeOnStop();
              }
              if (mStatusCode != TextToSpeech.SUCCESS && mStatusCode != TextToSpeech.STOPPED) {
                  mDispatcher.dispatchOnError(mStatusCode);
                  return TextToSpeech.ERROR;
              }
              if (mFileChannel == null) {
                  Log.e(TAG, "File not open");
                  return TextToSpeech.ERROR;
              }
              mDone = true;
              fileChannel = mFileChannel;
              sampleRateInHz = mSampleRateInHz;
              audioFormat = mAudioFormat;
              channelCount = mChannelCount;
          }
  
          try {
              // Write WAV header at start of file
              fileChannel.position(0);
              int dataLength = (int) (fileChannel.size() - WAV_HEADER_LENGTH);
              fileChannel.write(
                      makeWavHeader(sampleRateInHz, audioFormat, channelCount, dataLength));
  
              synchronized (mStateLock) {
                  closeFile();
                  mDispatcher.dispatchOnSuccess();
                  return TextToSpeech.SUCCESS;
              }
          } catch (IOException ex) {
              Log.e(TAG, "Failed to write to output file descriptor", ex);
              synchronized (mStateLock) {
                  cleanUp();
              }
              return TextToSpeech.ERROR;
          }
      }
  
      @Override
      public void error() {
          error(TextToSpeech.ERROR_SYNTHESIS);
      }
  
      @Override
      public void error(int errorCode) {
          if (DBG) Log.d(TAG, "FileSynthesisRequest.error()");
          synchronized (mStateLock) {
              if (mDone) {
                  return;
              }
              cleanUp();
              mStatusCode = errorCode;
          }
      }
  
      @Override
      public boolean hasStarted() {
          synchronized (mStateLock) {
              return mStarted;
          }
      }
  
      @Override
      public boolean hasFinished() {
          synchronized (mStateLock) {
              return mDone;
          }
      }
  
      private ByteBuffer makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount,
              int dataLength) {
          int sampleSizeInBytes = AudioFormat.getBytesPerSample(audioFormat);
          int byteRate = sampleRateInHz * sampleSizeInBytes * channelCount;
          short blockAlign = (short) (sampleSizeInBytes * channelCount);
          short bitsPerSample = (short) (sampleSizeInBytes * 8);
  
          byte[] headerBuf = new byte[WAV_HEADER_LENGTH];
          ByteBuffer header = ByteBuffer.wrap(headerBuf);
          header.order(ByteOrder.LITTLE_ENDIAN);
  
          header.put(new byte[]{ 'R', 'I', 'F', 'F' });
          header.putInt(dataLength + WAV_HEADER_LENGTH - 8);  // RIFF chunk size
          header.put(new byte[]{ 'W', 'A', 'V', 'E' });
          header.put(new byte[]{ 'f', 'm', 't', ' ' });
          header.putInt(16);  // size of fmt chunk
          header.putShort(WAV_FORMAT_PCM);
          header.putShort((short) channelCount);
          header.putInt(sampleRateInHz);
          header.putInt(byteRate);
          header.putShort(blockAlign);
          header.putShort(bitsPerSample);
          header.put(new byte[]{ 'd', 'a', 't', 'a' });
          header.putInt(dataLength);
          header.flip();
  
          return header;
      }
  
      @Override
      public void rangeStart(int markerInFrames, int start, int end) {
          mDispatcher.dispatchOnRangeStart(markerInFrames, start, end);
      }
  }
  