Home | History | Annotate | Download | only in walt
      1 /*
      2  * Copyright (C) 2016 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 org.chromium.latency.walt;
     18 
     19 import android.content.Context;
     20 import android.media.AudioManager;
     21 import android.os.Handler;
     22 
     23 import java.io.IOException;
     24 import java.util.ArrayList;
     25 import java.util.Locale;
     26 
     27 import static org.chromium.latency.walt.Utils.getIntPreference;
     28 
     29 class AudioTest extends BaseTest {
     30 
     31     static {
     32         System.loadLibrary("sync_clock_jni");
     33     }
     34 
     35     static final int CONTINUOUS_TEST_PERIOD = 500;
     36     static final int COLD_TEST_PERIOD = 5000;
     37 
     38     enum AudioMode {COLD, CONTINUOUS}
     39 
     40     private Handler handler = new Handler();
     41     private boolean userStoppedTest = false;
     42 
     43     // Sound params
     44     private final double duration = 0.3; // seconds
     45     private final int sampleRate = 8000;
     46     private final int numSamples = (int) (duration * sampleRate);
     47     private final byte generatedSnd[] = new byte[2 * numSamples];
     48     private final double freqOfTone = 880; // hz
     49 
     50     private AudioMode audioMode;
     51     private int period = 500; // time between runs in ms
     52 
     53     // Audio in
     54     private long last_tb = 0;
     55     private int msToRecord = 1000;
     56     private final int frameRate;
     57     private final int framesPerBuffer;
     58 
     59     private int initiatedBeeps, detectedBeeps;
     60     private int playbackRepetitions;
     61     private static final int playbackSyncAfterRepetitions = 20;
     62 
     63     // Audio out
     64     private int requestedBeeps;
     65     private int recordingRepetitions;
     66     private static int recorderSyncAfterRepetitions = 10;
     67     private final int threshold;
     68 
     69     ArrayList<Double> deltas_mic = new ArrayList<>();
     70     private ArrayList<Double> deltas_play2queue = new ArrayList<>();
     71     ArrayList<Double> deltas_queue2wire = new ArrayList<>();
     72     private ArrayList<Double> deltasJ2N = new ArrayList<>();
     73 
     74     long lastBeepTime;
     75 
     76     public static native long playTone();
     77     public static native void startWarmTest();
     78     public static native void stopTests();
     79     public static native void createEngine();
     80     public static native void destroyEngine();
     81     public static native void createBufferQueueAudioPlayer(int frameRate, int framesPerBuffer);
     82 
     83     public static native void startRecording();
     84     public static native void createAudioRecorder(int frameRate, int framesToRecord);
     85     public static native short[] getRecordedWave();
     86     public static native long getTeRec();
     87     public static native long getTcRec();
     88     public static native long getTePlay();
     89 
     90     AudioTest(Context context) {
     91         super(context);
     92         playbackRepetitions = getIntPreference(context, R.string.preference_audio_out_reps, 10);
     93         recordingRepetitions = getIntPreference(context, R.string.preference_audio_in_reps, 5);
     94         threshold = getIntPreference(context, R.string.preference_audio_in_threshold, 5000);
     95 
     96         //Check for optimal output sample rate and buffer size
     97         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
     98         String frameRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
     99         String framesPerBufferStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    100         logger.log("Optimal frame rate is: " + frameRateStr);
    101         logger.log("Optimal frames per buffer is: " + framesPerBufferStr);
    102 
    103         //Convert to ints
    104         frameRate = Integer.parseInt(frameRateStr);
    105         framesPerBuffer = Integer.parseInt(framesPerBufferStr);
    106 
    107         //Create the audio engine
    108         createEngine();
    109         createBufferQueueAudioPlayer(frameRate, framesPerBuffer);
    110         logger.log("Audio engine created");
    111     }
    112 
    113     AudioTest(Context context, AutoRunFragment.ResultHandler resultHandler) {
    114         this(context);
    115         this.resultHandler = resultHandler;
    116     }
    117 
    118     void setPlaybackRepetitions(int beepCount) {
    119         playbackRepetitions = beepCount;
    120     }
    121 
    122     void setRecordingRepetitions(int beepCount) {
    123         recordingRepetitions = beepCount;
    124     }
    125 
    126     void setPeriod(int period) {
    127         this.period = period;
    128     }
    129 
    130     void setAudioMode(AudioMode mode) {
    131         audioMode = mode;
    132     }
    133 
    134     AudioMode getAudioMode() {
    135         return audioMode;
    136     }
    137 
    138     int getOptimalFrameRate() {
    139         return frameRate;
    140     }
    141 
    142     int getThreshold() {
    143         return threshold;
    144     }
    145 
    146     void stopTest() {
    147         userStoppedTest = true;
    148     }
    149 
    150     void teardown() {
    151         destroyEngine();
    152         logger.log("Audio engine destroyed");
    153     }
    154 
    155     void beginRecordingMeasurement() {
    156         userStoppedTest = false;
    157         deltas_mic.clear();
    158         deltas_play2queue.clear();
    159         deltas_queue2wire.clear();
    160         deltasJ2N.clear();
    161 
    162         int framesToRecord = (int) (0.001 * msToRecord * frameRate);
    163         createAudioRecorder(frameRate, framesToRecord);
    164         logger.log("Audio recorder created; starting test");
    165 
    166         requestedBeeps = 0;
    167         doRecordingTestRepetition();
    168     }
    169 
    170     private void doRecordingTestRepetition() {
    171         if (requestedBeeps >= recordingRepetitions || userStoppedTest) {
    172             finishRecordingMeasurement();
    173             return;
    174         }
    175 
    176         if (requestedBeeps % recorderSyncAfterRepetitions == 0) {
    177             try {
    178                 waltDevice.syncClock();
    179             } catch (IOException e) {
    180                 logger.log("Error syncing clocks: " + e.getMessage());
    181                 if (testStateListener != null) testStateListener.onTestStoppedWithError();
    182                 return;
    183             }
    184         }
    185 
    186         requestedBeeps++;
    187         startRecording();
    188         switch (audioMode) {
    189             case CONTINUOUS:
    190                 handler.postDelayed(requestBeepRunnable, msToRecord / 2);
    191                 break;
    192             case COLD: // TODO: find a more accurate method to measure cold input latency
    193                 requestBeepRunnable.run();
    194                 break;
    195         }
    196         handler.postDelayed(stopBeepRunnable, msToRecord);
    197     }
    198 
    199     void beginPlaybackMeasurement() {
    200         userStoppedTest = false;
    201         if (audioMode == AudioMode.CONTINUOUS) {
    202             startWarmTest();
    203         }
    204         try {
    205             waltDevice.syncClock();
    206             waltDevice.startListener();
    207         } catch (IOException e) {
    208             logger.log("Error starting test: " + e.getMessage());
    209             if (testStateListener != null) testStateListener.onTestStoppedWithError();
    210             return;
    211         }
    212         deltas_mic.clear();
    213         deltas_play2queue.clear();
    214         deltas_queue2wire.clear();
    215         deltasJ2N.clear();
    216 
    217         logger.log("Starting playback test");
    218 
    219         initiatedBeeps = 0;
    220         detectedBeeps = 0;
    221 
    222         waltDevice.setTriggerHandler(playbackTriggerHandler);
    223 
    224         handler.postDelayed(playBeepRunnable, 300);
    225     }
    226 
    227     private WaltDevice.TriggerHandler playbackTriggerHandler = new WaltDevice.TriggerHandler() {
    228         @Override
    229         public void onReceive(WaltDevice.TriggerMessage tmsg) {
    230             // remove the far away playBeep callback(s)
    231             handler.removeCallbacks(playBeepRunnable);
    232 
    233             detectedBeeps++;
    234             long enqueueTime = getTePlay() - waltDevice.clock.baseTime;
    235             double dt_play2queue = (enqueueTime - lastBeepTime) / 1000.;
    236             deltas_play2queue.add(dt_play2queue);
    237 
    238             double dt_queue2wire = (tmsg.t - enqueueTime) / 1000.;
    239             deltas_queue2wire.add(dt_queue2wire);
    240 
    241             logger.log(String.format(Locale.US,
    242                     "Beep detected, initiatedBeeps=%d, detectedBeeps=%d\n" +
    243                             "dt native playTone to Enqueue = %.2f ms\n" +
    244                             "dt Enqueue to wire  = %.2f ms\n",
    245                     initiatedBeeps, detectedBeeps,
    246                     dt_play2queue,
    247                     dt_queue2wire
    248             ));
    249 
    250             if (traceLogger != null) {
    251                 traceLogger.log(lastBeepTime + waltDevice.clock.baseTime,
    252                         enqueueTime + waltDevice.clock.baseTime,
    253                         "Play-to-queue",
    254                         "Bar starts at play time, ends when enqueued");
    255                 traceLogger.log(enqueueTime + waltDevice.clock.baseTime,
    256                         tmsg.t + waltDevice.clock.baseTime,
    257                         "Enqueue-to-wire",
    258                         "Bar starts at enqueue time, ends when beep is detected");
    259             }
    260             if (testStateListener != null) testStateListener.onTestPartialResult(dt_queue2wire);
    261 
    262             // Schedule another beep soon-ish
    263             handler.postDelayed(playBeepRunnable, (long) (period + Math.random() * 50 - 25));
    264         }
    265     };
    266 
    267     private Runnable playBeepRunnable = new Runnable() {
    268         @Override
    269         public void run() {
    270             // debug: logger.log("\nPlaying tone...");
    271 
    272             // Check if we saw some transitions without beeping, might be noise audio cable.
    273             if (initiatedBeeps == 0 && detectedBeeps > 1) {
    274                 logger.log("Unexpected beeps detected, noisy cable?");
    275                 return;
    276             }
    277 
    278             if (initiatedBeeps >= playbackRepetitions || userStoppedTest) {
    279                 finishPlaybackMeasurement();
    280                 return;
    281             }
    282 
    283             initiatedBeeps++;
    284 
    285             if (initiatedBeeps % playbackSyncAfterRepetitions == 0) {
    286                 try {
    287                     waltDevice.stopListener();
    288                     waltDevice.syncClock();
    289                     waltDevice.startListener();
    290                 } catch (IOException e) {
    291                     logger.log("Error re-syncing clock: " + e.getMessage());
    292                     finishPlaybackMeasurement();
    293                     return;
    294                 }
    295             }
    296 
    297             try {
    298                 waltDevice.command(WaltDevice.CMD_AUDIO);
    299             } catch (IOException e) {
    300                 logger.log("Error sending command AUDIO: " + e.getMessage());
    301                 return;
    302             }
    303             long javaBeepTime = waltDevice.clock.micros();
    304             lastBeepTime = playTone() - waltDevice.clock.baseTime;
    305             double dtJ2N = (lastBeepTime - javaBeepTime)/1000.;
    306             deltasJ2N.add(dtJ2N);
    307             if (traceLogger != null) {
    308                 traceLogger.log(javaBeepTime + waltDevice.clock.baseTime,
    309                         lastBeepTime + waltDevice.clock.baseTime, "Java-to-native",
    310                         "Bar starts when Java tells native to beep and ends when buffer written in native");
    311             }
    312             logger.log(String.format(Locale.US,
    313                     "Called playTone(), dt Java to native = %.3f ms",
    314                     dtJ2N
    315             ));
    316 
    317 
    318             // Repost doBeep to some far away time to blink again even if nothing arrives from
    319             // Teensy. This callback will almost always get cancelled by onIncomingTimestamp()
    320             handler.postDelayed(playBeepRunnable, (long) (period * 3 + Math.random() * 100 - 50));
    321 
    322         }
    323     };
    324 
    325 
    326     private Runnable requestBeepRunnable = new Runnable() {
    327         @Override
    328         public void run() {
    329             // logger.log("\nRequesting beep from WALT...");
    330             String s;
    331             try {
    332                 s = waltDevice.command(WaltDevice.CMD_BEEP);
    333             } catch (IOException e) {
    334                 logger.log("Error sending command BEEP: " + e.getMessage());
    335                 return;
    336             }
    337             last_tb = Integer.parseInt(s);
    338             logger.log("Beeped, reply: " + s);
    339             handler.postDelayed(processRecordingRunnable, (long) (msToRecord * 2 + Math.random() * 100 - 50));
    340         }
    341     };
    342 
    343     private Runnable stopBeepRunnable = new Runnable() {
    344         @Override
    345         public void run() {
    346             try {
    347                 waltDevice.command(WaltDevice.CMD_BEEP_STOP);
    348             } catch (IOException e) {
    349                 logger.log("Error stopping tone from WALT: " + e.getMessage());
    350             }
    351         }
    352     };
    353 
    354     private Runnable processRecordingRunnable = new Runnable() {
    355         @Override
    356         public void run() {
    357             long te = getTeRec() - waltDevice.clock.baseTime;  // When a buffer was enqueued for recording
    358             long tc = getTcRec() - waltDevice.clock.baseTime;  // When callback receiving a recorded buffer fired
    359             long tb = last_tb;  // When WALT started a beep (according to WALT clock)
    360             short[] wave = getRecordedWave();
    361             int noisyAtFrame = 0;  // First frame when some noise starts
    362             while (noisyAtFrame < wave.length && wave[noisyAtFrame] < threshold)
    363                 noisyAtFrame++;
    364             if (noisyAtFrame == wave.length) {
    365                 logger.log("WARNING: No sound detected");
    366                 doRecordingTestRepetition();
    367                 return;
    368             }
    369 
    370             // Length of recorded buffer
    371             double duration_us = wave.length * 1e6 / frameRate;
    372 
    373             // Duration in microseconds of the initial silent part of the buffer, and the remaining
    374             // part after the beep started.
    375             double silent_us =  noisyAtFrame * 1e6 / frameRate;
    376             double remaining_us = duration_us - silent_us;
    377 
    378             // Time from the last frame in the buffer until the callback receiving the buffer fired
    379             double latencyCb_ms = (tc - tb - remaining_us) / 1000.;
    380 
    381             // Time from the moment a buffer was enqueued for recording until the first frame in
    382             // the buffer was recorded
    383             double latencyEnqueue_ms = (tb - te - silent_us) / 1000.;
    384 
    385             logger.log(String.format(Locale.US,
    386                     "Processed: L_cb = %.3f ms, L_eq = %.3f ms, noisy frame = %d",
    387                     latencyCb_ms,
    388                     latencyEnqueue_ms,
    389                     noisyAtFrame
    390             ));
    391 
    392             if (testStateListener != null) testStateListener.onTestPartialResult(latencyCb_ms);
    393             if (traceLogger != null) {
    394                 traceLogger.log((long) (tb + waltDevice.clock.baseTime + remaining_us),
    395                         tc + waltDevice.clock.baseTime,
    396                         "Beep-to-rec-callback",
    397                         "Bar starts when WALT plays beep and ends when recording callback received");
    398             }
    399 
    400             deltas_mic.add(latencyCb_ms);
    401             doRecordingTestRepetition();
    402         }
    403     };
    404 
    405     private void finishPlaybackMeasurement() {
    406         stopTests();
    407         waltDevice.stopListener();
    408         waltDevice.clearTriggerHandler();
    409         waltDevice.checkDrift();
    410 
    411         // Debug: logger.log("deltas_play2queue = array(" + deltas_play2queue.toString() +")");
    412         logger.log(String.format(Locale.US,
    413                 "\n%s audio playback results:\n" +
    414                         "Detected %d beeps out of %d initiated\n" +
    415                         "Median Java to native time is %.3f ms\n" +
    416                         "Median native playTone to Enqueue time is %.1f ms\n" +
    417                         "Buffer length is %d frames at %d Hz = %.2f ms\n" +
    418                         "-------------------------------\n" +
    419                         "Median time from Enqueue to wire is %.1f ms\n" +
    420                         "-------------------------------\n",
    421                 audioMode == AudioMode.COLD? "Cold" : "Continuous",
    422                 detectedBeeps, initiatedBeeps,
    423                 Utils.median(deltasJ2N),
    424                 Utils.median(deltas_play2queue),
    425                 framesPerBuffer, frameRate, 1000.0 / frameRate * framesPerBuffer,
    426                 Utils.median(deltas_queue2wire)
    427         ));
    428 
    429         if (resultHandler != null) {
    430             resultHandler.onResult(deltas_play2queue, deltas_queue2wire);
    431         }
    432         if (testStateListener != null) testStateListener.onTestStopped();
    433         if (traceLogger != null) traceLogger.flush(context);
    434     }
    435 
    436     private void finishRecordingMeasurement() {
    437         waltDevice.checkDrift();
    438 
    439         // Debug: logger.log("deltas_mic: " + deltas_mic.toString());
    440 
    441         logger.log(String.format(Locale.US,
    442                 "\nAudio recording/microphone results:\n" +
    443                         "Recorded %d beeps.\n" +
    444                         "-------------------------------\n" +
    445                         "Median callback latency - " +
    446                         "time from sampling the last frame to recorder callback is %.1f ms\n" +
    447                         "-------------------------------\n",
    448                 deltas_mic.size(),
    449                 Utils.median(deltas_mic)
    450         ));
    451 
    452         if (resultHandler != null) {
    453             resultHandler.onResult(deltas_mic);
    454         }
    455         if (testStateListener != null) testStateListener.onTestStopped();
    456         if (traceLogger != null) traceLogger.flush(context);
    457     }
    458 }
    459