/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.chromium.latency.walt; import android.content.Context; import android.media.AudioManager; import android.os.Handler; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; import static org.chromium.latency.walt.Utils.getIntPreference; class AudioTest extends BaseTest { static { System.loadLibrary("sync_clock_jni"); } static final int CONTINUOUS_TEST_PERIOD = 500; static final int COLD_TEST_PERIOD = 5000; enum AudioMode {COLD, CONTINUOUS} private Handler handler = new Handler(); private boolean userStoppedTest = false; // Sound params private final double duration = 0.3; // seconds private final int sampleRate = 8000; private final int numSamples = (int) (duration * sampleRate); private final byte generatedSnd[] = new byte[2 * numSamples]; private final double freqOfTone = 880; // hz private AudioMode audioMode; private int period = 500; // time between runs in ms // Audio in private long last_tb = 0; private int msToRecord = 1000; private final int frameRate; private final int framesPerBuffer; private int initiatedBeeps, detectedBeeps; private int playbackRepetitions; private static final int playbackSyncAfterRepetitions = 20; // Audio out private int requestedBeeps; private int recordingRepetitions; private static int recorderSyncAfterRepetitions = 10; private final int threshold; ArrayList<Double> deltas_mic = new ArrayList<>(); private ArrayList<Double> deltas_play2queue = new ArrayList<>(); ArrayList<Double> deltas_queue2wire = new ArrayList<>(); private ArrayList<Double> deltasJ2N = new ArrayList<>(); long lastBeepTime; public static native long playTone(); public static native void startWarmTest(); public static native void stopTests(); public static native void createEngine(); public static native void destroyEngine(); public static native void createBufferQueueAudioPlayer(int frameRate, int framesPerBuffer); public static native void startRecording(); public static native void createAudioRecorder(int frameRate, int framesToRecord); public static native short[] getRecordedWave(); public static native long getTeRec(); public static native long getTcRec(); public static native long getTePlay(); AudioTest(Context context) { super(context); playbackRepetitions = getIntPreference(context, R.string.preference_audio_out_reps, 10); recordingRepetitions = getIntPreference(context, R.string.preference_audio_in_reps, 5); threshold = getIntPreference(context, R.string.preference_audio_in_threshold, 5000); //Check for optimal output sample rate and buffer size AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); String frameRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); String framesPerBufferStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); logger.log("Optimal frame rate is: " + frameRateStr); logger.log("Optimal frames per buffer is: " + framesPerBufferStr); //Convert to ints frameRate = Integer.parseInt(frameRateStr); framesPerBuffer = Integer.parseInt(framesPerBufferStr); //Create the audio engine createEngine(); createBufferQueueAudioPlayer(frameRate, framesPerBuffer); logger.log("Audio engine created"); } AudioTest(Context context, AutoRunFragment.ResultHandler resultHandler) { this(context); this.resultHandler = resultHandler; } void setPlaybackRepetitions(int beepCount) { playbackRepetitions = beepCount; } void setRecordingRepetitions(int beepCount) { recordingRepetitions = beepCount; } void setPeriod(int period) { this.period = period; } void setAudioMode(AudioMode mode) { audioMode = mode; } AudioMode getAudioMode() { return audioMode; } int getOptimalFrameRate() { return frameRate; } int getThreshold() { return threshold; } void stopTest() { userStoppedTest = true; } void teardown() { destroyEngine(); logger.log("Audio engine destroyed"); } void beginRecordingMeasurement() { userStoppedTest = false; deltas_mic.clear(); deltas_play2queue.clear(); deltas_queue2wire.clear(); deltasJ2N.clear(); int framesToRecord = (int) (0.001 * msToRecord * frameRate); createAudioRecorder(frameRate, framesToRecord); logger.log("Audio recorder created; starting test"); requestedBeeps = 0; doRecordingTestRepetition(); } private void doRecordingTestRepetition() { if (requestedBeeps >= recordingRepetitions || userStoppedTest) { finishRecordingMeasurement(); return; } if (requestedBeeps % recorderSyncAfterRepetitions == 0) { try { waltDevice.syncClock(); } catch (IOException e) { logger.log("Error syncing clocks: " + e.getMessage()); if (testStateListener != null) testStateListener.onTestStoppedWithError(); return; } } requestedBeeps++; startRecording(); switch (audioMode) { case CONTINUOUS: handler.postDelayed(requestBeepRunnable, msToRecord / 2); break; case COLD: // TODO: find a more accurate method to measure cold input latency requestBeepRunnable.run(); break; } handler.postDelayed(stopBeepRunnable, msToRecord); } void beginPlaybackMeasurement() { userStoppedTest = false; if (audioMode == AudioMode.CONTINUOUS) { startWarmTest(); } try { waltDevice.syncClock(); waltDevice.startListener(); } catch (IOException e) { logger.log("Error starting test: " + e.getMessage()); if (testStateListener != null) testStateListener.onTestStoppedWithError(); return; } deltas_mic.clear(); deltas_play2queue.clear(); deltas_queue2wire.clear(); deltasJ2N.clear(); logger.log("Starting playback test"); initiatedBeeps = 0; detectedBeeps = 0; waltDevice.setTriggerHandler(playbackTriggerHandler); handler.postDelayed(playBeepRunnable, 300); } private WaltDevice.TriggerHandler playbackTriggerHandler = new WaltDevice.TriggerHandler() { @Override public void onReceive(WaltDevice.TriggerMessage tmsg) { // remove the far away playBeep callback(s) handler.removeCallbacks(playBeepRunnable); detectedBeeps++; long enqueueTime = getTePlay() - waltDevice.clock.baseTime; double dt_play2queue = (enqueueTime - lastBeepTime) / 1000.; deltas_play2queue.add(dt_play2queue); double dt_queue2wire = (tmsg.t - enqueueTime) / 1000.; deltas_queue2wire.add(dt_queue2wire); logger.log(String.format(Locale.US, "Beep detected, initiatedBeeps=%d, detectedBeeps=%d\n" + "dt native playTone to Enqueue = %.2f ms\n" + "dt Enqueue to wire = %.2f ms\n", initiatedBeeps, detectedBeeps, dt_play2queue, dt_queue2wire )); if (traceLogger != null) { traceLogger.log(lastBeepTime + waltDevice.clock.baseTime, enqueueTime + waltDevice.clock.baseTime, "Play-to-queue", "Bar starts at play time, ends when enqueued"); traceLogger.log(enqueueTime + waltDevice.clock.baseTime, tmsg.t + waltDevice.clock.baseTime, "Enqueue-to-wire", "Bar starts at enqueue time, ends when beep is detected"); } if (testStateListener != null) testStateListener.onTestPartialResult(dt_queue2wire); // Schedule another beep soon-ish handler.postDelayed(playBeepRunnable, (long) (period + Math.random() * 50 - 25)); } }; private Runnable playBeepRunnable = new Runnable() { @Override public void run() { // debug: logger.log("\nPlaying tone..."); // Check if we saw some transitions without beeping, might be noise audio cable. if (initiatedBeeps == 0 && detectedBeeps > 1) { logger.log("Unexpected beeps detected, noisy cable?"); return; } if (initiatedBeeps >= playbackRepetitions || userStoppedTest) { finishPlaybackMeasurement(); return; } initiatedBeeps++; if (initiatedBeeps % playbackSyncAfterRepetitions == 0) { try { waltDevice.stopListener(); waltDevice.syncClock(); waltDevice.startListener(); } catch (IOException e) { logger.log("Error re-syncing clock: " + e.getMessage()); finishPlaybackMeasurement(); return; } } try { waltDevice.command(WaltDevice.CMD_AUDIO); } catch (IOException e) { logger.log("Error sending command AUDIO: " + e.getMessage()); return; } long javaBeepTime = waltDevice.clock.micros(); lastBeepTime = playTone() - waltDevice.clock.baseTime; double dtJ2N = (lastBeepTime - javaBeepTime)/1000.; deltasJ2N.add(dtJ2N); if (traceLogger != null) { traceLogger.log(javaBeepTime + waltDevice.clock.baseTime, lastBeepTime + waltDevice.clock.baseTime, "Java-to-native", "Bar starts when Java tells native to beep and ends when buffer written in native"); } logger.log(String.format(Locale.US, "Called playTone(), dt Java to native = %.3f ms", dtJ2N )); // Repost doBeep to some far away time to blink again even if nothing arrives from // Teensy. This callback will almost always get cancelled by onIncomingTimestamp() handler.postDelayed(playBeepRunnable, (long) (period * 3 + Math.random() * 100 - 50)); } }; private Runnable requestBeepRunnable = new Runnable() { @Override public void run() { // logger.log("\nRequesting beep from WALT..."); String s; try { s = waltDevice.command(WaltDevice.CMD_BEEP); } catch (IOException e) { logger.log("Error sending command BEEP: " + e.getMessage()); return; } last_tb = Integer.parseInt(s); logger.log("Beeped, reply: " + s); handler.postDelayed(processRecordingRunnable, (long) (msToRecord * 2 + Math.random() * 100 - 50)); } }; private Runnable stopBeepRunnable = new Runnable() { @Override public void run() { try { waltDevice.command(WaltDevice.CMD_BEEP_STOP); } catch (IOException e) { logger.log("Error stopping tone from WALT: " + e.getMessage()); } } }; private Runnable processRecordingRunnable = new Runnable() { @Override public void run() { long te = getTeRec() - waltDevice.clock.baseTime; // When a buffer was enqueued for recording long tc = getTcRec() - waltDevice.clock.baseTime; // When callback receiving a recorded buffer fired long tb = last_tb; // When WALT started a beep (according to WALT clock) short[] wave = getRecordedWave(); int noisyAtFrame = 0; // First frame when some noise starts while (noisyAtFrame < wave.length && wave[noisyAtFrame] < threshold) noisyAtFrame++; if (noisyAtFrame == wave.length) { logger.log("WARNING: No sound detected"); doRecordingTestRepetition(); return; } // Length of recorded buffer double duration_us = wave.length * 1e6 / frameRate; // Duration in microseconds of the initial silent part of the buffer, and the remaining // part after the beep started. double silent_us = noisyAtFrame * 1e6 / frameRate; double remaining_us = duration_us - silent_us; // Time from the last frame in the buffer until the callback receiving the buffer fired double latencyCb_ms = (tc - tb - remaining_us) / 1000.; // Time from the moment a buffer was enqueued for recording until the first frame in // the buffer was recorded double latencyEnqueue_ms = (tb - te - silent_us) / 1000.; logger.log(String.format(Locale.US, "Processed: L_cb = %.3f ms, L_eq = %.3f ms, noisy frame = %d", latencyCb_ms, latencyEnqueue_ms, noisyAtFrame )); if (testStateListener != null) testStateListener.onTestPartialResult(latencyCb_ms); if (traceLogger != null) { traceLogger.log((long) (tb + waltDevice.clock.baseTime + remaining_us), tc + waltDevice.clock.baseTime, "Beep-to-rec-callback", "Bar starts when WALT plays beep and ends when recording callback received"); } deltas_mic.add(latencyCb_ms); doRecordingTestRepetition(); } }; private void finishPlaybackMeasurement() { stopTests(); waltDevice.stopListener(); waltDevice.clearTriggerHandler(); waltDevice.checkDrift(); // Debug: logger.log("deltas_play2queue = array(" + deltas_play2queue.toString() +")"); logger.log(String.format(Locale.US, "\n%s audio playback results:\n" + "Detected %d beeps out of %d initiated\n" + "Median Java to native time is %.3f ms\n" + "Median native playTone to Enqueue time is %.1f ms\n" + "Buffer length is %d frames at %d Hz = %.2f ms\n" + "-------------------------------\n" + "Median time from Enqueue to wire is %.1f ms\n" + "-------------------------------\n", audioMode == AudioMode.COLD? "Cold" : "Continuous", detectedBeeps, initiatedBeeps, Utils.median(deltasJ2N), Utils.median(deltas_play2queue), framesPerBuffer, frameRate, 1000.0 / frameRate * framesPerBuffer, Utils.median(deltas_queue2wire) )); if (resultHandler != null) { resultHandler.onResult(deltas_play2queue, deltas_queue2wire); } if (testStateListener != null) testStateListener.onTestStopped(); if (traceLogger != null) traceLogger.flush(context); } private void finishRecordingMeasurement() { waltDevice.checkDrift(); // Debug: logger.log("deltas_mic: " + deltas_mic.toString()); logger.log(String.format(Locale.US, "\nAudio recording/microphone results:\n" + "Recorded %d beeps.\n" + "-------------------------------\n" + "Median callback latency - " + "time from sampling the last frame to recorder callback is %.1f ms\n" + "-------------------------------\n", deltas_mic.size(), Utils.median(deltas_mic) )); if (resultHandler != null) { resultHandler.onResult(deltas_mic); } if (testStateListener != null) testStateListener.onTestStopped(); if (traceLogger != null) traceLogger.flush(context); } }