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 onCreate(Bundle savedInstanceState)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 onClick(View v)83 public void onClick(View v) { 84 if (v == mDoneButton) { 85 finish(); 86 } 87 } 88 89 @Override onResume()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 onPause()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 handleMessage(Message msg)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 Monitor()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 halt()180 public void halt() { 181 mProceed = false; 182 } 183 184 @Override run()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