Merge "Audio Latency Tests" into gingerbread
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index dc6cf20..dfc41f8 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -170,6 +170,8 @@
<string name="aq_linearity_exp">Gain linearity test</string>
<string name="aq_overflow_exp">Overflow check</string>
<string name="aq_bias_exp">Bias measurement</string>
+ <string name="aq_cold_latency">Cold recording latency</string>
+ <string name="aq_warm_latency">Warm recording latency</string>
<!-- Experiment outcomes -->
<string name="aq_fail">Fail</string>
@@ -191,4 +193,16 @@
<string name="aq_level_report">RMS = %1$.0f, target = %2$.0f\nTolerance = %3$.1f%%\nDuration = %4$.1fs</string>
<string name="aq_spectrum_report_error">Cannot perform test.\nCheck volume is sufficiently high?</string>
<string name="aq_spectrum_report_normal">RMS deviation = %1$.2f\nMax allowed deviation = %2$.1f</string>
+ <string name="aq_cold_latency_report">Latency = %1$dms, maximum allowed = %2$dms</string>
+ <string name="aq_warm_latency_report_error">RMS = %1$.0f, target = %2$.0f</string>
+ <string name="aq_warm_latency_report_normal">Latency = %1$dms</string>
+
+ <!-- General experiment messages -->
+ <string name="aq_audiorecord_buffer_size_error">Error getting minimum AudioRecord buffer size: %1$d</string>
+ <string name="aq_audiotrack_buffer_size_error">Error getting minimum AudioTrack buffer size: %1$d</string>
+ <string name="aq_init_audiorecord_error">Error initializing AudioRecord instance</string>
+ <string name="aq_init_audiotrack_error">Error initializing AudioTrack instance</string>
+ <string name="aq_recording_error">Error reading data from AudioRecord instance</string>
+ <string name="aq_exception_error">Exception thrown during test: %1$s</string>
+
</resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java
index fd84cd3..7d7c16d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java
@@ -45,8 +45,8 @@
/**
* Main UI for the Android Audio Quality Verifier.
*/
-public class AudioQualityVerifierActivity extends PassFailButtons.Activity implements View.OnClickListener,
- OnItemClickListener {
+public class AudioQualityVerifierActivity extends PassFailButtons.Activity
+ implements View.OnClickListener, OnItemClickListener {
public static final String TAG = "AudioQualityVerifier";
public static final int SAMPLE_RATE = 16000;
@@ -90,6 +90,8 @@
private boolean mRunningExperiment;
+ private BroadcastReceiver mReceiver;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -118,7 +120,7 @@
mExperiments = VerifierExperiments.getExperiments(this);
- BroadcastReceiver receiver = new BroadcastReceiver() {
+ mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
experimentReplied(intent);
@@ -127,7 +129,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_EXP_STARTED);
filter.addAction(ACTION_EXP_FINISHED);
- registerReceiver(receiver, filter);
+ registerReceiver(mReceiver, filter);
fillAdapter();
mList.setAdapter(mAdapter);
@@ -139,6 +141,7 @@
public void onResume() {
super.onResume();
mAdapter.notifyDataSetChanged(); // Update List UI
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
checkNotSilent();
}
@@ -275,4 +278,10 @@
}
mAdapter.notifyDataSetChanged();
}
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mReceiver);
+ }
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java
index e22d596..e55f9b7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java
@@ -50,7 +50,7 @@
final int minHardwareBufferSize =
AudioTrack.getMinBufferSize(AudioQualityVerifierActivity.SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT);
- mBufferSize = Math.max(minHardwareBufferSize, minBufferSize);
+ mBufferSize = Utils.getAudioTrackBufferSize(minBufferSize);
Log.i(TAG, "minBufferSize = " + minBufferSize + ", minHWSize = " + minHardwareBufferSize
+ ", bufferSize = " + mBufferSize);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java
index c6cf34a..98e8cd1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java
@@ -159,10 +159,7 @@
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);
+ final int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java
index 5774782..704b1df 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.media.AudioFormat;
+import android.media.AudioRecord;
import android.media.AudioTrack;
import android.os.Environment;
import android.util.Log;
@@ -39,6 +40,38 @@
public static final ByteOrder BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
/**
+ * @param minBufferSize requested
+ * @return the buffer size or a negative {@link AudioTrack} ERROR value
+ */
+ public static int getAudioTrackBufferSize(int minBufferSize) {
+ int minHardwareBufferSize = AudioTrack.getMinBufferSize(
+ AudioQualityVerifierActivity.SAMPLE_RATE,
+ AudioFormat.CHANNEL_OUT_MONO,
+ AudioQualityVerifierActivity.AUDIO_FORMAT);
+ if (minHardwareBufferSize < 0) {
+ return minHardwareBufferSize;
+ } else {
+ return Math.max(minHardwareBufferSize, minBufferSize);
+ }
+ }
+
+ /**
+ * @param minBufferSize requested
+ * @return the buffer size or a negative {@link AudioRecord} ERROR value
+ */
+ public static int getAudioRecordBufferSize(int minBufferSize) {
+ int minHardwareBufferSize = AudioRecord.getMinBufferSize(
+ AudioQualityVerifierActivity.SAMPLE_RATE,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioQualityVerifierActivity.AUDIO_FORMAT);
+ if (minHardwareBufferSize < 0) {
+ return minHardwareBufferSize;
+ } else {
+ return Math.max(minHardwareBufferSize, minBufferSize);
+ }
+ }
+
+ /**
* Time delay.
*
* @param ms time in milliseconds to pause for
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java
index f800907..044bde9 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java
@@ -17,11 +17,13 @@
package com.android.cts.verifier.audioquality;
import com.android.cts.verifier.audioquality.experiments.BiasExperiment;
+import com.android.cts.verifier.audioquality.experiments.ColdLatencyExperiment;
import com.android.cts.verifier.audioquality.experiments.OverflowExperiment;
import com.android.cts.verifier.audioquality.experiments.GainLinearityExperiment;
import com.android.cts.verifier.audioquality.experiments.GlitchExperiment;
import com.android.cts.verifier.audioquality.experiments.SoundLevelExperiment;
import com.android.cts.verifier.audioquality.experiments.SpectrumShapeExperiment;
+import com.android.cts.verifier.audioquality.experiments.WarmLatencyExperiment;
import android.content.Context;
@@ -47,7 +49,8 @@
mExperiments.add(new SpectrumShapeExperiment());
mExperiments.add(new GlitchExperiment(0));
mExperiments.add(new GlitchExperiment(7));
- // mExperiments.add(new VoiceRecognitionExperiment());
+ mExperiments.add(new ColdLatencyExperiment());
+ mExperiments.add(new WarmLatencyExperiment());
for (Experiment exp : mExperiments) {
exp.init(context);
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ColdLatencyExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ColdLatencyExperiment.java
new file mode 100644
index 0000000..648e6cb
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ColdLatencyExperiment.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2011 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.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Experiment;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder.AudioSource;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * {@link Experiment} that measures how long it takes for an initialized
+ * {@link AudioRecord} object to enter the recording state.
+ */
+public class ColdLatencyExperiment extends Experiment {
+
+ /**
+ * Rough latency amounts observed:
+ *
+ * N1 2.3.4: 350 ms
+ * NS 2.3.4: 250 ms
+ * Xoom 3.1: 100 ms
+ */
+ private static final int MAXIMUM_LATENCY_ALLOWED_MS = 500;
+
+ /** Enough time to say a short phrase usually entered as a voice command. */
+ private static final int BUFFER_TIME_MS = 25 * 1000;
+
+ /** Milliseconds to pause while repeatedly checking the recording state. */
+ private static final int DELAY_MS = 10;
+
+ /** Milliseconds to record before turning off the recording. */
+ private static final int RECORDING_DELAY_MS = 3000;
+
+ /** Milliseconds to pause before checking the latency after making a sound. */
+ private static final int LATENCY_CHECK_DELAY_MS = 5000;
+
+ private static final int TEST_TIMEOUT_SECONDS = 10;
+
+ public ColdLatencyExperiment() {
+ super(true);
+ }
+
+ @Override
+ protected String lookupName(Context context) {
+ return context.getString(R.string.aq_cold_latency);
+ }
+
+ @Override
+ public void run() {
+ ExecutorService executor = Executors.newCachedThreadPool();
+ RecordingTask recordingTask = new RecordingTask(RECORDING_DELAY_MS);
+
+ try {
+ // 1. Start recording for a couple seconds.
+ Future<Long> recordingFuture = executor.submit(recordingTask);
+ long recordTime = recordingFuture.get(RECORDING_DELAY_MS * 2, TimeUnit.MILLISECONDS);
+ if (recordTime < 0) {
+ setScore(getString(R.string.aq_fail));
+ return;
+ }
+
+ // 2. Wait a bit for the audio hardware to shut down.
+ long startTime = System.currentTimeMillis();
+ while (System.currentTimeMillis() - startTime < LATENCY_CHECK_DELAY_MS) {
+ Utils.delay(DELAY_MS);
+ }
+
+ // 3. Now measure the latency by starting up the hardware again.
+ long latency = getLatency();
+ if (latency < 0) {
+ setScore(getString(R.string.aq_fail));
+ } else {
+ setScore(latency < MAXIMUM_LATENCY_ALLOWED_MS
+ ? getString(R.string.aq_pass)
+ : getString(R.string.aq_fail));
+ setReport(String.format(getString(R.string.aq_cold_latency_report), latency,
+ MAXIMUM_LATENCY_ALLOWED_MS));
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ setScore(getString(R.string.aq_fail));
+ } catch (ExecutionException e) {
+ setScore(getString(R.string.aq_fail));
+ } catch (TimeoutException e) {
+ setScore(getString(R.string.aq_fail));
+ } finally {
+ recordingTask.stopRecording();
+ executor.shutdown();
+ mTerminator.terminate(false);
+ }
+ }
+
+ @Override
+ public int getTimeout() {
+ return TEST_TIMEOUT_SECONDS;
+ }
+
+ /** Task that records for a given length of time. */
+ private class RecordingTask implements Callable<Long> {
+
+ private static final int READ_TIME = 25;
+
+ private final long mRecordMs;
+
+ private final int mSamplesToRead;
+
+ private final byte[] mBuffer;
+
+ private boolean mKeepRecording = true;
+
+ public RecordingTask(long recordMs) {
+ this.mRecordMs = recordMs;
+ this.mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
+ this.mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE];
+ }
+
+ public Long call() throws Exception {
+ int minBufferSize = BUFFER_TIME_MS / 1000
+ * AudioQualityVerifierActivity.SAMPLE_RATE
+ * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+ int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
+ if (bufferSize < 0) {
+ setReport(getString(R.string.aq_audiorecord_buffer_size_error));
+ return -1L;
+ }
+
+ AudioRecord record = null;
+ try {
+ record = new AudioRecord(AudioSource.VOICE_RECOGNITION,
+ AudioQualityVerifierActivity.SAMPLE_RATE,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioQualityVerifierActivity.AUDIO_FORMAT,
+ bufferSize);
+
+ if (record.getRecordingState() != AudioRecord.STATE_INITIALIZED) {
+ setReport(getString(R.string.aq_init_audiorecord_error));
+ return -2L;
+ }
+
+ record.startRecording();
+ while (record.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+ // Wait until we can start recording...
+ Utils.delay(DELAY_MS);
+ }
+
+ long startTime = System.currentTimeMillis();
+ int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+ while (true) {
+ synchronized (this) {
+ if (!mKeepRecording) {
+ break;
+ }
+ }
+ int numBytesRead = record.read(mBuffer, 0, maxBytes);
+ if (numBytesRead < 0) {
+ setReport(getString(R.string.aq_recording_error));
+ return -3L;
+ } else if (System.currentTimeMillis() - startTime >= mRecordMs) {
+ return System.currentTimeMillis() - startTime;
+ }
+ }
+
+ return -4L;
+ } finally {
+ if (record != null) {
+ if (record.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
+ record.stop();
+ }
+ record.release();
+ record = null;
+ }
+ }
+ }
+
+ public void stopRecording() {
+ synchronized (this) {
+ mKeepRecording = false;
+ }
+ }
+ }
+
+ /**
+ * @return latency between starting to record and entering the record state or
+ * -1 if an error occurred
+ */
+ private long getLatency() {
+ int minBufferSize = BUFFER_TIME_MS / 1000
+ * AudioQualityVerifierActivity.SAMPLE_RATE
+ * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+ int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
+ if (bufferSize < 0) {
+ setReport(String.format(getString(R.string.aq_audiorecord_buffer_size_error),
+ bufferSize));
+ return -1;
+ }
+
+ AudioRecord record = null;
+ try {
+ record = new AudioRecord(AudioSource.VOICE_RECOGNITION,
+ AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
+ AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
+
+ if (record.getRecordingState() != AudioRecord.STATE_INITIALIZED) {
+ setReport(getString(R.string.aq_init_audiorecord_error));
+ return -1;
+ }
+
+ long startTime = System.currentTimeMillis();
+ record.startRecording();
+ while (record.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+ Utils.delay(DELAY_MS);
+ }
+ long endTime = System.currentTimeMillis();
+
+ return endTime - startTime;
+ } finally {
+ if (record != null) {
+ if (record.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
+ record.stop();
+ }
+ record.release();
+ record = null;
+ }
+ }
+ }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java
index f4a1e1f..16039cb 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java
@@ -131,10 +131,7 @@
public void run() {
final int minBufferSize = AudioQualityVerifierActivity.SAMPLE_RATE
* AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
- final int minHardwareBufferSize = AudioRecord.getMinBufferSize(
- AudioQualityVerifierActivity.SAMPLE_RATE,
- AudioFormat.CHANNEL_IN_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT);
- final int bufferSize = Math.max(minHardwareBufferSize, minBufferSize);
+ final int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/WarmLatencyExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/WarmLatencyExperiment.java
new file mode 100644
index 0000000..88aaf8c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/WarmLatencyExperiment.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2011 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.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Experiment;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaRecorder.AudioSource;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * {@link Experiment} that measures how long it takes for a stimulus emitted
+ * by a warmed up {@link AudioTrack} to be recorded by a warmed up
+ * {@link AudioRecord} instance.
+ */
+public class WarmLatencyExperiment extends Experiment {
+
+ /** Milliseconds to wait before playing the sound. */
+ private static final int DELAY_TIME = 2000;
+
+ /** Target RMS value to detect before quitting the experiment. */
+ private static final float TARGET_RMS = 4000;
+
+ /** Target latency to react to the sound. */
+ private static final long TARGET_LATENCY_MS = 200;
+
+ private static final int CHANNEL_IN_CONFIG = AudioFormat.CHANNEL_IN_MONO;
+ private static final int CHANNEL_OUT_CONFIG = AudioFormat.CHANNEL_OUT_MONO;
+ private static final float FREQ = 625.0f;
+ private static final int DURATION = 1;
+ private static final int OUTPUT_AMPL = 5000;
+ private static final float RAMP = 0.0f;
+ private static final int BUFFER_TIME_MS = 100;
+ private static final int READ_TIME = 25;
+
+ public WarmLatencyExperiment() {
+ super(true);
+ }
+
+ @Override
+ protected String lookupName(Context context) {
+ return context.getString(R.string.aq_warm_latency);
+ }
+
+ @Override
+ public void run() {
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ PlaybackTask playbackTask = new PlaybackTask(barrier);
+ RecordingTask recordingTask = new RecordingTask(barrier);
+
+ Future<Long> playbackTimeFuture = executor.submit(playbackTask);
+ Future<Long> recordTimeFuture = executor.submit(recordingTask);
+
+ try {
+ // Get the time when the sound is detected or throw an exception...
+ long recordTime = recordTimeFuture.get(DELAY_TIME * 2, TimeUnit.MILLISECONDS);
+
+ // Stop the playback now since the sound was detected. Get the time playback started.
+ playbackTask.stopPlaying();
+ long playbackTime = playbackTimeFuture.get();
+
+ if (recordTime == -1 || playbackTime == -1) {
+ setScore(getString(R.string.aq_fail));
+ } else {
+ long latency = recordTime - playbackTime;
+ setScore(latency < TARGET_LATENCY_MS
+ ? getString(R.string.aq_pass)
+ : getString(R.string.aq_fail));
+ setReport(String.format(getString(R.string.aq_warm_latency_report_normal),
+ latency));
+ }
+ } catch (InterruptedException e) {
+ setExceptionReport(e);
+ } catch (ExecutionException e) {
+ setExceptionReport(e);
+ } catch (TimeoutException e) {
+ setScore(getString(R.string.aq_fail));
+ setReport(String.format(getString(R.string.aq_warm_latency_report_error),
+ recordingTask.getLastRms(), TARGET_RMS));
+ } finally {
+ playbackTask.stopPlaying();
+ recordingTask.stopRecording();
+ mTerminator.terminate(false);
+ }
+ }
+
+ private void setExceptionReport(Exception e) {
+ setScore(getString(R.string.aq_fail));
+ setReport(String.format(getString(R.string.aq_exception_error), e.getClass().getName()));
+ }
+
+ @Override
+ public int getTimeout() {
+ return 10; // seconds
+ }
+
+ /**
+ * Task that plays a sinusoid after playing silence for a couple of seconds.
+ * Returns the playback start time.
+ */
+ private class PlaybackTask implements Callable<Long> {
+
+ private final byte[] mData;
+
+ private final int mBufferSize;
+
+ private final CyclicBarrier mReadyBarrier;
+
+ private int mPosition;
+
+ private boolean mKeepPlaying = true;
+
+ public PlaybackTask(CyclicBarrier barrier) {
+ this.mData = getAudioData();
+ this.mBufferSize = getBufferSize();
+ this.mReadyBarrier = barrier;
+ }
+
+ private byte[] getAudioData() {
+ short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
+ AudioQualityVerifierActivity.SAMPLE_RATE, OUTPUT_AMPL, RAMP);
+ return Utils.shortToByteArray(sinusoid);
+ }
+
+ private int getBufferSize() {
+ int minBufferSize = (BUFFER_TIME_MS * AudioQualityVerifierActivity.SAMPLE_RATE
+ * AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000;
+ return Utils.getAudioTrackBufferSize(minBufferSize);
+ }
+
+ public Long call() throws Exception {
+ if (mBufferSize == -1) {
+ setReport(getString(R.string.aq_audiotrack_buffer_size_error));
+ return -1l;
+ }
+
+ AudioTrack track = null;
+ try {
+ track = new AudioTrack(AudioManager.STREAM_MUSIC,
+ AudioQualityVerifierActivity.SAMPLE_RATE, CHANNEL_OUT_CONFIG,
+ AudioQualityVerifierActivity.AUDIO_FORMAT, mBufferSize,
+ AudioTrack.MODE_STREAM);
+
+ if (track.getPlayState() != AudioTrack.STATE_INITIALIZED) {
+ setReport(getString(R.string.aq_init_audiotrack_error));
+ return -1l;
+ }
+
+ track.play();
+ while (track.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
+ // Wait until we've started playing...
+ }
+
+ // Wait until the recording thread has started and is recording...
+ mReadyBarrier.await(1, TimeUnit.SECONDS);
+
+ long time = System.currentTimeMillis();
+ while (System.currentTimeMillis() - time < DELAY_TIME) {
+ synchronized (this) {
+ if (!mKeepPlaying) {
+ break;
+ }
+ }
+ // Play nothing...
+ }
+
+ long playTime = System.currentTimeMillis();
+ writeAudio(track);
+ while (true) {
+ synchronized (this) {
+ if (!mKeepPlaying) {
+ break;
+ }
+ }
+ writeAudio(track);
+ }
+
+ return playTime;
+ } finally {
+ if (track != null) {
+ if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
+ track.stop();
+ }
+ track.release();
+ track = null;
+ }
+ }
+ }
+
+ private void writeAudio(AudioTrack track) {
+ int length = mData.length;
+ int writeBytes = Math.min(mBufferSize, length - mPosition);
+ int numBytesWritten = track.write(mData, mPosition, writeBytes);
+ if (numBytesWritten < 0) {
+ throw new IllegalStateException("Couldn't write any data to the track!");
+ } else {
+ mPosition += numBytesWritten;
+ if (mPosition == length) {
+ mPosition = 0;
+ }
+ }
+ }
+
+ public void stopPlaying() {
+ synchronized (this) {
+ mKeepPlaying = false;
+ }
+ }
+ }
+
+ /** Task that records until detecting a sound of the target RMS. Returns the detection time. */
+ private class RecordingTask implements Callable<Long> {
+
+ private final int mSamplesToRead;
+
+ private final byte[] mBuffer;
+
+ private final CyclicBarrier mBarrier;
+
+ private boolean mKeepRecording = true;
+
+ private float mLastRms = 0.0f;
+
+ public RecordingTask(CyclicBarrier barrier) {
+ this.mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
+ this.mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE];
+ this.mBarrier = barrier;
+ }
+
+ public Long call() throws Exception {
+ int minBufferSize = BUFFER_TIME_MS / 1000
+ * AudioQualityVerifierActivity.SAMPLE_RATE
+ * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+ int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize);
+ if (bufferSize < 0) {
+ setReport(getString(R.string.aq_audiorecord_buffer_size_error));
+ return -1l;
+ }
+
+ long recordTime = -1;
+ AudioRecord record = null;
+ try {
+ record = new AudioRecord(AudioSource.VOICE_RECOGNITION,
+ AudioQualityVerifierActivity.SAMPLE_RATE, CHANNEL_IN_CONFIG,
+ AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
+
+ if (record.getRecordingState() != AudioRecord.STATE_INITIALIZED) {
+ setReport(getString(R.string.aq_init_audiorecord_error));
+ return -1l;
+ }
+
+ record.startRecording();
+ while (record.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+ // Wait until we can start recording...
+ }
+
+ // Wait until the playback thread has started and is playing...
+ mBarrier.await(1, TimeUnit.SECONDS);
+
+ int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+ while (true) {
+ synchronized (this) {
+ if (!mKeepRecording) {
+ break;
+ }
+ }
+ int numBytesRead = record.read(mBuffer, 0, maxBytes);
+ if (numBytesRead < 0) {
+ setReport(getString(R.string.aq_recording_error));
+ return -1l;
+ } else if (numBytesRead > 2) {
+ // TODO: Could be improved to use a sliding window?
+ short[] samples = Utils.byteToShortArray(mBuffer, 0, numBytesRead);
+ float[] results = mNative.measureRms(samples,
+ AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
+ mLastRms = results[Native.MEASURE_RMS_RMS];
+ if (mLastRms >= TARGET_RMS) {
+ recordTime = System.currentTimeMillis();
+ break;
+ }
+ }
+ }
+
+ return recordTime;
+ } finally {
+ if (record != null) {
+ if (record.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
+ record.stop();
+ }
+ record.release();
+ record = null;
+ }
+ }
+ }
+
+ public float getLastRms() {
+ return mLastRms;
+ }
+
+ public void stopRecording() {
+ synchronized (this) {
+ mKeepRecording = false;
+ }
+ }
+ }
+}