1 /* 2 * Copyright (C) 2011 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.experiments; 18 19 import com.android.cts.verifier.R; 20 import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity; 21 import com.android.cts.verifier.audioquality.Experiment; 22 import com.android.cts.verifier.audioquality.Native; 23 import com.android.cts.verifier.audioquality.Utils; 24 25 import android.content.Context; 26 import android.media.AudioFormat; 27 import android.media.AudioManager; 28 import android.media.AudioRecord; 29 import android.media.AudioTrack; 30 import android.media.MediaRecorder.AudioSource; 31 32 import java.util.concurrent.Callable; 33 import java.util.concurrent.CyclicBarrier; 34 import java.util.concurrent.ExecutionException; 35 import java.util.concurrent.ExecutorService; 36 import java.util.concurrent.Executors; 37 import java.util.concurrent.Future; 38 import java.util.concurrent.TimeUnit; 39 import java.util.concurrent.TimeoutException; 40 41 /** 42 * {@link Experiment} that measures how long it takes for a stimulus emitted 43 * by a warmed up {@link AudioTrack} to be recorded by a warmed up 44 * {@link AudioRecord} instance. 45 */ 46 public class WarmLatencyExperiment extends Experiment { 47 48 /** Milliseconds to wait before playing the sound. */ 49 private static final int DELAY_TIME = 2000; 50 51 /** Target RMS value to detect before quitting the experiment. */ 52 private static final float TARGET_RMS = 4000; 53 54 /** Target latency to react to the sound. */ 55 private static final long TARGET_LATENCY_MS = 200; 56 57 private static final int CHANNEL_IN_CONFIG = AudioFormat.CHANNEL_IN_MONO; 58 private static final int CHANNEL_OUT_CONFIG = AudioFormat.CHANNEL_OUT_MONO; 59 private static final float FREQ = 625.0f; 60 private static final int DURATION = 1; 61 private static final int OUTPUT_AMPL = 5000; 62 private static final float RAMP = 0.0f; 63 private static final int BUFFER_TIME_MS = 100; 64 private static final int READ_TIME = 25; 65 66 public WarmLatencyExperiment() { 67 super(true); 68 } 69 70 @Override 71 protected String lookupName(Context context) { 72 return context.getString(R.string.aq_warm_latency); 73 } 74 75 @Override 76 public void run() { 77 ExecutorService executor = Executors.newFixedThreadPool(2); 78 CyclicBarrier barrier = new CyclicBarrier(2); 79 PlaybackTask playbackTask = new PlaybackTask(barrier); 80 RecordingTask recordingTask = new RecordingTask(barrier); 81 82 Future<Long> playbackTimeFuture = executor.submit(playbackTask); 83 Future<Long> recordTimeFuture = executor.submit(recordingTask); 84 85 try { 86 // Get the time when the sound is detected or throw an exception... 87 long recordTime = recordTimeFuture.get(DELAY_TIME * 2, TimeUnit.MILLISECONDS); 88 89 // Stop the playback now since the sound was detected. Get the time playback started. 90 playbackTask.stopPlaying(); 91 long playbackTime = playbackTimeFuture.get(); 92 93 if (recordTime == -1 || playbackTime == -1) { 94 setScore(getString(R.string.aq_fail)); 95 } else { 96 long latency = recordTime - playbackTime; 97 setScore(latency < TARGET_LATENCY_MS 98 ? getString(R.string.aq_pass) 99 : getString(R.string.aq_fail)); 100 setReport(String.format(getString(R.string.aq_warm_latency_report_normal), 101 latency)); 102 } 103 } catch (InterruptedException e) { 104 setExceptionReport(e); 105 } catch (ExecutionException e) { 106 setExceptionReport(e); 107 } catch (TimeoutException e) { 108 setScore(getString(R.string.aq_fail)); 109 setReport(String.format(getString(R.string.aq_warm_latency_report_error), 110 recordingTask.getLastRms(), TARGET_RMS)); 111 } finally { 112 playbackTask.stopPlaying(); 113 recordingTask.stopRecording(); 114 mTerminator.terminate(false); 115 } 116 } 117 118 private void setExceptionReport(Exception e) { 119 setScore(getString(R.string.aq_fail)); 120 setReport(String.format(getString(R.string.aq_exception_error), e.getClass().getName())); 121 } 122 123 @Override 124 public int getTimeout() { 125 return 10; // seconds 126 } 127 128 /** 129 * Task that plays a sinusoid after playing silence for a couple of seconds. 130 * Returns the playback start time. 131 */ 132 private class PlaybackTask implements Callable<Long> { 133 134 private final byte[] mData; 135 136 private final int mBufferSize; 137 138 private final CyclicBarrier mReadyBarrier; 139 140 private int mPosition; 141 142 private boolean mKeepPlaying = true; 143 144 public PlaybackTask(CyclicBarrier barrier) { 145 this.mData = getAudioData(); 146 this.mBufferSize = getBufferSize(); 147 this.mReadyBarrier = barrier; 148 } 149 150 private byte[] getAudioData() { 151 short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION, 152 AudioQualityVerifierActivity.SAMPLE_RATE, OUTPUT_AMPL, RAMP); 153 return Utils.shortToByteArray(sinusoid); 154 } 155 156 private int getBufferSize() { 157 int minBufferSize = (BUFFER_TIME_MS * AudioQualityVerifierActivity.SAMPLE_RATE 158 * AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000; 159 return Utils.getAudioTrackBufferSize(minBufferSize); 160 } 161 162 public Long call() throws Exception { 163 if (mBufferSize == -1) { 164 setReport(getString(R.string.aq_audiotrack_buffer_size_error)); 165 return -1l; 166 } 167 168 AudioTrack track = null; 169 try { 170 track = new AudioTrack(AudioManager.STREAM_MUSIC, 171 AudioQualityVerifierActivity.SAMPLE_RATE, CHANNEL_OUT_CONFIG, 172 AudioQualityVerifierActivity.AUDIO_FORMAT, mBufferSize, 173 AudioTrack.MODE_STREAM); 174 175 if (track.getPlayState() != AudioTrack.STATE_INITIALIZED) { 176 setReport(getString(R.string.aq_init_audiotrack_error)); 177 return -1l; 178 } 179 180 track.play(); 181 while (track.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { 182 // Wait until we've started playing... 183 } 184 185 // Wait until the recording thread has started and is recording... 186 mReadyBarrier.await(1, TimeUnit.SECONDS); 187 188 long time = System.currentTimeMillis(); 189 while (System.currentTimeMillis() - time < DELAY_TIME) { 190 synchronized (this) { 191 if (!mKeepPlaying) { 192 break; 193 } 194 } 195 // Play nothing... 196 } 197 198 long playTime = System.currentTimeMillis(); 199 writeAudio(track); 200 while (true) { 201 synchronized (this) { 202 if (!mKeepPlaying) { 203 break; 204 } 205 } 206 writeAudio(track); 207 } 208 209 return playTime; 210 } finally { 211 if (track != null) { 212 if (track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { 213 track.stop(); 214 } 215 track.release(); 216 track = null; 217 } 218 } 219 } 220 221 private void writeAudio(AudioTrack track) { 222 int length = mData.length; 223 int writeBytes = Math.min(mBufferSize, length - mPosition); 224 int numBytesWritten = track.write(mData, mPosition, writeBytes); 225 if (numBytesWritten < 0) { 226 throw new IllegalStateException("Couldn't write any data to the track!"); 227 } else { 228 mPosition += numBytesWritten; 229 if (mPosition == length) { 230 mPosition = 0; 231 } 232 } 233 } 234 235 public void stopPlaying() { 236 synchronized (this) { 237 mKeepPlaying = false; 238 } 239 } 240 } 241 242 /** Task that records until detecting a sound of the target RMS. Returns the detection time. */ 243 private class RecordingTask implements Callable<Long> { 244 245 private final int mSamplesToRead; 246 247 private final byte[] mBuffer; 248 249 private final CyclicBarrier mBarrier; 250 251 private boolean mKeepRecording = true; 252 253 private float mLastRms = 0.0f; 254 255 public RecordingTask(CyclicBarrier barrier) { 256 this.mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000; 257 this.mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE]; 258 this.mBarrier = barrier; 259 } 260 261 public Long call() throws Exception { 262 int minBufferSize = BUFFER_TIME_MS / 1000 263 * AudioQualityVerifierActivity.SAMPLE_RATE 264 * AudioQualityVerifierActivity.BYTES_PER_SAMPLE; 265 int bufferSize = Utils.getAudioRecordBufferSize(minBufferSize); 266 if (bufferSize < 0) { 267 setReport(getString(R.string.aq_audiorecord_buffer_size_error)); 268 return -1l; 269 } 270 271 long recordTime = -1; 272 AudioRecord record = null; 273 try { 274 record = new AudioRecord(AudioSource.VOICE_RECOGNITION, 275 AudioQualityVerifierActivity.SAMPLE_RATE, CHANNEL_IN_CONFIG, 276 AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize); 277 278 if (record.getRecordingState() != AudioRecord.STATE_INITIALIZED) { 279 setReport(getString(R.string.aq_init_audiorecord_error)); 280 return -1l; 281 } 282 283 record.startRecording(); 284 while (record.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { 285 // Wait until we can start recording... 286 } 287 288 // Wait until the playback thread has started and is playing... 289 mBarrier.await(1, TimeUnit.SECONDS); 290 291 int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE; 292 while (true) { 293 synchronized (this) { 294 if (!mKeepRecording) { 295 break; 296 } 297 } 298 int numBytesRead = record.read(mBuffer, 0, maxBytes); 299 if (numBytesRead < 0) { 300 setReport(getString(R.string.aq_recording_error)); 301 return -1l; 302 } else if (numBytesRead > 2) { 303 // TODO: Could be improved to use a sliding window? 304 short[] samples = Utils.byteToShortArray(mBuffer, 0, numBytesRead); 305 float[] results = mNative.measureRms(samples, 306 AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f); 307 mLastRms = results[Native.MEASURE_RMS_RMS]; 308 if (mLastRms >= TARGET_RMS) { 309 recordTime = System.currentTimeMillis(); 310 break; 311 } 312 } 313 } 314 315 return recordTime; 316 } finally { 317 if (record != null) { 318 if (record.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { 319 record.stop(); 320 } 321 record.release(); 322 record = null; 323 } 324 } 325 } 326 327 public float getLastRms() { 328 return mLastRms; 329 } 330 331 public void stopRecording() { 332 synchronized (this) { 333 mKeepRecording = false; 334 } 335 } 336 } 337 } 338