// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.media;

import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.AudioEffect;
import android.media.audiofx.AudioEffect.Descriptor;
import android.os.Process;
import android.util.Log;

import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;

import java.nio.ByteBuffer;

// Owned by its native counterpart declared in audio_record_input.h. Refer to
// that class for general comments.
@JNINamespace("media")
class AudioRecordInput {
    private static final String TAG = "AudioRecordInput";
    // Set to true to enable debug logs. Always check in as false.
    private static final boolean DEBUG = false;
    // We are unable to obtain a precise measurement of the hardware delay on
    // Android. This is a conservative lower-bound based on measurments. It
    // could surely be tightened with further testing.
    private static final int HARDWARE_DELAY_MS = 100;

    private final long mNativeAudioRecordInputStream;
    private final int mSampleRate;
    private final int mChannels;
    private final int mBitsPerSample;
    private final int mHardwareDelayBytes;
    private final boolean mUsePlatformAEC;
    private ByteBuffer mBuffer;
    private AudioRecord mAudioRecord;
    private AudioRecordThread mAudioRecordThread;
    private AcousticEchoCanceler mAEC;

    private class AudioRecordThread extends Thread {
        // The "volatile" synchronization technique is discussed here:
        // http://stackoverflow.com/a/106787/299268
        // and more generally in this article:
        // https://www.ibm.com/developerworks/java/library/j-jtp06197/
        private volatile boolean mKeepAlive = true;

        @Override
        public void run() {
            Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
            try {
                mAudioRecord.startRecording();
            } catch (IllegalStateException e) {
                Log.e(TAG, "startRecording failed", e);
                return;
            }

            while (mKeepAlive) {
                int bytesRead = mAudioRecord.read(mBuffer, mBuffer.capacity());
                if (bytesRead > 0) {
                    nativeOnData(mNativeAudioRecordInputStream, bytesRead,
                                 mHardwareDelayBytes);
                } else {
                    Log.e(TAG, "read failed: " + bytesRead);
                    if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
                        // This can happen if there is already an active
                        // AudioRecord (e.g. in another tab).
                        mKeepAlive = false;
                    }
                }
            }

            try {
                mAudioRecord.stop();
            } catch (IllegalStateException e) {
                Log.e(TAG, "stop failed", e);
            }
        }

