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 playTone()76 public static native long playTone(); startWarmTest()77 public static native void startWarmTest(); stopTests()78 public static native void stopTests(); createEngine()79 public static native void createEngine(); destroyEngine()80 public static native void destroyEngine(); createBufferQueueAudioPlayer(int frameRate, int framesPerBuffer)81 public static native void createBufferQueueAudioPlayer(int frameRate, int framesPerBuffer); 82 startRecording()83 public static native void startRecording(); createAudioRecorder(int frameRate, int framesToRecord)84 public static native void createAudioRecorder(int frameRate, int framesToRecord); getRecordedWave()85 public static native short[] getRecordedWave(); getTeRec()86 public static native long getTeRec(); getTcRec()87 public static native long getTcRec(); getTePlay()88 public static native long getTePlay(); 89 AudioTest(Context context)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 AudioTest(Context context, AutoRunFragment.ResultHandler resultHandler)113 AudioTest(Context context, AutoRunFragment.ResultHandler resultHandler) { 114 this(context); 115 this.resultHandler = resultHandler; 116 } 117 setPlaybackRepetitions(int beepCount)118 void setPlaybackRepetitions(int beepCount) { 119 playbackRepetitions = beepCount; 120 } 121 setRecordingRepetitions(int beepCount)122 void setRecordingRepetitions(int beepCount) { 123 recordingRepetitions = beepCount; 124 } 125 setPeriod(int period)126 void setPeriod(int period) { 127 this.period = period; 128 } 129 setAudioMode(AudioMode mode)130 void setAudioMode(AudioMode mode) { 131 audioMode = mode; 132 } 133 getAudioMode()134 AudioMode getAudioMode() { 135 return audioMode; 136 } 137 getOptimalFrameRate()138 int getOptimalFrameRate() { 139 return frameRate; 140 } 141 getThreshold()142 int getThreshold() { 143 return threshold; 144 } 145 stopTest()146 void stopTest() { 147 userStoppedTest = true; 148 } 149 teardown()150 void teardown() { 151 destroyEngine(); 152 logger.log("Audio engine destroyed"); 153 } 154 beginRecordingMeasurement()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 doRecordingTestRepetition()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 beginPlaybackMeasurement()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 finishPlaybackMeasurement()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 finishRecordingMeasurement()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