Home | History | Annotate | Download | only in experiments
      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