Home | History | Annotate | Download | only in audioquality
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.cts.verifier.audioquality;
     18 
     19 import com.android.cts.verifier.R;
     20 
     21 import android.app.Activity;
     22 import android.media.AudioFormat;
     23 import android.media.AudioRecord;
     24 import android.media.MediaRecorder;
     25 import android.os.Bundle;
     26 import android.os.Handler;
     27 import android.os.Message;
     28 import android.util.Log;
     29 import android.view.View;
     30 import android.widget.Button;
     31 import android.widget.ProgressBar;
     32 import android.widget.TextView;
     33 
     34 /**
     35  * Play a continuous sound and allow the user to monitor the sound level
     36  * at the mic in real time, relative to the range the phone can detect.
     37  * This is not an absolute sound level meter, but lets the speaker volume
     38  * and position be adjusted so that the clipping point is known.
     39  */
     40 public class CalibrateVolumeActivity extends Activity implements View.OnClickListener {
     41     public static final String TAG = "AudioQualityVerifier";
     42 
     43     public static final int OUTPUT_AMPL = 5000;
     44     public static final float TARGET_RMS = 5000.0f;
     45     public static final float TARGET_AMPL = (float) (TARGET_RMS * Math.sqrt(2.0));
     46     private static final float FREQ = 625.0f;
     47 
     48     private static final float TOLERANCE = 1.03f;
     49 
     50     private static final int DEBOUNCE_TIME = 500; // Minimum time in ms between status text changes
     51     public static final boolean USE_PINK = false;
     52 
     53     private ProgressBar mSlider;
     54     private Button mDoneButton;
     55     private TextView mStatus;
     56     private BackgroundAudio mBackgroundAudio;
     57     private Monitor mMonitor;
     58     private Handler mHandler;
     59 
     60     private Native mNative;
     61 
     62     enum Status { LOW, OK, HIGH, UNDEFINED }
     63     private static int[] mStatusText = {
     64         R.string.aq_status_low, R.string.aq_status_ok, R.string.aq_status_high };
     65 
     66     @Override
     67     public void onCreate(Bundle savedInstanceState) {
     68         super.onCreate(savedInstanceState);
     69         setContentView(R.layout.aq_sound_level_meter);
     70 
     71         mSlider = (ProgressBar) findViewById(R.id.slider);
     72         mStatus = (TextView) findViewById(R.id.status);
     73         mStatus.setText(R.string.aq_status_unknown);
     74 
     75         mDoneButton = (Button) findViewById(R.id.doneButton);
     76         mDoneButton.setOnClickListener(this);
     77 
     78         mNative = Native.getInstance();
     79         mHandler = new UpdateHandler();
     80     }
     81 
     82     // Implements View.OnClickListener
     83     public void onClick(View v) {
     84         if (v == mDoneButton) {
     85             finish();
     86         }
     87     }
     88 
     89     @Override
     90     protected void onResume() {
     91         super.onResume();
     92         final int DURATION = 1;
     93         final float RAMP = 0.0f;
     94         byte[] noise;
     95         if (USE_PINK) {
     96             noise = Utils.getPinkNoise(this, OUTPUT_AMPL, DURATION);
     97         } else {
     98             short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
     99                     AudioQualityVerifierActivity.SAMPLE_RATE, OUTPUT_AMPL, RAMP);
    100             noise = Utils.shortToByteArray(sinusoid);
    101         }
    102         float[] results = mNative.measureRms(Utils.byteToShortArray(noise),
    103                 AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
    104         float rms = results[Native.MEASURE_RMS_RMS];
    105         Log.i(TAG, "Stimulus amplitude " + OUTPUT_AMPL + ", RMS " + rms);
    106         mBackgroundAudio = new BackgroundAudio(noise);
    107         mMonitor = new Monitor();
    108     }
    109 
    110     @Override
    111     protected void onPause() {
    112         super.onPause();
    113         mBackgroundAudio.halt();
    114         mMonitor.halt();
    115     }
    116 
    117     private class UpdateHandler extends Handler {
    118         private Status mState = Status.UNDEFINED;
    119         private long mTimestamp = 0; // Time of last status change in ms
    120 
    121         @Override
    122         public void handleMessage(Message msg) {
    123             int rms = msg.arg1;
    124             int max = mSlider.getMax();
    125             int progress = (max / 2 * rms) / Math.round(TARGET_RMS);
    126             if (progress > max) progress = max;
    127             mSlider.setProgress(progress);
    128 
    129             Status state;
    130             if (rms * TOLERANCE < TARGET_RMS) state = Status.LOW;
    131             else if (rms > TARGET_RMS * TOLERANCE) state = Status.HIGH;
    132             else state = Status.OK;
    133             if (state != mState) {
    134                 long timestamp = System.currentTimeMillis();
    135                 if (timestamp - mTimestamp > DEBOUNCE_TIME) {
    136                     mStatus.setText(mStatusText[state.ordinal()]);
    137                     mState = state;
    138                     mTimestamp = timestamp;
    139                 }
    140             }
    141         }
    142     }
    143 
    144     class Monitor extends Thread {
    145         private static final int BUFFER_TIME = 100; // Min time in ms to buffer for
    146         private static final int READ_TIME = 25;    // Max length of time in ms to read in one chunk
    147         private static final boolean DEBUG = false;
    148 
    149         private AudioRecord mRecord;
    150         private int mSamplesToRead;
    151         private byte[] mBuffer;
    152         private boolean mProceed;
    153 
    154         Monitor() {
    155             mProceed = true;
    156 
    157             mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
    158             mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE];
    159 
    160             final int minBufferSize = (BUFFER_TIME * AudioQualityVerifierActivity.SAMPLE_RATE *
    161                     AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000;
    162             final int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
    163 
    164             mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
    165                     AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
    166                     AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
    167             if (mRecord.getState() != AudioRecord.STATE_INITIALIZED) {
    168                 Log.e(TAG, "Couldn't open audio for recording");
    169                 return;
    170             }
    171             mRecord.startRecording();
    172             if (mRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
    173                 Log.e(TAG, "Couldn't record");
    174                 return;
    175             }
    176 
    177             start(); // Begin background thread
    178         }
    179 
    180         public void halt() {
    181             mProceed = false;
    182         }
    183 
    184         @Override
    185         public void run() {
    186             int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
    187             int bytes;
    188             while (true) {
    189                 if (!mProceed) {
    190                     mRecord.stop();
    191                     mRecord.release();
    192                     return; // End thread
    193                 }
    194                 bytes = mRecord.read(mBuffer, 0, maxBytes);
    195                 if (bytes < 0) {
    196                     if (bytes == AudioRecord.ERROR_INVALID_OPERATION) {
    197                         Log.e(TAG, "Recording object not initalized");
    198                     } else if (bytes == AudioRecord.ERROR_BAD_VALUE) {
    199                         Log.e(TAG, "Invalid recording parameters");
    200                     } else {
    201                         Log.e(TAG, "Error during recording");
    202                     }
    203                     return;
    204                 }
    205                 if (bytes >= 2) {
    206                     // Note: this won't work well if bytes is small (we should check)
    207                     short[] samples = Utils.byteToShortArray(mBuffer, 0, bytes);
    208                     float[] results = mNative.measureRms(samples,
    209                             AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
    210                     float rms = results[Native.MEASURE_RMS_RMS];
    211                     float duration = results[Native.MEASURE_RMS_DURATION];
    212                     float mean = results[Native.MEASURE_RMS_MEAN];
    213                     if (DEBUG) {
    214                         // Confirm the RMS calculation
    215                         float verifyRms = 0.0f;
    216                         for (short x : samples) {
    217                             verifyRms += x * x;
    218                         }
    219                         verifyRms /= samples.length;
    220                         Log.i(TAG, "RMS: " + rms + ", bytes: " + bytes
    221                                 + ", duration: " + duration + ", mean: " + mean
    222                                 + ", manual RMS: " + Math.sqrt(verifyRms));
    223                     }
    224                     Message msg = mHandler.obtainMessage();
    225                     msg.arg1 = Math.round(rms);
    226                     mHandler.sendMessage(msg);
    227                 }
    228             }
    229         }
    230     }
    231 }
    232