CTS Verifier test for Audio Loopback Latency

Using the Loopback plug, this test estimates the native round trip audio
latency of the system.
It builds on the knowledge from the Loopback app created as a stand alone
utility.

bug: 22344069
Change-Id: Icf007820b2f314fb0cb83c64c96a44e080702785
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index 364f13e..724b4d7 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -42,7 +42,7 @@
 
 LOCAL_AAPT_FLAGS += --version-name "5.0_r1.91 $(BUILD_NUMBER)"
 
-LOCAL_JNI_SHARED_LIBRARIES := libctsverifier_jni
+LOCAL_JNI_SHARED_LIBRARIES := libctsverifier_jni libaudioloopback_jni
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 5137761..9c8f7b6 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -1559,6 +1559,18 @@
             -->
         </activity>
 
+        <activity android:name=".audio.AudioLoopbackActivity"
+                  android:label="@string/audio_loopback_test">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_audio" />
+            <meta-data android:name="test_required_features" android:value="android.hardware.microphone" />
+            <meta-data android:name="test_excluded_features" android:value="android.hardware.type.watch" />
+            <meta-data android:name="test_excluded_features" android:value="android.hardware.type.television" />
+        </activity>
+
         <service android:name=".tv.MockTvInputService"
             android:permission="android.permission.BIND_TV_INPUT">
             <intent-filter>
