blob: c6cf34a76b45a5d0405f70195c0a39c0b2dd8581 [file] [log] [blame]
/*
* 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);
}
}
}
}
}