• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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