diff --git a/apps/CtsVerifier/jni/audio_loopback/Android.mk b/apps/CtsVerifier/jni/audio_loopback/Android.mk
new file mode 100644
index 0000000..3dfbc34
--- /dev/null
+++ b/apps/CtsVerifier/jni/audio_loopback/Android.mk
@@ -0,0 +1,28 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE      := libaudioloopback_jni
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES   := \
+	sles.cpp \
+	jni_sles.c
+
+LOCAL_C_INCLUDES := \
+        system/media/audio_utils/include \
+        frameworks/wilhelm/include
+
+LOCAL_SHARED_LIBRARIES := \
+	libutils \
+	libcutils \
+	libOpenSLES \
+	libnbaio \
+	liblog \
+	libaudioutils
+
+LOCAL_PRELINK_MODULE := false
+
+LOCAL_LDFLAGS := -Wl,--hash-style=sysv
+LOCAL_CFLAGS := -DSTDC_HEADERS
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/apps/CtsVerifier/jni/audio_loopback/jni_sles.c b/apps/CtsVerifier/jni/audio_loopback/jni_sles.c
new file mode 100644
index 0000000..a865078
--- /dev/null
+++ b/apps/CtsVerifier/jni/audio_loopback/jni_sles.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <android/log.h>
+#include "sles.h"
+#include "jni_sles.h"
+#include <stdio.h>
+#include <stddef.h>
+
+/////
+JNIEXPORT jlong JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesInit
+  (JNIEnv *env __unused, jobject obj __unused, jint samplingRate, jint frameCount, jint micSource) {
+
+    sles_data * pSles = NULL;
+
+    if (slesInit(&pSles, samplingRate, frameCount, micSource) != SLES_FAIL) {
+
+        return (long)pSles;
+    }
+    // FIXME This should be stored as a (long) field in the object,
+    //       so that incorrect Java code could not synthesize a bad sles pointer.
+    return 0;
+}
+
+JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesProcessNext
+  (JNIEnv *env __unused, jobject obj __unused, jlong sles, jdoubleArray samplesArray,
+          jlong offset) {
+    sles_data * pSles= (sles_data*) ((long)sles);
+
+    long maxSamples = (*env)->GetArrayLength(env, samplesArray);
+    double *pSamples = (*env)->GetDoubleArrayElements(env, samplesArray,0);
+
+    long availableSamples = maxSamples-offset;
+    double *pCurrentSample = pSamples+offset;
+
+    SLES_PRINTF("jni slesProcessNext pSles:%p, currentSample %p, availableSamples %ld ", pSles,
+            pCurrentSample, availableSamples);
+
+    int samplesRead = slesProcessNext(pSles, pCurrentSample, availableSamples);
+
+    return samplesRead;
+}
+
+JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesDestroy
+  (JNIEnv *env __unused, jobject obj __unused, jlong sles) {
+    sles_data * pSles= (sles_data*) ((long) sles);
+
+    int status = slesDestroy(&pSles);
+
+    return status;
+}
diff --git a/apps/CtsVerifier/jni/audio_loopback/jni_sles.h b/apps/CtsVerifier/jni/audio_loopback/jni_sles.h
new file mode 100644
index 0000000..7bff040
--- /dev/null
+++ b/apps/CtsVerifier/jni/audio_loopback/jni_sles.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <jni.h>
+
+#ifndef _Included_org_drrickorang_loopback_jni
+#define _Included_org_drrickorang_loopback_jni
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+////////////////////////
+JNIEXPORT jlong JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesInit
+  (JNIEnv *, jobject, jint, jint, jint );
+
+JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesProcessNext
+  (JNIEnv *, jobject , jlong, jdoubleArray, jlong );
+
+JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAudioThread_slesDestroy
+  (JNIEnv *, jobject , jlong );
+
+
+#ifdef __cplusplus
+}
+#endif
+#endif //_Included_org_drrickorang_loopback_jni
diff --git a/apps/CtsVerifier/jni/audio_loopback/sles.cpp b/apps/CtsVerifier/jni/audio_loopback/sles.cpp
new file mode 100644
index 0000000..7859d35
--- /dev/null
+++ b/apps/CtsVerifier/jni/audio_loopback/sles.cpp
@@ -0,0 +1,655 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+
+////////////////////////////////////////////
+/// Actual sles functions.
+
+
+// Test program to record from default audio input and playback to default audio output.
+// It will generate feedback (Larsen effect) if played through on-device speakers,
+// or acts as a delay if played through headset.
+
+#include "sles.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <stddef.h>
+
+#include <assert.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource) {
+    int status = SLES_FAIL;
+    if (ppSles != NULL) {
+        sles_data * pSles = (sles_data*) calloc(1, sizeof (sles_data));
+
+        SLES_PRINTF("malloc %d bytes at %p",sizeof(sles_data), pSles);
+        *ppSles = pSles;
+        if (pSles != NULL)
+        {
+            SLES_PRINTF("creating server. Sampling rate =%d, frame count = %d",samplingRate,
+                    frameCount);
+            status = slesCreateServer(pSles, samplingRate, frameCount, micSource);
+            SLES_PRINTF("slesCreateServer =%d",status);
+        }
+    }
+    return status;
+}
+int slesDestroy(sles_data ** ppSles) {
+    int status = SLES_FAIL;
+    if (ppSles != NULL) {
+        slesDestroyServer(*ppSles);
+
+        if (*ppSles != NULL)
+        {
+            free(*ppSles);
+            *ppSles = 0;
+        }
+        status = SLES_SUCCESS;
+    }
+    return status;
+}
+
+#define ASSERT_EQ(x, y) do { if ((x) == (y)) ; else { fprintf(stderr, "0x%x != 0x%x\n", \
+        (unsigned) (x), (unsigned) (y)); assert((x) == (y)); } } while (0)
+
+
+// Called after audio recorder fills a buffer with data
+static void recorderCallback(SLAndroidSimpleBufferQueueItf caller __unused, void *context) {
+    sles_data *pSles = (sles_data*) context;
+    if (pSles != NULL) {
+
+
+
+        SLresult result;
+
+        pthread_mutex_lock(&(pSles->mutex));
+        //ee  SLES_PRINTF("<R");
+
+        // We should only be called when a recording buffer is done
+        assert(pSles->rxFront <= pSles->rxBufCount);
+        assert(pSles->rxRear <= pSles->rxBufCount);
+        assert(pSles->rxFront != pSles->rxRear);
+        char *buffer = pSles->rxBuffers[pSles->rxFront];
+
+        // Remove buffer from record queue
+        if (++pSles->rxFront > pSles->rxBufCount) {
+            pSles->rxFront = 0;
+        }
+
+        ssize_t actual = audio_utils_fifo_write(&(pSles->fifo), buffer,
+                (size_t) pSles->bufSizeInFrames);
+        if (actual != (ssize_t) pSles->bufSizeInFrames) {
+            write(1, "?", 1);
+        }
+
+        // This is called by a realtime (SCHED_FIFO) thread,
+        // and it is unsafe to do I/O as it could block for unbounded time.
+        // Flash filesystem is especially notorious for blocking.
+        if (pSles->fifo2Buffer != NULL) {
+            actual = audio_utils_fifo_write(&(pSles->fifo2), buffer,
+                    (size_t) pSles->bufSizeInFrames);
+            if (actual != (ssize_t) pSles->bufSizeInFrames) {
+                write(1, "?", 1);
+            }
+        }
+
+        // Enqueue this same buffer for the recorder to fill again.
+        result = (*(pSles->recorderBufferQueue))->Enqueue(pSles->recorderBufferQueue, buffer,
+                pSles->bufSizeInBytes);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+        // Update our model of the record queue
+        SLuint32 rxRearNext = pSles->rxRear+1;
+        if (rxRearNext > pSles->rxBufCount) {
+            rxRearNext = 0;
+        }
+        assert(rxRearNext != pSles->rxFront);
+        pSles->rxBuffers[pSles->rxRear] = buffer;
+        pSles->rxRear = rxRearNext;
+
+
+
+        //ee  SLES_PRINTF("r>");
+        pthread_mutex_unlock(&(pSles->mutex));
+
+    } //pSles not null
+}
+
+
+// Called after audio player empties a buffer of data
+static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
+    sles_data *pSles = (sles_data*) context;
+    if (pSles != NULL) {
+
+        SLresult result;
+
+        pthread_mutex_lock(&(pSles->mutex));
+        //ee  SLES_PRINTF("<P");
+
+        // Get the buffer that just finished playing
+        assert(pSles->txFront <= pSles->txBufCount);
+        assert(pSles->txRear <= pSles->txBufCount);
+        assert(pSles->txFront != pSles->txRear);
+        char *buffer = pSles->txBuffers[pSles->txFront];
+        if (++pSles->txFront > pSles->txBufCount) {
+            pSles->txFront = 0;
+        }
+
+
+        ssize_t actual = audio_utils_fifo_read(&(pSles->fifo), buffer, pSles->bufSizeInFrames);
+        if (actual != (ssize_t) pSles->bufSizeInFrames) {
+            write(1, "/", 1);
+            // on underrun from pipe, substitute silence
+            memset(buffer, 0, pSles->bufSizeInFrames * pSles->channels * sizeof(short));
+        }
+
+        if (pSles->injectImpulse == -1) {
+            // Experimentally, a single frame impulse was insufficient to trigger feedback.
+            // Also a Nyquist frequency signal was also insufficient, probably because
+            // the response of output and/or input path was not adequate at high frequencies.
+            // This short burst of a few cycles of square wave at Nyquist/4 was found to work well.
+            for (unsigned i = 0; i < pSles->bufSizeInFrames / 8; i += 8) {
+                for (int j = 0; j < 8; j++) {
+                    for (unsigned k = 0; k < pSles->channels; k++) {
+                        ((short *)buffer)[(i+j)*pSles->channels+k] = j < 4 ? 0x7FFF : 0x8000;
+                    }
+                }
+            }
+            pSles->injectImpulse = 0;
+        }
+
+        // Enqueue the filled buffer for playing
+        result = (*(pSles->playerBufferQueue))->Enqueue(pSles->playerBufferQueue, buffer,
+                pSles->bufSizeInBytes);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+        // Update our model of the player queue
+        assert(pSles->txFront <= pSles->txBufCount);
+        assert(pSles->txRear <= pSles->txBufCount);
+        SLuint32 txRearNext = pSles->txRear+1;
+        if (txRearNext > pSles->txBufCount) {
+            txRearNext = 0;
+        }
+        assert(txRearNext != pSles->txFront);
+        pSles->txBuffers[pSles->txRear] = buffer;
+        pSles->txRear = txRearNext;
+
+
+        //ee    SLES_PRINTF("p>");
+        pthread_mutex_unlock(&(pSles->mutex));
+
+    } //pSles not null
+}
+
+int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource) {
+    int status = SLES_FAIL;
+
+    if (pSles == NULL) {
+        return status;
+    }
+
+    //        adb shell slesTest_feedback -r1 -t1 -s48000 -f240 -i300 -e3 -o/sdcard/log.wav
+    //            r1 and t1 are the receive and transmit buffer counts, typically 1
+    //            s is the sample rate, typically 48000 or 44100
+    //            f is the frame count per buffer, typically 240 or 256
+    //            i is the number of milliseconds before impulse.  You may need to adjust this.
+    //            e is number of seconds to record
+    //            o is output .wav file name
+
+
+    //        // default values
+    //        SLuint32 rxBufCount = 1;     // -r#
+    //        SLuint32 txBufCount = 1;     // -t#
+    //        SLuint32 bufSizeInFrames = 240;  // -f#
+    //        SLuint32 channels = 1;       // -c#
+    //        SLuint32 sampleRate = 48000; // -s#
+    //        SLuint32 exitAfterSeconds = 3; // -e#
+    //        SLuint32 freeBufCount = 0;   // calculated
+    //        SLuint32 bufSizeInBytes = 0; // calculated
+    //        int injectImpulse = 300; // -i#i
+    //
+    //        // Storage area for the buffer queues
+    //        char **rxBuffers;
+    //        char **txBuffers;
+    //        char **freeBuffers;
+    //
+    //        // Buffer indices
+    //        SLuint32 rxFront;    // oldest recording
+    //        SLuint32 rxRear;     // next to be recorded
+    //        SLuint32 txFront;    // oldest playing
+    //        SLuint32 txRear;     // next to be played
+    //        SLuint32 freeFront;  // oldest free
+    //        SLuint32 freeRear;   // next to be freed
+    //
+    //        audio_utils_fifo fifo; //(*)
+    //        SLAndroidSimpleBufferQueueItf recorderBufferQueue;
+    //        SLBufferQueueItf playerBufferQueue;
+
+    // default values
+    pSles->rxBufCount = 1;     // -r#
+    pSles->txBufCount = 1;     // -t#
+    pSles->bufSizeInFrames = frameCount;//240;  // -f#
+    pSles->channels = 1;       // -c#
+    pSles->sampleRate = samplingRate;//48000; // -s#
+    pSles->exitAfterSeconds = 3; // -e#
+    pSles->freeBufCount = 0;   // calculated
+    pSles->bufSizeInBytes = 0; // calculated
+    pSles->injectImpulse = 300; // -i#i
+
+    // Storage area for the buffer queues
+    //        char **rxBuffers;
+    //        char **txBuffers;
+    //        char **freeBuffers;
+
+    // Buffer indices
+    pSles->rxFront;    // oldest recording
+    pSles->rxRear;     // next to be recorded
+    pSles->txFront;    // oldest playing
+    pSles->txRear;     // next to be played
+    pSles->freeFront;  // oldest free
+    pSles->freeRear;   // next to be freed
+
+    pSles->fifo; //(*)
+    pSles->fifo2Buffer = NULL;
+    pSles->recorderBufferQueue;
+    pSles->playerBufferQueue;
+
+    // compute total free buffers as -r plus -t
+    pSles->freeBufCount = pSles->rxBufCount + pSles->txBufCount;
+    // compute buffer size
+    pSles->bufSizeInBytes = pSles->channels * pSles->bufSizeInFrames * sizeof(short);
+
+    // Initialize free buffers
+    pSles->freeBuffers = (char **) calloc(pSles->freeBufCount+1, sizeof(char *));
+    unsigned j;
+    for (j = 0; j < pSles->freeBufCount; ++j) {
+        pSles->freeBuffers[j] = (char *) malloc(pSles->bufSizeInBytes);
+    }
+    pSles->freeFront = 0;
+    pSles->freeRear = pSles->freeBufCount;
+    pSles->freeBuffers[j] = NULL;
+
+    // Initialize record queue
+    pSles->rxBuffers = (char **) calloc(pSles->rxBufCount+1, sizeof(char *));
+    pSles->rxFront = 0;
+    pSles->rxRear = 0;
+
+    // Initialize play queue
+    pSles->txBuffers = (char **) calloc(pSles->txBufCount+1, sizeof(char *));
+    pSles->txFront = 0;
+    pSles->txRear = 0;
+
+    size_t frameSize = pSles->channels * sizeof(short);
+#define FIFO_FRAMES 1024
+    pSles->fifoBuffer = new short[FIFO_FRAMES * pSles->channels];
+    audio_utils_fifo_init(&(pSles->fifo), FIFO_FRAMES, frameSize, pSles->fifoBuffer);
+
+    //        SNDFILE *sndfile;
+    //        if (outFileName != NULL) {
+    // create .wav writer
+    //            SF_INFO info;
+    //            info.frames = 0;
+    //            info.samplerate = sampleRate;
+    //            info.channels = channels;
+    //            info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
+    //            sndfile = sf_open(outFileName, SFM_WRITE, &info);
+    //            if (sndfile != NULL) {
+#define FIFO2_FRAMES 65536
+    pSles->fifo2Buffer = new short[FIFO2_FRAMES * pSles->channels];
+    audio_utils_fifo_init(&(pSles->fifo2), FIFO2_FRAMES, frameSize, pSles->fifo2Buffer);
+    //            } else {
+    //                fprintf(stderr, "sf_open failed\n");
+    //            }
+    //        } else {
+    //            sndfile = NULL;
+    //        }
+
+    SLresult result;
+
+    // create engine
+    pSles->engineObject;
+    result = slCreateEngine(&(pSles->engineObject), 0, NULL, 0, NULL, NULL);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->engineObject))->Realize(pSles->engineObject, SL_BOOLEAN_FALSE);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    SLEngineItf engineEngine;
+    result = (*(pSles->engineObject))->GetInterface(pSles->engineObject, SL_IID_ENGINE,
+            &engineEngine);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // create output mix
+    pSles->outputmixObject;
+    result = (*engineEngine)->CreateOutputMix(engineEngine, &(pSles->outputmixObject), 0, NULL,
+            NULL);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->outputmixObject))->Realize(pSles->outputmixObject, SL_BOOLEAN_FALSE);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // create an audio player with buffer queue source and output mix sink
+    SLDataSource audiosrc;
+    SLDataSink audiosnk;
+    SLDataFormat_PCM pcm;
+    SLDataLocator_OutputMix locator_outputmix;
+    SLDataLocator_BufferQueue locator_bufferqueue_tx;
+    locator_bufferqueue_tx.locatorType = SL_DATALOCATOR_BUFFERQUEUE;
+    locator_bufferqueue_tx.numBuffers = pSles->txBufCount;
+    locator_outputmix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
+    locator_outputmix.outputMix = pSles->outputmixObject;
+    pcm.formatType = SL_DATAFORMAT_PCM;
+    pcm.numChannels = pSles->channels;
+    pcm.samplesPerSec = pSles->sampleRate * 1000;
+    pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
+    pcm.containerSize = 16;
+    pcm.channelMask = pSles->channels == 1 ? SL_SPEAKER_FRONT_CENTER :
+            (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT);
+    pcm.endianness = SL_BYTEORDER_LITTLEENDIAN;
+    audiosrc.pLocator = &locator_bufferqueue_tx;
+    audiosrc.pFormat = &pcm;
+    audiosnk.pLocator = &locator_outputmix;
+    audiosnk.pFormat = NULL;
+    pSles->playerObject = NULL;
+    pSles->recorderObject = NULL;
+    SLInterfaceID ids_tx[1] = {SL_IID_BUFFERQUEUE};
+    SLboolean flags_tx[1] = {SL_BOOLEAN_TRUE};
+    result = (*engineEngine)->CreateAudioPlayer(engineEngine, &(pSles->playerObject),
+            &audiosrc, &audiosnk, 1, ids_tx, flags_tx);
+    if (SL_RESULT_CONTENT_UNSUPPORTED == result) {
+        fprintf(stderr, "Could not create audio player (result %x), check sample rate\n",
+                result);
+        SLES_PRINTF("ERROR: Could not create audio player (result %x), check sample rate\n",
+                result);
+        goto cleanup;
+    }
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->playerObject))->Realize(pSles->playerObject, SL_BOOLEAN_FALSE);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    SLPlayItf playerPlay;
+    result = (*(pSles->playerObject))->GetInterface(pSles->playerObject, SL_IID_PLAY,
+            &playerPlay);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->playerObject))->GetInterface(pSles->playerObject, SL_IID_BUFFERQUEUE,
+            &(pSles->playerBufferQueue));
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->playerBufferQueue))->RegisterCallback(pSles->playerBufferQueue,
+            playerCallback, pSles);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // Enqueue some zero buffers for the player
+    for (j = 0; j < pSles->txBufCount; ++j) {
+
+        // allocate a free buffer
+        assert(pSles->freeFront != pSles->freeRear);
+        char *buffer = pSles->freeBuffers[pSles->freeFront];
+        if (++pSles->freeFront > pSles->freeBufCount) {
+            pSles->freeFront = 0;
+        }
+
+        // put on play queue
+        SLuint32 txRearNext = pSles->txRear + 1;
+        if (txRearNext > pSles->txBufCount) {
+            txRearNext = 0;
+        }
+        assert(txRearNext != pSles->txFront);
+        pSles->txBuffers[pSles->txRear] = buffer;
+        pSles->txRear = txRearNext;
+        result = (*(pSles->playerBufferQueue))->Enqueue(pSles->playerBufferQueue,
+                buffer, pSles->bufSizeInBytes);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    }
+
+    result = (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // Create an audio recorder with microphone device source and buffer queue sink.
+    // The buffer queue as sink is an Android-specific extension.
+
+    SLDataLocator_IODevice locator_iodevice;
+    SLDataLocator_AndroidSimpleBufferQueue locator_bufferqueue_rx;
+    locator_iodevice.locatorType = SL_DATALOCATOR_IODEVICE;
+    locator_iodevice.deviceType = SL_IODEVICE_AUDIOINPUT;
+    locator_iodevice.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT;
+    locator_iodevice.device = NULL;
+    audiosrc.pLocator = &locator_iodevice;
+    audiosrc.pFormat = NULL;
+    locator_bufferqueue_rx.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+    locator_bufferqueue_rx.numBuffers = pSles->rxBufCount;
+    audiosnk.pLocator = &locator_bufferqueue_rx;
+    audiosnk.pFormat = &pcm;
+    {
+        SLInterfaceID ids_rx[2] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+                SL_IID_ANDROIDCONFIGURATION};
+        SLboolean flags_rx[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
+        result = (*engineEngine)->CreateAudioRecorder(engineEngine, &(pSles->recorderObject),
+                &audiosrc, &audiosnk, 2, ids_rx, flags_rx);
+        if (SL_RESULT_SUCCESS != result) {
+            fprintf(stderr, "Could not create audio recorder (result %x), "
+                    "check sample rate and channel count\n", result);
+            status = SLES_FAIL;
+
+            SLES_PRINTF("ERROR: Could not create audio recorder (result %x), "
+                    "check sample rate and channel count\n", result);
+            goto cleanup;
+        }
+    }
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    {
+        /* Get the Android configuration interface which is explicit */
+        SLAndroidConfigurationItf configItf;
+        result = (*(pSles->recorderObject))->GetInterface(pSles->recorderObject,
+                SL_IID_ANDROIDCONFIGURATION, (void*)&configItf);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+        SLuint32 presetValue = micSource;
+        /* Use the configuration interface to configure the recorder before it's realized */
+        if (presetValue != SL_ANDROID_RECORDING_PRESET_NONE) {
+            result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_RECORDING_PRESET,
+                    &presetValue, sizeof(SLuint32));
+            ASSERT_EQ(SL_RESULT_SUCCESS, result);
+        }
+
+    }
+
+    result = (*(pSles->recorderObject))->Realize(pSles->recorderObject, SL_BOOLEAN_FALSE);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    SLRecordItf recorderRecord;
+    result = (*(pSles->recorderObject))->GetInterface(pSles->recorderObject, SL_IID_RECORD,
+            &recorderRecord);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->recorderObject))->GetInterface(pSles->recorderObject,
+            SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &(pSles->recorderBufferQueue));
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    result = (*(pSles->recorderBufferQueue))->RegisterCallback(pSles->recorderBufferQueue,
+            recorderCallback, pSles);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // Enqueue some empty buffers for the recorder
+    for (j = 0; j < pSles->rxBufCount; ++j) {
+
+        // allocate a free buffer
+        assert(pSles->freeFront != pSles->freeRear);
+        char *buffer = pSles->freeBuffers[pSles->freeFront];
+        if (++pSles->freeFront > pSles->freeBufCount) {
+            pSles->freeFront = 0;
+        }
+
+        // put on record queue
+        SLuint32 rxRearNext = pSles->rxRear + 1;
+        if (rxRearNext > pSles->rxBufCount) {
+            rxRearNext = 0;
+        }
+        assert(rxRearNext != pSles->rxFront);
+        pSles->rxBuffers[pSles->rxRear] = buffer;
+        pSles->rxRear = rxRearNext;
+        result = (*(pSles->recorderBufferQueue))->Enqueue(pSles->recorderBufferQueue,
+                buffer, pSles->bufSizeInBytes);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    }
+
+    // Kick off the recorder
+    result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    // Tear down the objects and exit
+    status = SLES_SUCCESS;
+    cleanup:
+    SLES_PRINTF("Finished initialization with status: %d", status);
+
+    return status;
+}
+
+int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples) {
+    //int status = SLES_FAIL;
+
+    SLES_PRINTF("slesProcessNext: pSles = %p, currentSample: %p,  maxSamples = %ld", pSles,
+            pSamples, maxSamples);
+
+    int samplesRead = 0;
+
+    int currentSample = 0;
+    double *pCurrentSample = pSamples;
+    int maxValue = 32768;
+
+    if (pSles == NULL) {
+        return samplesRead;
+    }
+
+    SLresult result;
+    for (int i = 0; i < 10; i++) {
+        usleep(100000);
+        if (pSles->fifo2Buffer != NULL) {
+            for (;;) {
+                short buffer[pSles->bufSizeInFrames * pSles->channels];
+                ssize_t actual = audio_utils_fifo_read(&(pSles->fifo2), buffer,
+                        pSles->bufSizeInFrames);
+                if (actual <= 0)
+                    break;
+                {
+                    for (int jj =0; jj<actual && currentSample < maxSamples; jj++) {
+                        *(pCurrentSample++) = ((double)buffer[jj])/maxValue;
+                        currentSample++;
+                    }
+                }
+                samplesRead +=actual;
+            }
+        }
+        if (pSles->injectImpulse > 0) {
+            if (pSles->injectImpulse <= 100) {
+                pSles->injectImpulse = -1;
+                write(1, "I", 1);
+            } else {
+                if ((pSles->injectImpulse % 1000) < 100) {
+                    write(1, "i", 1);
+                }
+                pSles->injectImpulse -= 100;
+            }
+        } else if (i == 9) {
+            write(1, ".", 1);
+        }
+    }
+    SLBufferQueueState playerBQState;
+    result = (*(pSles->playerBufferQueue))->GetState(pSles->playerBufferQueue,
+            &playerBQState);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    SLAndroidSimpleBufferQueueState recorderBQState;
+    result = (*(pSles->recorderBufferQueue))->GetState(pSles->recorderBufferQueue,
+            &recorderBQState);
+    ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+    SLES_PRINTF("End of slesProcessNext: pSles = %p, samplesRead = %d, maxSamples= %ld", pSles,
+            samplesRead, maxSamples);
+
+    return samplesRead;
+}
+
+int slesDestroyServer(sles_data *pSles) {
+    int status = SLES_FAIL;
+
+    SLES_PRINTF("Start slesDestroyServer: pSles = %p", pSles);
+    if (pSles == NULL) {
+        return status;
+    }
+
+    if (NULL != pSles->playerObject) {
+
+        SLES_PRINTF("stopping player...");
+        SLPlayItf playerPlay;
+        SLresult result = (*(pSles->playerObject))->GetInterface(pSles->playerObject,
+                SL_IID_PLAY, &playerPlay);
+
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+        //stop player and recorder if they exist
+        result = (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    }
+
+    if (NULL != pSles->recorderObject) {
+        SLES_PRINTF("stopping recorder...");
+        SLRecordItf recorderRecord;
+        SLresult result = (*(pSles->recorderObject))->GetInterface(pSles->recorderObject,
+                SL_IID_RECORD, &recorderRecord);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+
+        result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
+        ASSERT_EQ(SL_RESULT_SUCCESS, result);
+    }
+
+    usleep(1000);
+
+    audio_utils_fifo_deinit(&(pSles->fifo));
+    delete[] pSles->fifoBuffer;
+
+    SLES_PRINTF("slesDestroyServer 2");
+
+    //        if (sndfile != NULL) {
+    audio_utils_fifo_deinit(&(pSles->fifo2));
+    delete[] pSles->fifo2Buffer;
+
+    SLES_PRINTF("slesDestroyServer 3");
+
+    //            sf_close(sndfile);
+    //        }
+    if (NULL != pSles->playerObject) {
+        (*(pSles->playerObject))->Destroy(pSles->playerObject);
+    }
+
+    SLES_PRINTF("slesDestroyServer 4");
+
+    if (NULL != pSles->recorderObject) {
+        (*(pSles->recorderObject))->Destroy(pSles->recorderObject);
+    }
+
+    SLES_PRINTF("slesDestroyServer 5");
+
+    (*(pSles->outputmixObject))->Destroy(pSles->outputmixObject);
+    SLES_PRINTF("slesDestroyServer 6");
+    (*(pSles->engineObject))->Destroy(pSles->engineObject);
+    SLES_PRINTF("slesDestroyServer 7");
+
+    //        free(pSles);
+    //        pSles=NULL;
+
+    status = SLES_SUCCESS;
+
+    SLES_PRINTF("End slesDestroyServer: status = %d", status);
+    return status;
+}
+
diff --git a/apps/CtsVerifier/jni/audio_loopback/sles.h b/apps/CtsVerifier/jni/audio_loopback/sles.h
new file mode 100644
index 0000000..2550b81
--- /dev/null
+++ b/apps/CtsVerifier/jni/audio_loopback/sles.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+#include <pthread.h>
+#include <android/log.h>
+
+#ifndef _Included_org_drrickorang_loopback_sles
+#define _Included_org_drrickorang_loopback_sles
+
+//struct audio_utils_fifo;
+#define SLES_PRINTF(...)  __android_log_print(ANDROID_LOG_INFO, "sles_jni", __VA_ARGS__);
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+#include <audio_utils/fifo.h>
+
+typedef struct {
+    SLuint32 rxBufCount;     // -r#
+    SLuint32 txBufCount;     // -t#
+    SLuint32 bufSizeInFrames;  // -f#
+    SLuint32 channels;       // -c#
+    SLuint32 sampleRate; // -s#
+    SLuint32 exitAfterSeconds; // -e#
+    SLuint32 freeBufCount;   // calculated
+    SLuint32 bufSizeInBytes; // calculated
+    int injectImpulse; // -i#i
+
+    // Storage area for the buffer queues
+    char **rxBuffers;
+    char **txBuffers;
+    char **freeBuffers;
+
+    // Buffer indices
+    SLuint32 rxFront;    // oldest recording
+    SLuint32 rxRear;     // next to be recorded
+    SLuint32 txFront;    // oldest playing
+    SLuint32 txRear;     // next to be played
+    SLuint32 freeFront;  // oldest free
+    SLuint32 freeRear;   // next to be freed
+
+    struct audio_utils_fifo fifo; //(*)
+    struct audio_utils_fifo fifo2;
+    short *fifo2Buffer;
+    short *fifoBuffer;
+    SLAndroidSimpleBufferQueueItf recorderBufferQueue;
+    SLBufferQueueItf playerBufferQueue;
+
+    pthread_mutex_t mutex;// = PTHREAD_MUTEX_INITIALIZER;
+
+    //other things that belong here
+    SLObjectItf playerObject;
+    SLObjectItf recorderObject;
+    SLObjectItf outputmixObject;
+    SLObjectItf engineObject;
+} sles_data;
+
+enum {
+    SLES_SUCCESS = 0,
+    SLES_FAIL = 1,
+} SLES_STATUS_ENUM;
+
+int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource);
+
+//note the double pointer to properly free the memory of the structure
+int slesDestroy(sles_data ** ppSles);
+
+///full
+int slesFull(sles_data *pSles);
+
+int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource);
+int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples);
+int slesDestroyServer(sles_data *pSles);
+
+#ifdef __cplusplus
+}
+#endif
+#endif //_Included_org_drrickorang_loopback_sles
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_activity.xml b/apps/CtsVerifier/res/layout/audio_loopback_activity.xml
new file mode 100644
index 0000000..626ac4f
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_loopback_activity.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dip"
+        android:orientation="vertical">
+
+  <TextView
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:scrollbars="vertical"
+      android:gravity="bottom"
+      android:id="@+id/info_text"
+      android:text="@string/audio_loopback_instructions" />
+  <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:orientation="vertical">
+      <Button
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+          android:id="@+id/audio_loopback_plug_ready_btn"
+          android:text="@string/audio_loopback_plug_ready_btn"/>
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/audio_loopback_layout">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/audio_loopback_instructions2"
+            android:id="@+id/audio_loopback_instructions2"/>
+
+        <SeekBar
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/audio_loopback_level_seekbar"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/audio_loopback_level_text"
+            android:id="@+id/audio_loopback_level_text"/>
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/audio_loopback_test_btn"
+            android:id="@+id/audio_loopback_test_btn"/>
+
+        <ProgressBar
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/audio_loopback_progress_bar"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/audio_loopback_results_text"
+            android:id="@+id/audio_loopback_results_text"/>
+    </LinearLayout>
+    </LinearLayout>
+
+  <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 52cdd10..b72e83f 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -1804,4 +1804,27 @@
     <string name="audio_routingnotification_recHeader">AudioRecord Routing Notifications</string>
     <string name="audio_routingnotification_trackRoutingMsg">AudioTrack rerouting</string>
     <string name="audio_routingnotification_recordRoutingMsg">AudioRecord rerouting</string>
