/*
 * Copyright (C) 2010 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.android.cts.verifier.audioquality;

import com.android.cts.verifier.R;

import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

/**
 * Play a continuous sound and allow the user to monitor the sound level
 * at the mic in real time, relative to the range the phone can detect.
 * This is not an absolute sound level meter, but lets the speaker volume
 * and position be adjusted so that the clipping point is known.
 */
public class CalibrateVolumeActivity extends Activity implements View.OnClickListener {
    public static final String TAG = "AudioQualityVerifier";

    public static final int OUTPUT_AMPL = 5000;
    public static final float TARGET_RMS = 5000.0f;
    public static final float TARGET_AMPL = (float) (TARGET_RMS * Math.sqrt(2.0));
    private static final float FREQ = 625.0f;

    private static final float TOLERANCE = 1.03f;

    private static final int DEBOUNCE_TIME = 500; // Minimum time in ms between status text changes
    public static final boolean USE_PINK = false;

    private ProgressBar mSlider;
    private Button mDoneButton;
    private TextView mStatus;
    private BackgroundAudio mBackgroundAudio;
    private Monitor mMonitor;
    private Handler mHandler;

    private Native mNative;

    enum Status { LOW, OK, HIGH, UNDEFINED }
    private static int[] mStatusText = {
        R.string.aq_status_low, R.string.aq_status_ok, R.string.aq_status_high };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.aq_sound_level_meter);

        mSlider = (ProgressBar) findViewById(R.id.slider);
        mStatus = (TextView) findViewById(R.id.status);
        mStatus.setText(R.string.aq_status_unknown);

        mDoneButton = (Button) findViewById(R.id.doneButton);
        mDoneButton.setOnClickListener(this);

        mNative = Native.getInstance();
        mHandler = new UpdateHandler();
    }

    // Implements View.OnClickListener
    public void onClick(View v) {
        if (v == mDoneButton) {
            finish();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        final int DURATION = 1;
        final float RAMP = 0.0f;
        byte[] noise;
        if (USE_PINK) {
            noise = Utils.getPinkNoise(this, OUTPUT_AMPL, DURATION);
        } else {
            short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
                    AudioQualityVerifierActivity.SAMPLE_RATE, OUTPUT_AMPL, RAMP);
            noise = Utils.shortToByteArray(sinusoid);
        }
        float[] results = mNative.measureRms(Utils.byteToShortArray(noise),
                AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
        float rms = results[Native.MEASURE_RMS_RMS];
        Log.i(TAG, "Stimulus amplitude " + OUTPUT_AMPL + ", RMS " + rms);
        mBackgroundAudio = new BackgroundAudio(noise);
        mMonitor = new Monitor();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mBackgroundAudio.halt();
        mMonitor.halt();
    }

    private class UpdateHandler extends Handler {
        private Status mState = Status.UNDEFINED;
        private long mTimestamp = 0; // Time of last status change in ms

        @Override
        public void handleMessage(Message msg) {
            int rms = msg.arg1;
            int max = mSlider.getMax();
            int progress = (max / 2 * rms) / Math.round(TARGET_RMS);
            if (progress > max) progress = max;
            mSlider.setProgress(progress);

            Status state;
            if (rms * TOLERANCE < TARGET_RMS) state = Status.LOW;
            else if (rms > TARGET_RMS * TOLERANCE) state = Status.HIGH;
            else state = Status.OK;
            if (state != mState) {
                long timestamp = System.currentTimeMillis();
                if (timestamp - mTimestamp > DEBOUNCE_TIME) {
                    mStatus.setText(mStatusText[state.ordinal()]);
                    mState = state;
                    mTimestamp = timestamp;
                }
            }
        }
    }

    class Monitor extends Thread {
        private static final int BUFFER_TIME = 100; // Min time in ms to buffer for
        private static final int READ_TIME = 25;    // Max length of time in ms to read in one chunk
        private static final boolean DEBUG = false;

        private AudioRecord mRecord;
        private int mSamplesToRead;
        private byte[] mBuffer;
        private boolean mProceed;

        Monitor() {
            mProceed = true;

            mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
            mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE];

            final int minBufferSize = (BUFFER_TIME * AudioQualityVerifierActivity.SAMPLE_RATE *
                    AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000;
            final int minHardwareBufferSize = AudioRecord.getMinBufferSize(
                    AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
                    AudioQualityVerifierActivity.AUDIO_FORMAT);
            final int bufferSize = Math.max(minHardwareBufferSize, minBufferSize);

            mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
                    AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
                    AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
            if (mRecord.getState() != AudioRecord.STATE_INITIALIZED) {
                Log.e(TAG, "Couldn't open audio for recording");
                return;
            }
            mRecord.startRecording();
            if (mRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
                Log.e(TAG, "Couldn't record");
                return;
            }

            start(); // Begin background thread
        }

        public void halt() {
            mProceed = false;
        }

        @Override
        public void run() {
            int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
            int bytes;
            while (true) {
                if (!mProceed) {
                    mRecord.stop();
                    mRecord.release();
                    return; // End thread
                }
                bytes = mRecord.read(mBuffer, 0, maxBytes);
                if (bytes < 0) {
                    if (bytes == AudioRecord.ERROR_INVALID_OPERATION) {
                        Log.e(TAG, "Recording object not initalized");
                    } else if (bytes == AudioRecord.ERROR_BAD_VALUE) {
                        Log.e(TAG, "Invalid recording parameters");
                    } else {
                        Log.e(TAG, "Error during recording");
                    }
                    return;
                }
                if (bytes >= 2) {
                    // Note: this won't work well if bytes is small (we should check)
                    short[] samples = Utils.byteToShortArray(mBuffer, 0, bytes);
                    float[] results = mNative.measureRms(samples,
                            AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
                    float rms = results[Native.MEASURE_RMS_RMS];
                    float duration = results[Native.MEASURE_RMS_DURATION];
                    float mean = results[Native.MEASURE_RMS_MEAN];
                    if (DEBUG) {
                        // Confirm the RMS calculation
                        float verifyRms = 0.0f;
                        for (short x : samples) {
                            verifyRms += x * x;
                        }
                        verifyRms /= samples.length;
                        Log.i(TAG, "RMS: " + rms + ", bytes: " + bytes
                                + ", duration: " + duration + ", mean: " + mean
                                + ", manual RMS: " + Math.sqrt(verifyRms));
                    }
                    Message msg = mHandler.obtainMessage();
                    msg.arg1 = Math.round(rms);
                    mHandler.sendMessage(msg);
                }
            }
        }
    }
}
