content: /*
   * Copyright (C) 2018 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 com.google.android.exoplayer2.audio;
  
  import static java.lang.Math.min;
  
  import androidx.annotation.Nullable;
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.util.Assertions;
  import com.google.android.exoplayer2.util.Log;
  import com.google.android.exoplayer2.util.Util;
  import java.io.IOException;
  import java.io.RandomAccessFile;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  
  /**
   * Audio processor that outputs its input unmodified and also outputs its input to a given sink.
   * This is intended to be used for diagnostics and debugging.
   *
   * <p>This audio processor can be inserted into the audio processor chain to access audio data
   * before/after particular processing steps have been applied. For example, to get audio output
   * after playback speed adjustment and silence skipping have been applied it is necessary to pass a
   * custom {@link AudioProcessorChain} when creating the audio sink, and include this audio processor
   * after all other audio processors.
   */
  public final class TeeAudioProcessor extends BaseAudioProcessor {
  
    /** A sink for audio buffers handled by the audio processor. */
    public interface AudioBufferSink {
  
      /** Called when the audio processor is flushed with a format of subsequent input. */
      void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding);
  
      /**
       * Called when data is written to the audio processor.
       *
       * @param buffer A read-only buffer containing input which the audio processor will handle.
       */
      void handleBuffer(ByteBuffer buffer);
    }
  
    private final AudioBufferSink audioBufferSink;
  
    /**
     * Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}.
     *
     * @param audioBufferSink The audio buffer sink that will receive input queued to this audio
     *     processor.
     */
    public TeeAudioProcessor(AudioBufferSink audioBufferSink) {
      this.audioBufferSink = Assertions.checkNotNull(audioBufferSink);
    }
  
    @Override
    public AudioFormat onConfigure(AudioFormat inputAudioFormat) {
      // This processor is always active (if passed to the sink) and outputs its input.
      return inputAudioFormat;
    }
  
    @Override
    public void queueInput(ByteBuffer inputBuffer) {
      int remaining = inputBuffer.remaining();
      if (remaining == 0) {
        return;
      }
      audioBufferSink.handleBuffer(inputBuffer.asReadOnlyBuffer());
      replaceOutputBuffer(remaining).put(inputBuffer).flip();
    }
  
    @Override
    protected void onFlush() {
      flushSinkIfActive();
    }
  
    @Override
    protected void onQueueEndOfStream() {
      flushSinkIfActive();
    }
  
    @Override
    protected void onReset() {
      flushSinkIfActive();
    }
  
    private void flushSinkIfActive() {
      if (isActive()) {
        audioBufferSink.flush(
            inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding);
      }
    }
  
    /**
     * A sink for audio buffers that writes output audio as .wav files with a given path prefix. When
     * new audio data is handled after flushing the audio processor, a counter is incremented and its
     * value is appended to the output file name.
     *
     * <p>Note: if writing to external storage it's necessary to grant the {@code
     * WRITE_EXTERNAL_STORAGE} permission.
     */
    public static final class WavFileAudioBufferSink implements AudioBufferSink {
  
      private static final String TAG = "WaveFileAudioBufferSink";
  
      private static final int FILE_SIZE_MINUS_8_OFFSET = 4;
      private static final int FILE_SIZE_MINUS_44_OFFSET = 40;
      private static final int HEADER_LENGTH = 44;
  
      private final String outputFileNamePrefix;
      private final byte[] scratchBuffer;
      private final ByteBuffer scratchByteBuffer;
  
      private int sampleRateHz;
      private int channelCount;
      private @C.PcmEncoding int encoding;
      @Nullable private RandomAccessFile randomAccessFile;
      private int counter;
      private int bytesWritten;
  
      /**
       * Creates a new audio buffer sink that writes to .wav files with the given prefix.
       *
       * @param outputFileNamePrefix The prefix for output files.
       */
      public WavFileAudioBufferSink(String outputFileNamePrefix) {
        this.outputFileNamePrefix = outputFileNamePrefix;
        scratchBuffer = new byte[1024];
        scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN);
      }
  
      @Override
      public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
        try {
          reset();
        } catch (IOException e) {
          Log.e(TAG, "Error resetting", e);
        }
        this.sampleRateHz = sampleRateHz;
        this.channelCount = channelCount;
        this.encoding = encoding;
      }
  
      @Override
      public void handleBuffer(ByteBuffer buffer) {
        try {
          maybePrepareFile();
          writeBuffer(buffer);
        } catch (IOException e) {
          Log.e(TAG, "Error writing data", e);
        }
      }
  
      private void maybePrepareFile() throws IOException {
        if (randomAccessFile != null) {
          return;
        }
        RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw");
        writeFileHeader(randomAccessFile);
        this.randomAccessFile = randomAccessFile;
        bytesWritten = HEADER_LENGTH;
      }
  
      private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException {
        // Write the start of the header as big endian data.
        randomAccessFile.writeInt(WavUtil.RIFF_FOURCC);
        randomAccessFile.writeInt(-1);
        randomAccessFile.writeInt(WavUtil.WAVE_FOURCC);
        randomAccessFile.writeInt(WavUtil.FMT_FOURCC);
  
        // Write the rest of the header as little endian data.
        scratchByteBuffer.clear();
        scratchByteBuffer.putInt(16);
        scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding));
        scratchByteBuffer.putShort((short) channelCount);
        scratchByteBuffer.putInt(sampleRateHz);
        int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
        scratchByteBuffer.putInt(bytesPerSample * sampleRateHz);
        scratchByteBuffer.putShort((short) bytesPerSample);
        scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount));
        randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position());
  
        // Write the start of the data chunk as big endian data.
        randomAccessFile.writeInt(WavUtil.DATA_FOURCC);
        randomAccessFile.writeInt(-1);
      }
  
      private void writeBuffer(ByteBuffer buffer) throws IOException {
        RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile);
        while (buffer.hasRemaining()) {
          int bytesToWrite = min(buffer.remaining(), scratchBuffer.length);
          buffer.get(scratchBuffer, 0, bytesToWrite);
          randomAccessFile.write(scratchBuffer, 0, bytesToWrite);
          bytesWritten += bytesToWrite;
        }
      }
  
      private void reset() throws IOException {
        @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile;
        if (randomAccessFile == null) {
          return;
        }
  
        try {
          scratchByteBuffer.clear();
          scratchByteBuffer.putInt(bytesWritten - 8);
          randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET);
          randomAccessFile.write(scratchBuffer, 0, 4);
  
          scratchByteBuffer.clear();
          scratchByteBuffer.putInt(bytesWritten - 44);
          randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET);
          randomAccessFile.write(scratchBuffer, 0, 4);
        } catch (IOException e) {
          // The file may still be playable, so just log a warning.
          Log.w(TAG, "Error updating file size", e);
        }
  
        try {
          randomAccessFile.close();
        } finally {
          this.randomAccessFile = null;
        }
      }
  
      private String getNextOutputFileName() {
        return Util.formatInvariant("%s-%04d.wav", outputFileNamePrefix, counter++);
      }
    }
  }
  