/**
   * Initializes the basic audio track to be able to playback.
   *
   * @param sampleRate The sample rate of the track
   * @param numChannels The number of channels available in the track.
   */
  private void initDevice(int sampleRate, int numChannels) throws IOException {
    L.d(TAG, "initDevice called in state:" + state);
    lock.lock();
    try {
      final int format = findFormatFromChannels(numChannels);
      final int minSize =
          AudioTrack.getMinBufferSize(sampleRate, format, AudioFormat.ENCODING_PCM_16BIT);

      if (minSize == AudioTrack.ERROR || minSize == AudioTrack.ERROR_BAD_VALUE) {
        L.e(TAG, "minSize=" + minSize);
        throw new IOException("getMinBufferSize returned " + minSize);
      }
      track =
          new AudioTrack(
              AudioManager.STREAM_MUSIC,
              sampleRate,
              format,
              AudioFormat.ENCODING_PCM_16BIT,
              minSize * 4,
              AudioTrack.MODE_STREAM);
      sonic = new Sonic(sampleRate, numChannels);
    } finally {
      lock.unlock();
    }
  }
 @Override
 public void setDataSource(String source) {
   L.d(TAG, "setDataSource: " + source);
   switch (state) {
     case IDLE:
       this.path = source;
       state = State.INITIALIZED;
       L.d(TAG, "State changed to: " + state);
       break;
     default:
       error("setDataSource");
       break;
   }
 }
 @Override
 public void prepare() throws IOException {
   L.v(TAG, "prepare called in state: " + state);
   switch (state) {
     case INITIALIZED:
     case STOPPED:
       initStream();
       state = State.PREPARED;
       L.d(TAG, "State changed to: " + state);
       break;
     default:
       error("prepare");
   }
 }
  private void initStream() throws IOException, IllegalArgumentException {
    L.v(TAG, "initStream called in state=" + state);
    lock.lock();
    try {
      extractor = new MediaExtractor();
      if (path != null) {
        extractor.setDataSource(path);
      } else {
        error("initStream");
        throw new IOException();
      }
      int trackNum = 0;
      final MediaFormat oFormat = extractor.getTrackFormat(trackNum);

      if (!oFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
        error("initStream");
        throw new IOException("No KEY_SAMPLE_RATE");
      }
      int sampleRate = oFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);

      if (!oFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
        error("initStream");
        throw new IOException("No KEY_CHANNEL_COUNT");
      }
      int channelCount = oFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);

      if (!oFormat.containsKey(MediaFormat.KEY_MIME)) {
        error("initStream");
        throw new IOException("No KEY_MIME");
      }
      final String mime = oFormat.getString(MediaFormat.KEY_MIME);

      if (!oFormat.containsKey(MediaFormat.KEY_DURATION)) {
        error("initStream");
        throw new IOException("No KEY_DURATION");
      }
      duration = oFormat.getLong(MediaFormat.KEY_DURATION);

      L.v(TAG, "Sample rate: " + sampleRate);
      L.v(TAG, "Mime type: " + mime);
      initDevice(sampleRate, channelCount);
      extractor.selectTrack(trackNum);
      codec = MediaCodec.createDecoderByType(mime);
      codec.configure(oFormat, null, null, 0);
    } finally {
      lock.unlock();
    }
  }
 @Override
 public void pause() {
   L.v(TAG, "pause called");
   switch (state) {
     case PLAYBACK_COMPLETED:
       state = State.PAUSED;
       L.d(TAG, "State changed to: " + state);
       stayAwake(false);
       break;
     case STARTED:
     case PAUSED:
       track.pause();
       state = State.PAUSED;
       L.d(TAG, "State changed to: " + state);
       stayAwake(false);
       break;
     default:
       error("pause");
   }
 }
 @Override
 public void reset() {
   L.v(TAG, "reset called in state: " + state);
   stayAwake(false);
   lock.lock();
   try {
     continuing = false;
     try {
       if (state != State.PLAYBACK_COMPLETED) {
         while (isDecoding) {
           synchronized (decoderLock) {
             decoderLock.notify();
             decoderLock.wait();
           }
         }
       }
     } catch (InterruptedException e) {
       L.e(TAG, "Interrupted in reset while waiting for decoder thread to stop.", e);
     }
     if (codec != null) {
       codec.release();
       L.d(TAG, "releasing codec");
       codec = null;
     }
     if (extractor != null) {
       extractor.release();
       extractor = null;
     }
     if (track != null) {
       track.release();
       track = null;
     }
     state = State.IDLE;
     L.d(TAG, "State changed to: " + state);
   } finally {
     lock.unlock();
   }
 }
 @Override
 public void start() {
   L.v(TAG, "start called in state:" + state);
   switch (state) {
     case PLAYBACK_COMPLETED:
       try {
         initStream();
       } catch (IOException e) {
         e.printStackTrace();
         error("start");
         break;
       }
     case PREPARED:
       state = State.STARTED;
       L.d(TAG, "State changed to: " + state);
       continuing = true;
       track.play();
       decode();
       stayAwake(true);
       break;
     case STARTED:
       break;
     case PAUSED:
       state = State.STARTED;
       L.d(TAG, "State changed to: " + state + " with path=" + path);
       synchronized (decoderLock) {
         decoderLock.notify();
       }
       track.play();
       stayAwake(true);
       break;
     default:
       error("start");
       break;
   }
 }
        @Override
        public void run() {
          isDecoding = true;
          codec.start();
          @SuppressWarnings("deprecation")
          ByteBuffer[] inputBuffers = codec.getInputBuffers();
          @SuppressWarnings("deprecation")
          ByteBuffer[] outputBuffers = codec.getOutputBuffers();
          boolean sawInputEOS = false;
          boolean sawOutputEOS = false;
          while (!sawInputEOS && !sawOutputEOS && continuing) {
            if (state == State.PAUSED) {
              try {
                synchronized (decoderLock) {
                  decoderLock.wait();
                }
              } catch (InterruptedException e) {
                // Purposely not doing anything here
              }
              continue;
            }
            if (sonic != null) {
              sonic.setSpeed(speed);
              sonic.setPitch(1);
            }

            int inputBufIndex = codec.dequeueInputBuffer(200);
            if (inputBufIndex >= 0) {
              ByteBuffer dstBuf = inputBuffers[inputBufIndex];
              int sampleSize = extractor.readSampleData(dstBuf, 0);
              long presentationTimeUs = 0;
              if (sampleSize < 0) {
                sawInputEOS = true;
                sampleSize = 0;
              } else {
                presentationTimeUs = extractor.getSampleTime();
              }
              codec.queueInputBuffer(
                  inputBufIndex,
                  0,
                  sampleSize,
                  presentationTimeUs,
                  sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
              if (flushCodec) {
                codec.flush();
                flushCodec = false;
              }
              if (!sawInputEOS) {
                extractor.advance();
              }
            }
            final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            byte[] modifiedSamples = new byte[info.size];
            int res;
            //noinspection deprecation
            do {
              res = codec.dequeueOutputBuffer(info, 200);
              if (res >= 0) {
                final byte[] chunk = new byte[info.size];
                outputBuffers[res].get(chunk);
                outputBuffers[res].clear();
                if (chunk.length > 0) {
                  sonic.writeBytesToStream(chunk, chunk.length);
                } else {
                  sonic.flushStream();
                }
                int available = sonic.availableBytes();
                if (available > 0) {
                  if (modifiedSamples.length < available) {
                    modifiedSamples = new byte[available];
                  }
                  sonic.readBytesFromStream(modifiedSamples, available);
                  track.write(modifiedSamples, 0, available);
                }
                codec.releaseOutputBuffer(res, false);
                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                  sawOutputEOS = true;
                }
              } else //noinspection deprecation
              if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //noinspection deprecation
                outputBuffers = codec.getOutputBuffers();
              } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                track.stop();
                lock.lock();
                try {
                  track.release();
                  final MediaFormat oFormat = codec.getOutputFormat();

                  initDevice(
                      oFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),
                      oFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
                  //noinspection deprecation
                  outputBuffers = codec.getOutputBuffers();
                  track.play();
                } catch (IOException e) {
                  e.printStackTrace();
                } finally {
                  lock.unlock();
                }
              }
            } while (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
                || res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
          }

          codec.stop();
          track.stop();
          isDecoding = false;
          if (continuing && (sawInputEOS || sawOutputEOS)) {
            state = State.PLAYBACK_COMPLETED;
            L.d(TAG, "State changed to: " + state);
            Thread t =
                new Thread(
                    new Runnable() {
                      @Override
                      public void run() {
                        if (onCompletionListener != null) {
                          onCompletionListener.onCompletion();
                        }
                        stayAwake(false);
                      }
                    });
            t.setDaemon(true);
            t.start();
          }
          synchronized (decoderLock) {
            decoderLock.notifyAll();
          }
        }
 private void decode() {
   L.d(TAG, "decode called ins state=" + state);
   executor.execute(decoderRunnable);
 }
 private void error(String methodName) {
   L.e(TAG, "Error in " + methodName + " at state=" + state);
   state = State.ERROR;
   stayAwake(false);
 }