+
+    <!-- Audio Loopback Latency Test -->
+    <string name="audio_loopback_test">Audio Loopback Latency Test</string>
+     <string name="audio_loopback_info">
+          This test requires the Loopback Plug. Please connect a Loopback Plug on the headset
+          connector, and proceed with the instructions on the screen.
+          The system will measure the input-output audio latency by injecting a pulse on the output,
+          and computing the distance between replicas of the pulse.
+          You can vary the Audio Level slider to ensure the pulse will feed back at adecuate levels.
+          Repeat until a confidence level >= 0.6 is achieved.
+    </string>
+    <string name="audio_loopback_instructions">
+          Please connect a "Loopback Plug" and press "Loopback Plug Ready".
+    </string>
+    <string name="audio_loopback_plug_ready_btn">Loopback Plug Ready</string>
+    <string name="audio_loopback_instructions2">
+          Set the audio level to a suitable value, then press Test button.
+          It might require multiple tries until a confidence >= 0.6 is achieved.
+    </string>
+    <string name="audio_loopback_level_text">Audio Level</string>
+    <string name="audio_loopback_test_btn">Test</string>
+    <string name="audio_loopback_results_text">Results...</string>
+
 </resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackActivity.java
new file mode 100644
index 0000000..e603a69
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackActivity.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2015 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 com.android.cts.verifier.audio;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+import android.content.Context;
+
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+
+import android.util.Log;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.SeekBar;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+
+/**
+ * Tests Audio Device roundtrip latency by using a loopback plug.
+ */
+public class AudioLoopbackActivity extends PassFailButtons.Activity {
+    private static final String TAG = "AudioLoopbackActivity";
+
+    public static final int BYTES_PER_FRAME = 2;
+
+    NativeAudioThread nativeAudioThread = null;
+
+    private int mSamplingRate = 44100;
+    private int mMinBufferSizeInFrames = 0;
+    private static final double CONFIDENCE_THRESHOLD = 0.6;
+    private Correlation mCorrelation = new Correlation();
+
+    OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
+    Context mContext;
+
+    Button mLoopbackPlugReady;
+    TextView mAudioLevelText;
+    SeekBar mAudioLevelSeekbar;
+    LinearLayout mLinearLayout;
+    Button mTestButton;
+    TextView mResultText;
+    ProgressBar mProgressBar;
+
+    int mMaxLevel;
+    private class OnBtnClickListener implements OnClickListener {
+        @Override
+        public void onClick(View v) {
+            switch (v.getId()) {
+                case R.id.audio_loopback_plug_ready_btn:
+                    Log.i(TAG, "audio loopback plug ready");
+                    //enable all the other views.
+                    enableLayout(true);
+                    break;
+                case R.id.audio_loopback_test_btn:
+                    Log.i(TAG, "audio loopback test");
+                    startAudioTest();
+                    break;
+
+            }
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.audio_loopback_activity);
+
+        mContext = this;
+
+        mLoopbackPlugReady = (Button)findViewById(R.id.audio_loopback_plug_ready_btn);
+        mLoopbackPlugReady.setOnClickListener(mBtnClickListener);
+        mLinearLayout = (LinearLayout)findViewById(R.id.audio_loopback_layout);
+        mAudioLevelText = (TextView)findViewById(R.id.audio_loopback_level_text);
+        mAudioLevelSeekbar = (SeekBar)findViewById(R.id.audio_loopback_level_seekbar);
+        mTestButton =(Button)findViewById(R.id.audio_loopback_test_btn);
+        mTestButton.setOnClickListener(mBtnClickListener);
+        mResultText = (TextView)findViewById(R.id.audio_loopback_results_text);
+        mProgressBar = (ProgressBar)findViewById(R.id.audio_loopback_progress_bar);
+        showWait(false);
+
+        enableLayout(false);         //disabled all content
+        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+        mMaxLevel = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        mAudioLevelSeekbar.setMax(mMaxLevel);
+        am.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(0.7 * mMaxLevel), 0);
+        refreshLevel();
+
+        mAudioLevelSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+                AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+                am.setStreamVolume(AudioManager.STREAM_MUSIC,
+                        progress, 0);
+                refreshLevel();
+                Log.i(TAG,"Changed stream volume to: " + progress);
+            }
+        });
+
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+        setInfoResources(R.string.sample_test, R.string.audio_loopback_info, -1);
+    }
+
+    /**
+     * refresh Audio Level seekbar and text
+     */
+    private void refreshLevel() {
+        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+        int currentLevel = am.getStreamVolume(AudioManager.STREAM_MUSIC);
+        mAudioLevelSeekbar.setProgress(currentLevel);
+
+        String levelText = String.format("%s: %d/%d",
+                getResources().getString(R.string.audio_loopback_level_text),
+                currentLevel, mMaxLevel);
+        mAudioLevelText.setText(levelText);
+    }
+
+    /**
+     * enable test ui elements
+     */
+    private void enableLayout(boolean enable) {
+        for (int i = 0; i<mLinearLayout.getChildCount(); i++) {
+            View view = mLinearLayout.getChildAt(i);
+            view.setEnabled(enable);
+        }
+    }
+
+    /**
+     * show active progress bar
+     */
+    private void showWait(boolean show) {
+        if (show) {
+            mProgressBar.setVisibility(View.VISIBLE) ;
+        } else {
+            mProgressBar.setVisibility(View.INVISIBLE) ;
+        }
+    }
+
+    /**
+     *  Start the loopback audio test
+     */
+    private void startAudioTest() {
+        getPassButton().setEnabled(false);
+
+        //get system defaults for sampling rate, buffers.
+        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+        String value = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+        mMinBufferSizeInFrames = Integer.parseInt(value);
+
+        int minBufferSizeInBytes = BYTES_PER_FRAME * mMinBufferSizeInFrames;
+
+        mSamplingRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
+
+        Log.i(TAG, String.format("startAudioTest sr:%d , buffer:%d frames",
+                mSamplingRate, mMinBufferSizeInFrames));
+
+        nativeAudioThread = new NativeAudioThread();
+        if (nativeAudioThread != null) {
+            nativeAudioThread.setMessageHandler(mMessageHandler);
+            nativeAudioThread.mSessionId = 0;
+            nativeAudioThread.setParams(mSamplingRate,
+                    minBufferSizeInBytes,
+                    minBufferSizeInBytes,
+                    0x03 /*voice recognition*/);
+            nativeAudioThread.start();
+
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+
+            nativeAudioThread.runTest();
+
+        }
+    }
+
+    /**
+     * handler for messages from audio thread
+     */
+    private Handler mMessageHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            switch(msg.what) {
+                case NativeAudioThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED:
+                    Log.v(TAG,"got message native rec started!!");
+                    showWait(true);
+                    mResultText.setText("Test Running...");
+                    break;
+                case NativeAudioThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR:
+                    Log.v(TAG,"got message native rec can't start!!");
+                    showWait(false);
+                    mResultText.setText("Test Error.");
+                    break;
+                case NativeAudioThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE:
+                case NativeAudioThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
+                    if (nativeAudioThread != null) {
+                        Log.v(TAG,"Finished recording.");
+                        double [] waveData = nativeAudioThread.getWaveData();
+                        mCorrelation.computeCorrelation(waveData, mSamplingRate);
+                        mResultText.setText(String.format(
+                                "Test Finished\nLatency:%.2f ms\nConfidence: %.2f",
+                                mCorrelation.mEstimatedLatencyMs,
+                                mCorrelation.mEstimatedLatencyConfidence));
+
+                        recordTestResults();
+                        if (mCorrelation.mEstimatedLatencyConfidence >= CONFIDENCE_THRESHOLD) {
+                            getPassButton().setEnabled(true);
+                        }
+
+                        //close
+                        if (nativeAudioThread != null) {
+                            nativeAudioThread.isRunning = false;
+                            try {
+                                nativeAudioThread.finish();
+                                nativeAudioThread.join();
+                            } catch (InterruptedException e) {
+                                e.printStackTrace();
+                            }
+                            nativeAudioThread = null;
+                        }
+                        showWait(false);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    /**
+     * Store test results in log
+     */
+    private void recordTestResults() {
+
+        getReportLog().addValue(
+                "Estimated Latency",
+                mCorrelation.mEstimatedLatencyMs,
+                ResultType.LOWER_BETTER,
+                ResultUnit.MS);
+
+        getReportLog().addValue(
+                "Confidence",
+                mCorrelation.mEstimatedLatencyConfidence,
+                ResultType.HIGHER_BETTER,
+                ResultUnit.NONE);
+
+        int audioLevel = mAudioLevelSeekbar.getProgress();
+        getReportLog().addValue(
+                "Audio Level",
+                audioLevel,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        getReportLog().addValue(
+                "Frames Buffer Size",
+                mMinBufferSizeInFrames,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        getReportLog().addValue(
+                "Sampling Rate",
+                mSamplingRate,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        Log.v(TAG,"Results Recorded");
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/Correlation.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Correlation.java
new file mode 100644
index 0000000..75b04eb
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Correlation.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2015 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 com.android.cts.verifier.audio;
+
+import android.util.Log;
+
+
+public class Correlation {
+
+    private int mBlockSize = 4096;
+    private int mSamplingRate = 44100;
+    private double [] mDataDownsampled = new double [mBlockSize];
+    private double [] mDataAutocorrelated = new double[mBlockSize];
+
+    public double mEstimatedLatencySamples = 0;
+    public double mEstimatedLatencyMs = 0;
+    public double mEstimatedLatencyConfidence = 0.0;
+
+    public void init(int blockSize, int samplingRate) {
+        mBlockSize = blockSize;
+        mSamplingRate = samplingRate;
+    }
+
+    public boolean computeCorrelation(double [] data, int samplingRate) {
+        boolean status = false;
+        log("Started Auto Correlation for data with " + data.length + " points");
+        mSamplingRate = samplingRate;
+
+        downsampleData(data, mDataDownsampled);
+
+        //correlation vector
+        autocorrelation(mDataDownsampled, mDataAutocorrelated);
+
+        int N = data.length; //all samples available
+        double groupSize =  (double) N / mBlockSize;  //samples per downsample point.
+
+        double maxValue = 0;
+        int maxIndex = -1;
+
+        double minLatencyMs = 8; //min latency expected. This algorithm should be improved.
+        int minIndex = (int)(0.5 + minLatencyMs * mSamplingRate / (groupSize*1000));
+
+        double average = 0;
+        double rms = 0;
+        //find max
+        for (int i=minIndex; i<mDataAutocorrelated.length; i++) {
+            average += mDataAutocorrelated[i];
+            rms += mDataAutocorrelated[i]*mDataAutocorrelated[i];
+            if (mDataAutocorrelated[i] > maxValue) {
+                maxValue = mDataAutocorrelated[i];
+                maxIndex = i;
+            }
+        }
+
+        rms = Math.sqrt(rms/mDataAutocorrelated.length);
+        average = average/mDataAutocorrelated.length;
+        log(String.format(" Maxvalue %f, max Index : %d/%d (%d)  minIndex=%d",maxValue, maxIndex,
+                mDataAutocorrelated.length, data.length, minIndex));
+
+        log(String.format("  average : %.3f  rms: %.3f", average, rms));
+
+        mEstimatedLatencyConfidence = 0.0;
+        if (average>0) {
+            double factor = 3.0;
+
+            double raw = (rms-average) /(factor*average);
+            log(String.format("Raw: %.3f",raw));
+            mEstimatedLatencyConfidence = Math.max(Math.min(raw, 1.0),0.0);
+        }
+
+        log(String.format(" ****Confidence: %.2f",mEstimatedLatencyConfidence));
+
+        mEstimatedLatencySamples = maxIndex*groupSize;
+
+        mEstimatedLatencyMs = mEstimatedLatencySamples *1000/mSamplingRate;
+
+        log(String.format(" latencySamples: %.2f  %.2f ms", mEstimatedLatencySamples,
+                mEstimatedLatencyMs));
+
+        status = true;
+        return status;
+    }
+
+    private boolean downsampleData(double [] data, double [] dataDownsampled) {
+
+        boolean status = false;
+        // mDataDownsampled = new double[mBlockSize];
+        for (int i=0; i<mBlockSize; i++) {
+            dataDownsampled[i] = 0;
+        }
+
+        int N = data.length; //all samples available
+        double groupSize =  (double) N / mBlockSize;
+
+        int currentIndex = 0;
+        double nextGroup = groupSize;
+        for (int i = 0; i<N && currentIndex<mBlockSize; i++) {
+
+            if (i> nextGroup) { //advanced to next group.
+                currentIndex++;
+                nextGroup += groupSize;
+            }
+
+            if (currentIndex>=mBlockSize) {
+                break;
+            }
+            dataDownsampled[currentIndex] += Math.abs(data[i]);
+        }
+
+        status = true;
+        return status;
+    }
+
+    private boolean autocorrelation(double [] data, double [] dataOut) {
+        boolean status = false;
+
+        double sumsquared = 0;
+        int N = data.length;
+        for (int i=0; i<N; i++) {
+            double value = data[i];
+            sumsquared += value*value;
+        }
+
+        if (sumsquared>0) {
+            for (int i = 0; i < N; i++) {
+                dataOut[i] = 0;
+                for (int j = 0; j < N - i; j++) {
+
+                    dataOut[i] += data[j] * data[i + j];
+                }
+                dataOut[i] = dataOut[i] / sumsquared;
+            }
+            status = true;
+        }
+
+        return status;
+    }
+
+    private static void log(String msg) {
+        Log.v("Recorder", msg);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAudioThread.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAudioThread.java
new file mode 100644
index 0000000..224d4c8
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAudioThread.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2015 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.drrickorang.loopback;
+
+package com.android.cts.verifier.audio;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+//import android.media.MediaPlayer;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+import android.os.Handler;
+import  android.os.Message;
+
+/**
+ * A thread/audio track based audio synth.
+ */
+public class NativeAudioThread extends Thread {
+
+    public boolean isRunning = false;
+    double twoPi = 6.28318530718;
+
+    public int mSessionId;
+
+    public double[] mvSamples; //captured samples
+    int mSamplesIndex;
+
+    private final int mSecondsToRun = 2;
+    public int mSamplingRate = 48000;
+    private int mChannelConfigIn = AudioFormat.CHANNEL_IN_MONO;
+    private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
+
+    int mMinPlayBufferSizeInBytes = 0;
+    int mMinRecordBuffSizeInBytes = 0;
+    private int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO;
+
+    int mMicSource = 0;
+
+//    private double [] samples = new double[50000];
+
+    boolean isPlaying = false;
+    private Handler mMessageHandler;
+    boolean isDestroying = false;
+    boolean hasDestroyingErrors = false;
+
+    static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED = 892;
+    static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR = 893;
+    static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE = 894;
+    static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS = 895;
+
+    public void setParams(int samplingRate, int playBufferInBytes, int recBufferInBytes,
+            int micSource) {
+        mSamplingRate = samplingRate;
+        mMinPlayBufferSizeInBytes = playBufferInBytes;
+        mMinRecordBuffSizeInBytes = recBufferInBytes;
+        mMicSource = micSource;
+    }
+
+    //JNI load
+    static {
+        try {
+            System.loadLibrary("audioloopback_jni");
+        } catch (UnsatisfiedLinkError e) {
+            log("Error loading loopback JNI library");
+            e.printStackTrace();
+        }
+
+        /* TODO: gracefully fail/notify if the library can't be loaded */
+    }
+
+    //jni calls
+    public native long slesInit(int samplingRate, int frameCount, int micSource);
+    public native int slesProcessNext(long sles_data, double[] samples, long offset);
+    public native int slesDestroy(long sles_data);
+
+    public void run() {
+
+        setPriority(Thread.MAX_PRIORITY);
+        isRunning = true;
+
+        //erase output buffer
+        if (mvSamples != null)
+            mvSamples = null;
+
+        //resize
+        int nNewSize = (int)(1.1* mSamplingRate * mSecondsToRun ); //10% more just in case
+        mvSamples = new double[nNewSize];
+        mSamplesIndex = 0; //reset index
+
+        //clear samples
+        for (int i=0; i<nNewSize; i++) {
+            mvSamples[i] = 0;
+        }
+
+        //start playing
+        isPlaying = true;
+
+
+        log(" Started capture test");
+        if (mMessageHandler != null) {
+            Message msg = Message.obtain();
+            msg.what = NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED;
+            mMessageHandler.sendMessage(msg);
+        }
+
+
+
+        log(String.format("about to init, sampling rate: %d, buffer:%d", mSamplingRate,
+                mMinPlayBufferSizeInBytes/2 ));
+        long sles_data = slesInit(mSamplingRate, mMinPlayBufferSizeInBytes/2, mMicSource);
+        log(String.format("sles_data = 0x%X",sles_data));
+
+        if (sles_data == 0 ) {
+            log(" ERROR at JNI initialization");
+            if (mMessageHandler != null) {
+                Message msg = Message.obtain();
+                msg.what = NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR;
+                mMessageHandler.sendMessage(msg);
+            }
+        }  else {
+
+            //wait a little bit...
+            try {
+                sleep(10); //just to let it start properly?
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+
+
+
+            mSamplesIndex = 0;
+            int totalSamplesRead = 0;
+            long offset = 0;
+            for (int ii = 0; ii < mSecondsToRun; ii++) {
+                log(String.format("block %d...", ii));
+                int samplesRead = slesProcessNext(sles_data, mvSamples,offset);
+                totalSamplesRead += samplesRead;
+
+                offset += samplesRead;
+                log(" [" + ii + "] jni samples read:" + samplesRead + "  currentOffset:" + offset);
+            }
+
+            log(String.format(" samplesRead: %d, sampleOffset:%d", totalSamplesRead, offset));
+            log(String.format("about to destroy..."));
+
+            runDestroy(sles_data);
+
+            int maxTry = 20;
+            int tryCount = 0;
+            //isDestroying = true;
+            while (isDestroying) {
+
+                try {
+                    sleep(40);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+
+                tryCount++;
+
+                log("destroy try: " + tryCount);
+
+                if (tryCount >= maxTry) {
+                    hasDestroyingErrors = true;
+                    log("WARNING: waited for max time to properly destroy JNI.");
+                    break;
+                }
+            }
+            log(String.format("after destroying. TotalSamplesRead = %d", totalSamplesRead));
+
+            if (totalSamplesRead==0)
+            {
+                hasDestroyingErrors = true;
+            }
+
+            endTest();
+        }
+    }
+
+    public void setMessageHandler(Handler messageHandler) {
+        mMessageHandler = messageHandler;
+    }
+
+    private void runDestroy(final long sles_data ) {
+        isDestroying = true;
+
+        //start thread
+
+        final long local_sles_data = sles_data;
+        ////
+        Thread thread = new Thread(new Runnable() {
+            public void run() {
+                isDestroying = true;
+                log("**Start runnable destroy");
+
+                int status = slesDestroy(local_sles_data);
+                log(String.format("**End runnable destroy sles delete status: %d", status));
+                isDestroying = false;
+            }
+        });
+
+        thread.start();
+
+
+
+        log("end of runDestroy()");
+
+
+    }
+
+    public void togglePlay() {
+
+    }
+
+    public void runTest() {
+
+
+    }
+
+   public void endTest() {
+       log("--Ending capture test--");
+       isPlaying = false;
+
+
+       if (mMessageHandler != null) {
+           Message msg = Message.obtain();
+           if (hasDestroyingErrors)
+               msg.what = NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS;
+           else
+               msg.what = NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE;
+           mMessageHandler.sendMessage(msg);
+       }
+
+   }
+
+    public void finish() {
+
+        if (isRunning) {
+            isRunning = false;
+            try {
+                sleep(20);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static void log(String msg) {
+        Log.v("Loopback", msg);
+    }
+
+    double [] getWaveData () {
+        return mvSamples;
+    }
+
+}  //end thread.