        public void joinRecordThread() {
            mKeepAlive = false;
            while (isAlive()) {
                try {
                    join();
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }

    @CalledByNative
    private static AudioRecordInput createAudioRecordInput(long nativeAudioRecordInputStream,
            int sampleRate, int channels, int bitsPerSample, int bytesPerBuffer,
            boolean usePlatformAEC) {
        return new AudioRecordInput(nativeAudioRecordInputStream, sampleRate, channels,
                                    bitsPerSample, bytesPerBuffer, usePlatformAEC);
    }

    private AudioRecordInput(long nativeAudioRecordInputStream, int sampleRate, int channels,
                             int bitsPerSample, int bytesPerBuffer, boolean usePlatformAEC) {
        mNativeAudioRecordInputStream = nativeAudioRecordInputStream;
        mSampleRate = sampleRate;
        mChannels = channels;
        mBitsPerSample = bitsPerSample;
        mHardwareDelayBytes = HARDWARE_DELAY_MS * sampleRate / 1000 * bitsPerSample / 8;
        mUsePlatformAEC = usePlatformAEC;

        // We use a direct buffer so that the native class can have access to
        // the underlying memory address. This avoids the need to copy from a
        // jbyteArray to native memory. More discussion of this here:
        // http://developer.android.com/training/articles/perf-jni.html
        mBuffer = ByteBuffer.allocateDirect(bytesPerBuffer);
        // Rather than passing the ByteBuffer with every OnData call (requiring
        // the potentially expensive GetDirectBufferAddress) we simply have the
        // the native class cache the address to the memory once.
        //
        // Unfortunately, profiling with traceview was unable to either confirm
        // or deny the advantage of this approach, as the values for
        // nativeOnData() were not stable across runs.
        nativeCacheDirectBufferAddress(mNativeAudioRecordInputStream, mBuffer);
    }

    @SuppressLint("NewApi")
    @CalledByNative
    private boolean open() {
        if (mAudioRecord != null) {
            Log.e(TAG, "open() called twice without a close()");
            return false;
        }
        int channelConfig;
        if (mChannels == 1) {
            channelConfig = AudioFormat.CHANNEL_IN_MONO;
        } else if (mChannels == 2) {
            channelConfig = AudioFormat.CHANNEL_IN_STEREO;
        } else {
            Log.e(TAG, "Unsupported number of channels: " + mChannels);
            return false;
        }

        int audioFormat;
        if (mBitsPerSample == 8) {
            audioFormat = AudioFormat.ENCODING_PCM_8BIT;
        } else if (mBitsPerSample == 16) {
            audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        } else {
            Log.e(TAG, "Unsupported bits per sample: " + mBitsPerSample);
            return false;
        }

        // TODO(ajm): Do we need to make this larger to avoid underruns? The
        // Android documentation notes "this size doesn't guarantee a smooth
        // recording under load".
        int minBufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, audioFormat);
        if (minBufferSize < 0) {
            Log.e(TAG, "getMinBufferSize error: " + minBufferSize);
            return false;
        }

        // We will request mBuffer.capacity() with every read call. The
        // underlying AudioRecord buffer should be at least this large.
        int audioRecordBufferSizeInBytes = Math.max(mBuffer.capacity(), minBufferSize);
        try {
            // TODO(ajm): Allow other AudioSource types to be requested?
            mAudioRecord = new AudioRecord(AudioSource.VOICE_COMMUNICATION,
                                           mSampleRate,
                                           channelConfig,
                                           audioFormat,
                                           audioRecordBufferSizeInBytes);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "AudioRecord failed", e);
            return false;
        }

        if (AcousticEchoCanceler.isAvailable()) {
            mAEC = AcousticEchoCanceler.create(mAudioRecord.getAudioSessionId());
            if (mAEC == null) {
                Log.e(TAG, "AcousticEchoCanceler.create failed");
                return false;
            }
            int ret = mAEC.setEnabled(mUsePlatformAEC);
            if (ret != AudioEffect.SUCCESS) {
                Log.e(TAG, "setEnabled error: " + ret);
                return false;
            }

            if (DEBUG) {
                Descriptor descriptor = mAEC.getDescriptor();
                Log.d(TAG, "AcousticEchoCanceler " +
                        "name: " + descriptor.name + ", " +
                        "implementor: " + descriptor.implementor + ", " +
                        "uuid: " + descriptor.uuid);
            }
        }
        return true;
    }

    @CalledByNative
    private void start() {
        if (mAudioRecord == null) {
            Log.e(TAG, "start() called before open().");
            return;
        }
        if (mAudioRecordThread != null) {
            // start() was already called.
            return;
        }
        mAudioRecordThread = new AudioRecordThread();
        mAudioRecordThread.start();
    }

    @CalledByNative
    private void stop() {
        if (mAudioRecordThread == null) {
            // start() was never called, or stop() was already called.
            return;
        }
        mAudioRecordThread.joinRecordThread();
        mAudioRecordThread = null;
    }

    @SuppressLint("NewApi")
    @CalledByNative
    private void close() {
        if (mAudioRecordThread != null) {
            Log.e(TAG, "close() called before stop().");
            return;
        }
        if (mAudioRecord == null) {
            // open() was not called.
            return;
        }

        if (mAEC != null) {
            mAEC.release();
            mAEC = null;
        }
        mAudioRecord.release();
        mAudioRecord = null;
    }

    private native void nativeCacheDirectBufferAddress(long nativeAudioRecordInputStream,
                                                       ByteBuffer buffer);
    private native void nativeOnData(long nativeAudioRecordInputStream, int size,
                                     int hardwareDelayBytes);
}
