Merge "Fix Missing Screen Density" into gingerbread
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index 112b1bf..1a77577 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -25,7 +25,7 @@
 
 LOCAL_PACKAGE_NAME := CtsVerifier
 
-LOCAL_JNI_SHARED_LIBRARIES := libctsverifier_jni
+LOCAL_JNI_SHARED_LIBRARIES := libctsverifier_jni libaudioquality
 
 LOCAL_SDK_VERSION := current
 
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 5577f38..d5a1c84 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -21,6 +21,9 @@
       android:versionName="1.0">
       
     <uses-sdk android:minSdkVersion="5"></uses-sdk>
+    
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
 
     <application android:label="@string/app_name">
 
@@ -71,5 +74,26 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_sensors" />
         </activity>
-    </application>
+
+      <activity android:name=".audioquality.AudioQualityVerifierActivity"
+                android:label="@string/aq_verifier"
+                android:launchMode="singleTask"
+                android:screenOrientation="portrait">
+          <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" />
+      </activity>
+      
+      <activity android:name=".audioquality.CalibrateVolumeActivity"
+                android:label="@string/aq_calibrate_volume_name" />
+      
+      <activity android:name=".audioquality.ViewResultsActivity"
+                android:label="@string/aq_view_results_name" />
+
+      <service android:name=".audioquality.ExperimentService" />
+      
+   </application>
+
 </manifest> 
diff --git a/apps/CtsVerifier/assets/audioquality/pink_10000_3s b/apps/CtsVerifier/assets/audioquality/pink_10000_3s
new file mode 100644
index 0000000..e248a50
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/pink_10000_3s
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/pink_5000_1s b/apps/CtsVerifier/assets/audioquality/pink_5000_1s
new file mode 100644
index 0000000..6d88505
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/pink_5000_1s
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/pink_5000_2s b/apps/CtsVerifier/assets/audioquality/pink_5000_2s
new file mode 100644
index 0000000..4acb0ca
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/pink_5000_2s
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_1 b/apps/CtsVerifier/assets/audioquality/stim_dt_1
new file mode 100644
index 0000000..65830bd
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_1
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_2 b/apps/CtsVerifier/assets/audioquality/stim_dt_2
new file mode 100644
index 0000000..c61e7c8
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_2
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_3 b/apps/CtsVerifier/assets/audioquality/stim_dt_3
new file mode 100644
index 0000000..be5f755
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_3
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_31 b/apps/CtsVerifier/assets/audioquality/stim_dt_31
new file mode 100644
index 0000000..8ff5b20
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_31
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_4 b/apps/CtsVerifier/assets/audioquality/stim_dt_4
new file mode 100644
index 0000000..a9c0497
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_4
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_5 b/apps/CtsVerifier/assets/audioquality/stim_dt_5
new file mode 100644
index 0000000..73c2777
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_5
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_6 b/apps/CtsVerifier/assets/audioquality/stim_dt_6
new file mode 100644
index 0000000..381c79a
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_6
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_66 b/apps/CtsVerifier/assets/audioquality/stim_dt_66
new file mode 100644
index 0000000..84964ec
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_66
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_7 b/apps/CtsVerifier/assets/audioquality/stim_dt_7
new file mode 100644
index 0000000..c304764
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_7
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_8 b/apps/CtsVerifier/assets/audioquality/stim_dt_8
new file mode 100644
index 0000000..7479f34
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_8
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/stim_dt_9 b/apps/CtsVerifier/assets/audioquality/stim_dt_9
new file mode 100644
index 0000000..02f5276
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/stim_dt_9
Binary files differ
diff --git a/apps/CtsVerifier/assets/audioquality/tone_250 b/apps/CtsVerifier/assets/audioquality/tone_250
new file mode 100644
index 0000000..ed411f5
--- /dev/null
+++ b/apps/CtsVerifier/assets/audioquality/tone_250
Binary files differ
diff --git a/apps/CtsVerifier/jni/Android.mk b/apps/CtsVerifier/jni/Android.mk
index 98a4678..4343259 100644
--- a/apps/CtsVerifier/jni/Android.mk
+++ b/apps/CtsVerifier/jni/Android.mk
@@ -14,19 +14,4 @@
 # limitations under the License.
 #
 
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libctsverifier_jni
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_PRELINK_MODULE := false
-
-LOCAL_SRC_FILES := \
-		CtsVerifierJniOnLoad.cpp \
-		com_android_cts_verifier_os_FileUtils.cpp	
-
-LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
-
-include $(BUILD_SHARED_LIBRARY)
+include $(call all-subdir-makefiles)
diff --git a/apps/CtsVerifier/jni/audioquality/Android.mk b/apps/CtsVerifier/jni/audioquality/Android.mk
new file mode 100644
index 0000000..565cec2
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2010 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CPP_EXTENSION := .cpp
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRELINK_MODULE := false
+
+LOCAL_MODULE    := libaudioquality
+LOCAL_SRC_FILES := Fft.cpp Window.cpp GlitchTest.cpp MeasureRms.cpp \
+   OverflowCheck.cpp LinearityTest.cpp CompareSpectra.cpp \
+   GenerateSinusoid.cpp Wrapper.cpp
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/apps/CtsVerifier/jni/audioquality/CompareSpectra.cpp b/apps/CtsVerifier/jni/audioquality/CompareSpectra.cpp
new file mode 100644
index 0000000..262a400
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/CompareSpectra.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+// An amplitude-normalized spectrum comparison method.
+
+#include "Fft.h"
+#include "Window.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+/* Find the endpoints of the signal stored in data such that the rms of
+   the found segment exceeds signalOnRms.  data is of length n.  The
+   RMS calculations used to find the endpoints use a window of length
+   step and advance step samples.  The approximate, conservative
+   endpoint sample indices are returned in start and end. */
+static void findEndpoints(short* data, int n, int step, float signalOnRms,
+                          int* start, int* end) {
+    int size = step;
+    *start = *end = 0;
+    int last_frame = n - size;
+    for (int frame = 0; frame < last_frame; frame += step) {
+        double sum = 0.0;
+        for (int i=0; i < size; ++i) {
+            float val = data[i + frame];
+            sum += (val * val);
+        }
+        float rms = sqrt(sum / size);
+        if (! *start) {
+            if (rms >= signalOnRms) {
+                *start = frame + size;
+            }
+            continue;
+        } else {
+            if (rms < signalOnRms) {
+                *end = frame - size;
+                // fprintf(stderr, "n:%d onset:%d offset:%d\n", n, *start, *end);
+                return;
+            }
+        }
+    }
+    // Handle the case where the signal does not drop below threshold
+    // after onset.
+    if ((*start > 0) && (! *end)) {
+        *end = n - size - 1;
+    }
+}
+
+// Sum the magnitude squared spectra.
+static void accumulateMagnitude(float* im, float* re, int size, double* mag) {
+    for (int i = 0; i < size; ++i) {
+        mag[i] += ((re[i] * re[i]) + (im[i] * im[i]));
+    }
+}
+
+/* Return a pointer to 1+(fftSize/2) spectrum magnitude values
+   averaged over all of the numSamples in pcm.  It is the
+   responsibility of the caller to free this magnitude array.  Return
+   NULL on failure.  Use 50% overlap on the spectra. An FFT of
+   fftSize points is used to compute the spectra, The overall signal
+   rms over all frequencies between lowestBin and highestBin is
+   returned as a scalar in rms. */
+double* getAverageSpectrum(short* pcm, int numSamples, int fftSize,
+                           int lowestBin, int highestBin, float* rms) {
+    if (numSamples < fftSize) return NULL;
+    int numFrames = 1 + ((2 * (numSamples - fftSize)) / fftSize);
+    int numMag = 1 + (fftSize / 2);
+    float* re = new float[fftSize];
+    float* im = new float[fftSize];
+    double* mag = new double[numMag];
+    for (int i = 0; i < numMag; ++i) {
+        mag[i] = 0.0;
+    }
+    Window wind(fftSize);
+    Fft ft;
+    int pow2 = ft.fftPow2FromWindowSize(fftSize);
+    ft.fftInit(pow2);
+    int input_p = 0;
+    for (int i = 0; i < numFrames; ++i) {
+        wind.window(pcm + input_p, re, 0.0);
+        for (int j = 0; j < fftSize; ++j) {
+            im[j] = 0.0;
+        }
+        ft.fft(re,im);
+        accumulateMagnitude(im, re, numMag, mag);
+        input_p += fftSize / 2;
+    }
+    double averageEnergy = 0.0; // per frame average energy
+    for (int i = 0; i < numMag; ++i) {
+        double e = mag[i]/numFrames;
+        if ((i >= lowestBin) && (i <= highestBin))
+            averageEnergy += e;
+        mag[i] = sqrt(e);
+    }
+    *rms = sqrt(averageEnergy / (highestBin - lowestBin + 1));
+    delete [] re;
+    delete [] im;
+    return mag;
+}
+
+/* Compare the average magnitude spectra of the signals in pcm and
+   refPcm, which are of length numSamples and numRefSamples,
+   respectively; both sampled at sample_rate.  The maximum deviation
+   between average spectra, expressed in dB, is returned in
+   maxDeviation, and the rms of all dB variations is returned in
+   rmsDeviation.  Note that a lower limit is set on the frequencies that
+   are compared so as to ignore irrelevant DC and rumble components.  If
+   the measurement fails for some reason, return 0; else return 1, for
+   success.  Causes for failure include the amplitude of one or both of
+   the signals being too low, or the duration of the signals being too
+   short.
+
+   Note that the expected signal collection scenario is that the phone
+   would be stimulated with a broadband signal as in a recognition
+   attempt, so that there will be some "silence" regions at the start and
+   end of the pcm signals.  The preferred stimulus would be pink noise,
+   but any broadband signal should work. */
+int compareSpectra(short* pcm, int numSamples, short* refPcm,
+                   int numRefSamples, float sample_rate,
+                   float* maxDeviation, float* rmsDeviation) {
+    int fftSize = 512;           // must be a power of 2
+    float lowestFreq = 100.0;    // don't count DC, room rumble, etc.
+    float highestFreq = 3500.0;  // ignore most effects of sloppy anti alias filters.
+    int lowestBin = int(0.5 + (lowestFreq * fftSize / sample_rate));
+    int highestBin = int(0.5 + (highestFreq * fftSize / sample_rate));
+    float signalOnRms = 1000.0; // endpointer RMS on/off threshold
+    int endpointStepSize = int(0.5 + (sample_rate * 0.02)); // 20ms setp
+    float rms1 = 0.0;
+    float rms2 = 0.0;
+    int onset = 0;
+    int offset = 0;
+    findEndpoints(refPcm, numRefSamples, endpointStepSize, signalOnRms,
+                   &onset, &offset);
+    double* spect1 = getAverageSpectrum(refPcm + onset, offset - onset,
+                                        fftSize, lowestBin, highestBin, &rms1);
+    findEndpoints(pcm, numSamples, endpointStepSize, signalOnRms,
+                  &onset, &offset);
+    double* spect2 = getAverageSpectrum(pcm + onset, offset - onset,
+                                        fftSize, lowestBin, highestBin, &rms2);
+    int magSize = 1 + (fftSize/2);
+    if ((rms1 <= 0.0) || (rms2 <= 0.0))
+        return 0; // failure because one or both signals are too short or
+                  // too low in amplitude.
+    float rmsNorm = rms2 / rms1; // compensate for overall gain differences
+    // fprintf(stderr, "Level normalization: %f dB\n", 20.0 * log10(rmsNorm));
+    *maxDeviation = 0.0;
+    float sumDevSquared = 0.0;
+    for (int i = lowestBin; i <= highestBin; ++i) {
+        double val = 1.0;
+        if ((spect1[i] > 0.0) && (spect2[i] > 0.0)) {
+            val = 20.0 * log10(rmsNorm * spect1[i] / spect2[i]);
+        }
+        sumDevSquared += val * val;
+        if (fabs(val) > fabs(*maxDeviation)) {
+            *maxDeviation = val;
+        }
+        // fprintf(stderr, "%d %f\n", i, val);
+    }
+    *rmsDeviation = sqrt(sumDevSquared / (highestBin - lowestBin + 1));
+    delete [] spect1;
+    delete [] spect2;
+    return 1; // success
+}
+
+
diff --git a/apps/CtsVerifier/jni/audioquality/CompareSpectra.h b/apps/CtsVerifier/jni/audioquality/CompareSpectra.h
new file mode 100644
index 0000000..8b473e1
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/CompareSpectra.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef COMPARE_SPECTRA_H
+#define COMPARE_SPECTRA_H
+
+/* Compare the average magnitude spectra of the signals in pcm and
+   refPcm, which are of length numSamples and nRefSamples,
+   respectively; both sampled at sample_rate.  The maximum deviation
+   between average spectra, expressed in dB, is returned in
+   maxDeviation, and the rms of all dB variations is returned in
+   rmsDeviation.  Note that a lower limit is set on the frequencies that
+   are compared so as to ignore irrelevant DC and rumble components.  If
+   the measurement fails for some reason, return 0; else return 1, for
+   success.  Causes for failure include the amplitude of one or both of
+   the signals being too low, or the duration of the signals being too
+   short.
+
+   Note that the expected signal collection scenario is that the phone
+   would be stimulated with a broadband signal as in a recognition
+   attempt, so that there will be some "silence" regions at the start and
+   end of the pcm signals.  The preferred stimulus would be pink noise,
+   but any broadband signal should work. */
+
+int compareSpectra(short* pcm, int numSamples, short* refPcm,
+                   int nRefSamples, float sampleRate,
+                   float* maxDeviation, float* rmsDeviation);
+
+#endif // COMPARE_SPECTRA_H
diff --git a/apps/CtsVerifier/jni/audioquality/Fft.cpp b/apps/CtsVerifier/jni/audioquality/Fft.cpp
new file mode 100644
index 0000000..867bc86
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Fft.cpp
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 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 "Fft.h"
+
+#include <stdlib.h>
+#include <math.h>
+
+Fft::Fft(void) : mCosine(0), mSine(0), mFftSize(0), mFftTableSize(0) { }
+
+Fft::~Fft(void) {
+    fftCleanup();
+}
+
+/* Construct a FFT table suitable to perform a DFT of size 2^power. */
+void Fft::fftInit(int power) {
+    fftCleanup();
+    fftMakeTable(power);
+}
+
+void Fft::fftCleanup() {
+        delete [] mSine;
+        delete [] mCosine;
+        mSine = NULL;
+        mCosine = NULL;
+        mFftTableSize = 0;
+        mFftSize = 0;
+}
+
+/* z <- (10 * log10(x^2 + y^2))    for n elements */
+int Fft::fftLogMag(float *x, float *y, float *z, int n) {
+    float *xp, *yp, *zp, t1, t2, ssq;
+
+    if(x && y && z && n) {
+        for(xp=x+n, yp=y+n, zp=z+n; zp > z;) {
+            t1 = *--xp;
+            t2 = *--yp;
+            ssq = (t1*t1)+(t2*t2);
+            *--zp = (ssq > 0.0)? 10.0 * log10((double)ssq) : -200.0;
+        }
+        return 1;    //true
+    } else {
+        return 0;    // false/fail
+    }
+}
+
+int Fft::fftMakeTable(int pow2) {
+    int lmx, lm;
+    float *c, *s;
+    double scl, arg;
+
+    mFftSize = 1 << pow2;
+    mFftTableSize = lmx = mFftSize/2;
+    mSine = new float[lmx];
+    mCosine = new float[lmx];
+    scl = (M_PI*2.0)/mFftSize;
+    for (s=mSine, c=mCosine, lm=0; lm<lmx; lm++ ) {
+        arg = scl * lm;
+        *s++ = sin(arg);
+        *c++ = cos(arg);
+    }
+    mBase = (mFftTableSize * 2)/mFftSize;
+    mPower2 = pow2;
+    return(mFftTableSize);
+}
+
+
+/* Compute the discrete Fourier transform of the 2**l complex sequence
+ * in x (real) and y (imaginary).  The DFT is computed in place and the
+ * Fourier coefficients are returned in x and y.
+ */
+void Fft::fft( float *x, float *y ) {
+    float c, s, t1, t2;
+    int j1, j2, li, lix, i;
+    int lmx, lo, lixnp, lm, j, nv2, k=mBase, im, jm, l = mPower2;
+
+    for (lmx=mFftSize, lo=0; lo < l; lo++, k *= 2) {
+        lix = lmx;
+        lmx /= 2;
+        lixnp = mFftSize - lix;
+        for (i=0, lm=0; lm<lmx; lm++, i += k ) {
+            c = mCosine[i];
+            s = mSine[i];
+            for ( li = lixnp+lm, j1 = lm, j2 = lm+lmx; j1<=li;
+                  j1+=lix, j2+=lix ) {
+                t1 = x[j1] - x[j2];
+                t2 = y[j1] - y[j2];
+                x[j1] += x[j2];
+                y[j1] += y[j2];
+                x[j2] = (c * t1) + (s * t2);
+                y[j2] = (c * t2) - (s * t1);
+            }
+        }
+    }
+
+    /* Now perform the bit reversal. */
+    j = 1;
+    nv2 = mFftSize/2;
+    for ( i=1; i < mFftSize; i++ ) {
+        if ( j < i ) {
+            jm = j-1;
+            im = i-1;
+            t1 = x[jm];
+            t2 = y[jm];
+            x[jm] = x[im];
+            y[jm] = y[im];
+            x[im] = t1;
+            y[im] = t2;
+        }
+        k = nv2;
+        while ( j > k ) {
+            j -= k;
+            k /= 2;
+        }
+        j += k;
+    }
+}
+
+/* Compute the discrete inverse Fourier transform of the 2**l complex
+ * sequence in x (real) and y (imaginary).  The DFT is computed in
+ * place and the Fourier coefficients are returned in x and y.  Note
+ * that this DOES NOT scale the result by the inverse FFT size.
+ */
+void Fft::ifft(float *x, float *y ) {
+    float c, s, t1, t2;
+    int j1, j2, li, lix, i;
+    int lmx, lo, lixnp, lm, j, nv2, k=mBase, im, jm, l = mPower2;
+
+    for (lmx=mFftSize, lo=0; lo < l; lo++, k *= 2) {
+        lix = lmx;
+        lmx /= 2;
+        lixnp = mFftSize - lix;
+        for (i=0, lm=0; lm<lmx; lm++, i += k ) {
+            c = mCosine[i];
+            s = - mSine[i];
+            for ( li = lixnp+lm, j1 = lm, j2 = lm+lmx; j1<=li;
+                        j1+=lix, j2+=lix ) {
+                t1 = x[j1] - x[j2];
+                t2 = y[j1] - y[j2];
+                x[j1] += x[j2];
+                y[j1] += y[j2];
+                x[j2] = (c * t1) + (s * t2);
+                y[j2] = (c * t2) - (s * t1);
+            }
+        }
+    }
+
+    /* Now perform the bit reversal. */
+    j = 1;
+    nv2 = mFftSize/2;
+    for ( i=1; i < mFftSize; i++ ) {
+        if ( j < i ) {
+            jm = j-1;
+            im = i-1;
+            t1 = x[jm];
+            t2 = y[jm];
+            x[jm] = x[im];
+            y[jm] = y[im];
+            x[im] = t1;
+            y[im] = t2;
+        }
+        k = nv2;
+        while ( j > k ) {
+            j -= k;
+            k /= 2;
+        }
+        j += k;
+    }
+}
+
+int Fft::fftGetSize(void) { return mFftSize; }
+
+int Fft::fftGetPower2(void) { return mPower2; }
diff --git a/apps/CtsVerifier/jni/audioquality/Fft.h b/apps/CtsVerifier/jni/audioquality/Fft.h
new file mode 100644
index 0000000..6a21d9a
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Fft.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef LEGACY_TALKIN_FFT_H
+#define LEGACY_TALKIN_FFT_H
+
+class Fft {
+public:
+    Fft(void);
+
+    virtual ~Fft(void);
+
+    // Prepare for radix-2 FFT's of size (1<<pow2) 
+    void fftInit(int pow2);
+
+    // Forward fft.  Real time-domain components in x, imaginary in y 
+    void fft(float *x, float *y);
+
+    // Inverse fft.  Real frequency-domain components in x, imaginary in y 
+    void ifft(float *x, float *y);
+
+    // Compute the dB-scaled log-magnitude spectrum from the real spectal
+    // amplitude values in 'x', and imaginary values in 'y'.  Return the
+    // magnitude spectrum in z.  Compute 'n' components. 
+    int fftLogMag(float *x, float *y, float *z, int n);
+
+    int fftGetSize();
+
+    int fftGetPower2();
+
+    // Return the power of 2 required to contain at least size samples.
+    static int fftPow2FromWindowSize(int size) {
+        int pow2 = 1;
+        while ((1 << pow2) < size)
+            pow2++;
+        return pow2;
+    }
+
+private:
+    // Free up memory and reset the static globals. 
+    void fftCleanup();
+
+    // Create the sine/cosine basis tables and return the size of the FFT
+    // corresponding to pow2. 
+    int fftMakeTable(int pow2);
+
+    float* mSine;
+    float* mCosine;
+    int mFftTableSize;
+    int mFftSize;
+    int mPower2;
+    int mBase;
+};
+
+#endif // LEGACY_TALKIN_FFT_H 
diff --git a/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.cpp b/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.cpp
new file mode 100644
index 0000000..1823d58
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+// Generate a sinusoidal signal with optional onset and offset ramps.
+
+#include <stdlib.h>
+#include <math.h>
+
+static inline short clipAndRound(float val) {
+    if (val > 32767.0)
+        return 32767;
+    if (val < -32768.0)
+        return -32768;
+    if (val >= 0.0)
+        return static_cast<short>(0.5 + val);
+    return static_cast<short>(val - 0.5);
+}
+
+/* Return a sinusoid of frequency freq Hz and amplitude ampl in output
+   of length numOutput.  If ramp > 0.0, taper the ends of the signal
+   with a half-raised-cosine function.  It is up to the caller to
+   delete[] output.  If this call fails due to unreasonable arguments,
+   numOutput will be zero, and output will be NULL.  Note that the
+   duration of the up/down ramps will be within the specified
+   duration.  Note that if amplitude is specified outside of the
+   numerical range of int16, the signal will be clipped at +- 32767. */
+void generateSinusoid(float freq, float duration, float sampleRate,
+                      float amplitude, float ramp,
+                      int* numOutput, short** output) {
+    // Sanity check
+    if ((duration < (2.0 * ramp)) || ((freq * 2.0) > sampleRate) || (ramp < 0.0)) {
+        *output = NULL;
+        *numOutput = 0;
+        return;
+    }
+    int numSamples = int(0.5 + (sampleRate * duration));
+    double arg = M_PI * 2.0 * freq / sampleRate;
+    short* wave = new short[numSamples];
+    int numRamp = int(0.5 + (sampleRate * ramp));
+    for (int i = 0; i < numSamples; ++i) {
+        float val = amplitude * sin(arg * i);
+        if (numRamp > 0) {
+            if (i < numRamp) {
+                float gain = (0.5 - (0.5 * cos((0.5 + i) * M_PI / numRamp)));
+                val *= gain;
+            } else {
+                if (i > (numSamples - numRamp - 1)) {
+                    float gain = (0.5 - (0.5 * cos((0.5 + (numSamples - i - 1))
+                            * M_PI / numRamp)));
+                    val *= gain;
+                }
+            }
+        }
+        wave[i] = clipAndRound(val);
+    }
+    *numOutput = numSamples;
+    *output = wave;
+}
diff --git a/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.h b/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.h
new file mode 100644
index 0000000..eeedca6
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/GenerateSinusoid.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef GENERATE_SINUSOID_H
+#define GENERATE_SINUSOID_H
+
+/* Return a sinusoid of frequency freq Hz and amplitude ampl in output
+   of length numOutput.  If ramp > 0.0, taper the ends of the signal
+   with a half-raised-cosine function.  It is up to the caller to
+   delete[] output.  If this call fails due to unreasonable arguments,
+   numOutput will be zero, and output will be NULL.  Note that the
+   duration of the up/down ramps will be within the specified
+   duration.  Note that if amplitude is specified outside of the
+   numerical range of int16, the signal will be clipped at +- 32767. */
+void generateSinusoid(float freq, float duration, float sampleRate,
+                      float amplitude, float ramp,
+                      int* numOutput, short** output);
+
+#endif // GENERATE_SINUSOID_H
+
diff --git a/apps/CtsVerifier/jni/audioquality/GlitchTest.cpp b/apps/CtsVerifier/jni/audioquality/GlitchTest.cpp
new file mode 100644
index 0000000..2605f77
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/GlitchTest.cpp
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010 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 "GlitchTest.h"
+#include "Window.h"
+#include "Fft.h"
+
+#include <math.h>
+
+GlitchTest::GlitchTest(void) : mRe(0), mIm(0), mFt(0), mWind(0) {}
+
+void GlitchTest::init(float sampleRate, float stimFreq, float onsetThresh,
+                      float dbSnrThresh) {
+    cleanup(); // in case Init gets called multiple times.
+    // Analysis parameters
+    float windowDur = 0.025;    // Duration of the analysis window in seconds. 
+    float frameInterval = 0.01; // Interval in seconds between frame onsets 
+    float lowestFreq = 200.0;   // Lowest frequency to include in
+                                // background noise power integration.
+    mSampleRate = sampleRate;
+    mOnsetThresh = onsetThresh;
+    mDbSnrThresh = dbSnrThresh;
+    mFrameStep = (int)(0.5 + (mSampleRate * frameInterval));
+    mWindowSize = (int)(0.5 + (mSampleRate * windowDur));
+    mWind = new Window(mWindowSize);
+    mFt = new Fft;
+    mFt->fftInit(mFt->fftPow2FromWindowSize(mWindowSize));
+    mFftSize = mFt->fftGetSize();
+    mRe = new float[mFftSize];
+    mIm = new float[mFftSize];
+    float freqInterval = mSampleRate / mFftSize;
+    // We can exclude low frequencies from consideration, since the
+    // phone may have DC offset in the A/D, and there may be rumble in
+    // the testing room.    
+    mLowestSpectrumBin = (int)(0.5 + (lowestFreq / freqInterval));
+    // These are the bin indices within which most of the energy due to
+    // the (windowed) tone should be found. 
+    mLowToneBin = (int)(0.5 + (stimFreq / freqInterval)) - 2;
+    mHighToneBin = (int)(0.5 + (stimFreq / freqInterval)) + 2;
+    if (mLowestSpectrumBin >= mLowToneBin) {
+        mLowestSpectrumBin = mHighToneBin + 1;
+    }
+}
+
+int GlitchTest::checkToneSnr(short* pcm, int numSamples, float* duration,
+                             int* numBadFrames) {
+    *numBadFrames = 0;
+    *duration = 0.0;
+    if (!(mRe && mIm)) {
+        return -1; // not initialized.
+    }
+    int n_frames = 1 + ((numSamples - mWindowSize) / mFrameStep);
+    if (n_frames < 4) { // pathologically short input signal 
+        return -2;
+    }
+    *numBadFrames = 0;
+    int onset = -1;
+    int offset = -1;
+    for (int frame = 0; frame < n_frames; ++frame) {
+        int numSpectra = 0;
+        mWind->window(pcm + frame*mFrameStep, mRe, 0.0);
+        realMagSqSpectrum(mRe, mWindowSize, mRe, &numSpectra);
+        int maxLoc = 0;
+        float maxValue = 0.0;
+        findPeak(mRe, mLowestSpectrumBin, numSpectra, &maxLoc, &maxValue);
+        // possible states: (1) before tone onset; (2) within tone
+        // region; (3) after tone offset.
+        if ((onset < 0) && (offset < 0)) { // (1) 
+            if ((maxLoc >= mLowToneBin) && (maxLoc <= mHighToneBin)
+                    && (maxValue > mOnsetThresh)) {
+                onset = frame;
+            }
+            continue;
+        }
+        if ((onset >= 0) && (offset < 0)) { // (2) 
+            if (frame > (onset + 2)) { // let the framer get completely
+                                       // into the tonal signal
+                double sumNoise = 1.0; // init. to small non-zero vals to
+                                       // avoid log or divz problems
+                double sumSignal = 1.0;
+                float snr = 0.0;
+                if (maxValue < mOnsetThresh) {
+                    offset = frame;
+                    *duration = mFrameStep * (offset - onset) / mSampleRate;
+                    if (*numBadFrames >= 1) {
+                        (*numBadFrames) -= 1; // account for expected glitch at
+                                            // signal offset
+                    }
+                    continue;
+                }
+                for (int i = mLowestSpectrumBin; i < mLowToneBin; ++i) {
+                    sumNoise += mRe[i]; // note that mRe contains the magnitude
+                                        // squared spectrum.
+                }
+                for (int i = mLowToneBin; i <= mHighToneBin; ++i)
+                    sumSignal += mRe[i];
+                for (int i = mHighToneBin + 1; i < numSpectra; ++i) {
+                    sumNoise += mRe[i]; // Note: mRe has the mag squared spectrum.
+                }
+                snr = 10.0 * log10(sumSignal / sumNoise);
+                if (snr < mDbSnrThresh)
+                    (*numBadFrames) += 1;
+            }
+            continue;
+        }
+        if ((onset >= 0) && (offset > 0)) { // (3)
+            if ((maxLoc >= mLowToneBin) && (maxLoc <= mHighToneBin) &&
+                    (maxValue > mOnsetThresh)) { // tone should not pop up again!
+                (*numBadFrames) += 1;
+            }
+            continue;
+        }
+    }
+    if ((onset >= 0) && (offset > 0))
+        return 1;  // Success.
+    if (onset < 0) {
+        return -3; // Signal onset not found.
+    }
+    return -4;     // Signal offset not found.
+}
+
+void GlitchTest::cleanup(void) {
+    delete [] mRe;
+    delete [] mIm;
+    delete mFt;
+    delete mWind;
+    mRe = 0;
+    mIm = 0;
+    mWind = 0;
+    mFt = 0;
+}
+
+int GlitchTest::realMagSqSpectrum(float* data, int numInput,
+                                  float* output, int* numOutput) {
+    *numOutput = 0;
+    if ((numInput <= 0) || (numInput > mFftSize))
+        return 0;
+    int i = 0;
+    for (i = 0; i < numInput; ++i) {
+        mRe[i] = data[i];
+        mIm[i] = 0.0;
+    }
+    for ( ; i < mFftSize; ++i) {
+        mRe[i] = 0.0;
+        mIm[i] = 0.0;
+    }
+    mFt->fft(mRe, mIm);
+    *numOutput = 1 + (mFftSize / 2);
+    for (i = 0; i < *numOutput; ++i) {
+        output[i] = (mRe[i] * mRe[i]) + (mIm[i] * mIm[i]);
+    }
+    return 1;
+}
+
+void GlitchTest::findPeak(float* data, int startSearch, int endSearch,
+                          int* maxLoc, float* maxValue) {
+    float amax = data[startSearch];
+    int loc = startSearch;
+    for (int i = startSearch + 1; i < endSearch; ++i) {
+        if (data[i] > amax) {
+            amax = data[i];
+            loc = i;
+        }
+    }
+    *maxLoc = loc;
+    *maxValue = 10.0 * log10(amax / mWindowSize);
+}
+
diff --git a/apps/CtsVerifier/jni/audioquality/GlitchTest.h b/apps/CtsVerifier/jni/audioquality/GlitchTest.h
new file mode 100644
index 0000000..de96e6e
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/GlitchTest.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef GLITCH_TEST_H
+#define GLITCH_TEST_H
+
+class Fft;
+class Window;
+
+class GlitchTest {
+public:
+    GlitchTest(void);
+
+    virtual ~GlitchTest(void) {
+        cleanup();
+    }
+
+    /* Set up the instance to operate on input test signals sampled at
+       sample_rate that contain a stimulis tone of stim_freq frequency.
+       The signal will be considered on during the interval it exceeds
+       onset_thresh (dB re 1.0).  Any frames containing a tone that have
+       a signal energy to out-of-band energy ratio less than
+       db_snr_thresh are counted as bad frames.  Init must be called
+       before CheckToneSnr. */
+    void init(float sample_rate, float stim_freq, float onset_thresh,
+              float db_snr_thresh);
+
+    /* Analyze the n_samples of the lin16 signal in pcm.  This signal is
+       assumed sampled at sample_rate, and to contain a sinusoid with
+       center frequency stim_freq, embedded somewhere in time, with
+       "silence" intervals before and after.  The contiguous duration of
+       the tone that exceeds onset_thresh (in dB re 1.0) is returned as
+       seconds in duration. The number of frames for which the ratio of
+       the energy at the tone frequency to the energy in the rest of the
+       spectrum is less than db_snr_thresh is returned in n_bad_frames.
+       If the test succeed, the method returns 1, else it returns a
+       negative number that reflects the cause of failure as follows:
+         -1     The instance is not initialized.
+         -2     There are not enough samples to do a reasonable check.
+         -3     The tone signal onset was not found.
+         -4     The tone signal end was not found. */
+    int checkToneSnr(short* pcm, int n_samples, float* duration,
+                     int* n_bad_frames);
+
+private:
+    // Free memory, etc.
+    void cleanup(void);
+
+    /* Do a real FFT on the n_input samples in data, and return n_output
+       power spectral density points in output.  The output points include
+       DC through the Nyquist frequency (i.e. 1 + fft_size/2).  output
+       must be large enough to accommodate this size. If n_input==0 or
+       n_input > fft_size, return 0; else return 1. */
+    int realMagSqSpectrum(float* data, int n_input,
+                             float* output, int* n_output);
+
+    /* Find the largest value in data starting at start_search and ending
+       at end_search-1.  The values in data are assumed to be magnitude
+       squared values from a spectrum computation based on window_size
+       sample points.  Return the index where the largest value was found,
+       and return the dB (re 1.0) equivalent of the highest magnitude. */
+    void findPeak(float* data, int start_search, int end_search,
+                  int* max_loc, float* max_value);
+
+    // Real and Imaginary analysis arrays.
+    float* mRe;
+    float* mIm;
+    // Fourier transform and window.
+    Fft* mFt;
+    Window* mWind;
+    // Derived parameters and other variables.
+    float mSampleRate;
+    int mFrameStep;
+    int mWindowSize;
+    int mFftSize;
+    float mOnsetThresh;
+    float mDbSnrThresh;
+    int mLowestSpectrumBin;
+    int mLowToneBin;
+    int mHighToneBin;
+};
+
+#endif // GLITCH_TEST_H
diff --git a/apps/CtsVerifier/jni/audioquality/LinearityTest.cpp b/apps/CtsVerifier/jni/audioquality/LinearityTest.cpp
new file mode 100644
index 0000000..f8cf9ac
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/LinearityTest.cpp
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+/* This test accepts a collection of N speech waveforms collected as
+   part of N recognition attempts.  The waveforms are ordered by
+   increasing presentation level.  The test determines the extent to
+   which the peak amplitudes in the waveforms track the change in
+   presentation level.  Failure to track the presentation level within
+   some reasonable margin is an indication of clipping or of automatic
+   gain control in the signal path.
+
+   The speech stimuli that are used for this test should simply be
+   replications of exactly the same speech signal presented at
+   different levels.  It is expected that all recognition attempts on
+   this signal result in correct recognition.  A warning, but not a
+   hard failure, should be issued if any of the attempts fail.  The
+   hard failure criterion for this test should be only based on the
+   amplitude linearity tracking. */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+
+/* Keep a record of the top N absolute values found using a slow
+   bubble-sort.  This is OK, since the use of this program is not time
+   critical, and N is usually small.  Note that the argument n = N-1. */
+static void bubbleUp(int* store, int n, short val) {
+    if (val < store[n])
+        return;
+    for (int i = 0; i <= n; ++i) {
+        if (val >= store[i]) {
+            for (int j = n; j > i ; j--)
+                store[j] = store[j-1];
+            store[i] = val;
+            return;
+        }
+    }
+}
+
+/* Make two measurements on the signal of length numSamples sampled at
+   sampleRate in pcm: the RMS of the highest amplitude 30ms segment
+   (returned in peakRms), and the RMS of the top 50 peak absolute
+   values found (returned in peakAverage).  If the signal is too
+   short to make reasonable measurements, the function returns 0, else
+   it returns 1. */
+static int peakLevels(short* pcm, int numSamples, float sampleRate,
+                      float* peakAverage, float* peakRms) {
+    float rmsFrameSize = 0.03;
+    float rmsFrameStep = 0.01;
+    int frameStep = int(0.5 + (sampleRate * rmsFrameStep));
+    int frameSize = int(0.5 + (sampleRate * rmsFrameSize));
+    int numFrames = 1 + ((numSamples - frameSize) / frameStep);
+
+    if (numFrames < 10) {
+        return 0; // failure for too short signal
+    }
+
+    // Peak RMS calculation
+    double maxEnergy = 0.0;
+    for (int frame = 0; frame < numFrames; ++frame) {
+        double energy = 0.0;
+        int limit = (frame * frameStep) + frameSize;
+        for (int i = frame * frameStep; i < limit; ++i) {
+            double s = pcm[i];
+            energy += s * s;
+        }
+        if (energy > maxEnergy) {
+            maxEnergy = energy;
+        }
+    }
+    *peakRms = sqrt(maxEnergy / frameSize);
+
+    // Find the absolute highest topN peaks in the signal and compute
+    // the RMS of their values.
+    int topN = 50; // The number of highest peaks over which to average.
+    int topM = topN - 1;
+    int* maxVal = new int[topN];
+    for (int i = 0; i < topN; ++i) {
+        maxVal[i] = 0;
+    }
+    for (int i = 0; i < numSamples; ++i) {
+        if (pcm[i] >= 0) {
+            bubbleUp(maxVal, topM, pcm[i]);
+        } else {
+            bubbleUp(maxVal, topM, -pcm[i]);
+        }
+    }
+    float sum = 0.0;
+    // The RMS is taken bacause we want the values of the highest peaks
+    // to dominate.
+    for (int i = 0; i < topN; ++i) {
+        float fval = maxVal[i];
+        sum += (fval * fval);
+    }
+    delete [] maxVal;
+    *peakAverage = sqrt(sum/topN);
+    return 1; // success
+}
+
+/* There are numSignals int16 signals in pcms.  sampleCounts is an
+   integer array of length numSignals containing their respective
+   lengths in samples.  They are all sampled at sampleRate.  The pcms
+   are ordered by increasing stimulus level.  The level steps between
+   successive stimuli were of size dbStepSize dB.  The signal with
+   index referenceStim (0 <= referenceStim < numSignals) should be in
+   an amplitude range that is reasonably certain to be linear (e.g. at
+   normal speaking levels).  The maximum deviation in linearity found
+   (in dB) is returned in maxDeviation.  The function returns 1 if
+   the measurements could be made, or a negative number that
+   indicates the error, as follows:
+      -1 The input signals or sample counts are missing.
+      -2 The number of input signals is < 2.
+      -3 The specified sample rate is <= 4000.0
+      -4 The dB step size for the increase in stimulus level is <= 0.0
+      -5 The specified reverence stimulus number is out of range.
+      -6 One or more of the stimuli is too short in duration. */
+int linearityTest(short** pcms, int* sampleCounts, int numSignals,
+                  float sampleRate, float dbStepSize, int referenceStim,
+                  float* maxDeviation) {
+    if (!(pcms && sampleCounts)) {
+        return -1; // Input signals or sample counts are missing
+    }
+    if (numSignals < 2) {
+        return -2; // the number of input signals must be >= 2;
+    }
+    if (sampleRate <= 4000.0) {
+        return -3; // The sample rate must be > 4000 Hz.
+    }
+    if (dbStepSize <= 0.0) {
+        return -4; // The dB step size must be > 0.0
+    }
+    if (!((referenceStim >= 0) && (referenceStim < numSignals))) {
+        return -5; // (0 <= referenceStim < numSignals) must be true
+    }
+    float* peakAverage = new float[numSignals];
+    float* peakRms = new float[numSignals];
+    for (int sig = 0; sig < numSignals; ++sig) {
+        if (!peakLevels(pcms[sig], sampleCounts[sig],
+             sampleRate, peakAverage + sig, peakRms + sig)) {
+            return -6; // failure because a signal is too short.
+        }
+    }
+    float peakAverageRef = peakAverage[referenceStim];
+    float peakRmsRef = peakRms[referenceStim];
+    float maxDev = 0.0;
+    for (int i = 0; i < numSignals; ++i) {
+        float dbAverage = 20.0 * log10(peakAverage[i]/peakAverageRef);
+        float dbRms = 20.0 * log10(peakRms[i]/peakRmsRef);
+        float reference = dbStepSize * (i - referenceStim);
+        float average_level = 0.5 * (dbAverage + dbRms);
+        float dev = fabs(average_level - reference);
+        // fprintf(stderr,"dbAverage:%f dbRms:%f reference:%f dev:%f\n",
+        //         dbAverage, dbRms, reference, dev);
+        if (dev > maxDev)
+            maxDev = dev;
+    }
+    *maxDeviation = maxDev;
+    return 1;
+}
diff --git a/apps/CtsVerifier/jni/audioquality/LinearityTest.h b/apps/CtsVerifier/jni/audioquality/LinearityTest.h
new file mode 100644
index 0000000..d92566a
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/LinearityTest.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef LINEARITY_TEST_H
+#define LINEARITY_TEST_H
+
+/* There are numSignals int16 signals in pcms.  sampleCounts is an
+   integer array of length numSignals containing their respective
+   lengths in samples.  They are all sampled at sampleRate.  The pcms
+   are ordered by increasing stimulus level.  The level steps between
+   successive stimuli were of size dbStepSize dB.  The signal with
+   index referenceStim (0 <= referenceStim < numSignals) should be in
+   an amplitude range that is reasonably certain to be linear (e.g. at
+   normal speaking levels).  The maximum deviation in linearity found
+   (in dB) is returned in maxDeviation.  The function returns 1 if
+   the measurements could be made, or a negative number that
+   indicates the error, as follows:
+      -1 The input signals or sample counts are missing.
+      -2 The number of input signals is < 2.
+      -3 The specified sample rate is <= 4000.0
+      -4 The dB step size for the increase in stimulus level is <= 0.0/
+      -5 The specified reverence stimulus number is out of range.
+      -6 One or more of the stimuli is too short in duration. */
+int linearityTest(short** pcms, int* sampleCounts, int numSignals,
+                  float sampleRate, float dbStepSize,
+                  int referenceStim, float* maxDeviation);
+
+
+#endif /* LINEARITY_TEST_H */
diff --git a/apps/CtsVerifier/jni/audioquality/MeasureRms.cpp b/apps/CtsVerifier/jni/audioquality/MeasureRms.cpp
new file mode 100644
index 0000000..640b191
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/MeasureRms.cpp
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2010 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 <math.h>
+
+/* Return the median of the n values in "values".
+   Uses a stupid bubble sort, but is only called once on small array. */
+float getMedian(float* values, int n) {
+    if (n <= 0)
+        return 0.0;
+    if (n == 1)
+        return values[0];
+    if (n == 2)
+        return 0.5 * (values[0] + values[1]);
+    for (int i = 1; i < n; ++i)
+        for (int j = i; j < n; ++j) {
+            if (values[j] < values[i-1]) {
+                float tmp = values[i-1];
+                values[i-1] = values[j];
+                values[j] = tmp;
+            }
+        }
+    int ind = int(0.5 + (0.5 * n)) - 1;
+    return values[ind];
+}
+
+float computeAndRemoveMean(short* pcm, int numSamples) {
+    float sum = 0.0;
+
+    for (int i = 0; i < numSamples; ++i)
+        sum += pcm[i];
+    short mean;
+    if (sum >= 0.0)
+        mean = (short)(0.5 + (sum / numSamples));
+    else
+        mean = (short)((sum / numSamples) - 0.5);
+    for (int i = 0; i < numSamples; ++i)
+        pcm[i] -= mean;
+    return sum / numSamples;
+}
+
+void measureRms(short* pcm, int numSamples, float sampleRate, float onsetThresh,
+                float* rms, float* stdRms, float* mean, float* duration) {
+    *rms = 0.0;
+    *stdRms = 0.0;
+    *duration = 0.0;
+    float frameDur = 0.025;    // Both the duration and interval of the
+                                // analysis frames.
+    float calInterval = 0.250; // initial part of signal used to
+                                // establish background level (seconds).
+    double sumFrameRms = 1.0;
+    float sumSampleSquares = 0.0;
+    double sumFrameSquares = 1.0; // init. to small number to avoid
+                                    // log and divz problems.
+    int frameSize = (int)(0.5 + (sampleRate * frameDur));
+    int numFrames = numSamples / frameSize;
+    int numCalFrames = int(0.5 + (calInterval / frameDur));
+    if (numCalFrames < 1)
+        numCalFrames = 1;
+    int frame = 0;
+
+    *mean = computeAndRemoveMean(pcm, numSamples);
+
+    if (onsetThresh < 0.0) { // Handle the case where we want to
+                              // simply measure the RMS of the entire
+                              // input sequence.
+        for (frame = 0; frame < numFrames; ++frame) {
+            short* p_data = pcm + (frame * frameSize);
+            int i;
+            for (i = 0, sumSampleSquares = 0.0; i < frameSize; ++i) {
+                float samp = p_data[i];
+                sumSampleSquares += samp * samp;
+            }
+            sumSampleSquares /= frameSize;
+            sumFrameSquares += sumSampleSquares;
+            double localRms = sqrt(sumSampleSquares);
+            sumFrameRms += localRms;
+        }
+        *rms = sumFrameRms / numFrames;
+        *stdRms = sqrt((sumFrameSquares / numFrames) - (*rms * *rms));
+        *duration = frameSize * numFrames / sampleRate;
+        return;
+    }
+
+    /* This handles the case where we look for a target signal against a
+       background, and expect the signal to start some time after the
+       beginning, and to finish some time before the end of the input
+       samples. */
+    if (numFrames < (3 * numCalFrames)) {
+        return;
+    }
+    float* calValues = new float[numCalFrames];
+    float calMedian = 0.0;
+    int onset = -1;
+    int offset = -1;
+
+    for (frame = 0; frame < numFrames; ++frame) {
+        short* p_data = pcm + (frame * frameSize);
+        int i;
+        for (i = 0, sumSampleSquares = 1.0; i < frameSize; ++i) {
+            float samp = p_data[i];
+            sumSampleSquares += samp * samp;
+        }
+        sumSampleSquares /= frameSize;
+        /* We handle three states: (1) before the onset of the signal; (2)
+           within the signal; (3) following the signal.  The signal is
+           assumed to be at least onsetThresh dB above the background
+           noise, and that at least one frame of silence/background
+           precedes the onset of the signal. */
+        if (onset < 0) { // (1)
+            sumFrameSquares += sumSampleSquares;
+            if (frame < numCalFrames ) {
+                calValues[frame] = sumSampleSquares;
+                continue;
+            }
+            if (frame == numCalFrames) {
+                calMedian = getMedian(calValues, numCalFrames);
+                if (calMedian < 10.0)
+                    calMedian = 10.0; // avoid divz, etc.
+            }
+            float ratio = 10.0 * log10(sumSampleSquares / calMedian);
+            if (ratio > onsetThresh) {
+                onset = frame;
+                sumFrameSquares = 1.0;
+                sumFrameRms = 1.0;
+            }
+            continue;
+        }
+        if ((onset > 0) && (offset < 0)) { // (2)
+            int sig_frame = frame - onset;
+            if (sig_frame < numCalFrames) {
+                calValues[sig_frame] = sumSampleSquares;
+            } else {
+                if (sig_frame == numCalFrames) {
+                    calMedian = getMedian(calValues, numCalFrames);
+                    if (calMedian < 10.0)
+                        calMedian = 10.0; // avoid divz, etc.
+                }
+                float ratio = 10.0 * log10(sumSampleSquares / calMedian);
+                int denFrames = frame - onset - 1;
+                if (ratio < (-onsetThresh)) { // found signal end
+                    *rms = sumFrameRms / denFrames;
+                    *stdRms = sqrt((sumFrameSquares / denFrames) - (*rms * *rms));
+                    *duration = frameSize * (frame - onset) / sampleRate;
+                    offset = frame;
+                    continue;
+                }
+            }
+            sumFrameSquares += sumSampleSquares;
+            double localRms = sqrt(sumSampleSquares);
+            sumFrameRms += localRms;
+            continue;
+        }
+        if (offset > 0) { // (3)
+            /* If we have found the real signal end, the level should stay
+               low till data end.  If not, flag this anomaly by increasing the
+               reported duration. */
+            float localRms = 1.0 + sqrt(sumSampleSquares);
+            float localSnr = 20.0 * log10(*rms / localRms);
+            if (localSnr < onsetThresh)
+                *duration = frameSize * (frame - onset) / sampleRate;
+            continue;
+        }
+    }
+    delete [] calValues;
+}
+
diff --git a/apps/CtsVerifier/jni/audioquality/MeasureRms.h b/apps/CtsVerifier/jni/audioquality/MeasureRms.h
new file mode 100644
index 0000000..78b43ae
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/MeasureRms.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef MEASURE_RMS_H
+#define MEASURE_RMS_H
+
+/* Measure the rms of the non-silence segment of the signal in pcm, which
+   is of numSamples length, and sampled at sampleRate.  pcm is assumed to
+   consist of silence - signal - silence, as might be logged during a
+   speech recognition attempt.  The stimulus signal in this case should
+   be approximately a 3-second burst of pink noise presented at a level
+   comparable to normal speaking level.  The RMS is measured using 25ms
+   duration non-overlapping windows.  These are averaged over the whole
+   non-silence part of pcm, and the result is returned in rms.  The
+   standard deviation of this measurement over all frames is returned in
+   stdRms, and the estimated duration of the non-silence region, in
+   seconds, is returned in duration.  The target signal is taken to be
+   that segment that is onsetThresh dB above the background, and is
+   expected to be continuous, once the onset has been found.  If
+   onsetThresh < 0.0, simply make the measurememt over the entire pcm
+   signal.  In both cases, the mean of the entire signal is returned in
+   mean. */
+void measureRms(short* pcm, int numSamples, float sampleRate,
+                float onsetThresh, float* rms, float* stdRms,
+                float* mean, float* duration);
+
+
+#endif /* MEASURE_RMS_H */
diff --git a/apps/CtsVerifier/jni/audioquality/OverflowCheck.cpp b/apps/CtsVerifier/jni/audioquality/OverflowCheck.cpp
new file mode 100644
index 0000000..20e9285
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/OverflowCheck.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+/* This assumes an input signal that has at least a few hundred
+   milliseconds of high-amplitude sinusoidal signal.  The signal is
+   expected to be a relatively low-frequency sinusoid (200-400 Hz).
+   If the signal is linearly reproduced or clipped, There will be no
+   large step changes in value from one sample to the next.  On the
+   other hand, if the signal contains numerical overflow
+   (wrap-around), very large excursions will be produced.
+
+   This program first searches for the high-amplitude recorded
+   segment, then examines just that part of the signal for "large
+   excursions", and returns the results of the search as four
+   integers:
+
+   n_wraps signal_size sine_start sine_end
+
+   where n_wraps is the number of anomolous value jumps found,
+   signal_size is the number of lin16 samples found in the file,
+   sine_start and sine_end are the limits of the region searched for
+   anomolous jumps. */
+
+#include <stdlib.h>
+#include <math.h>
+
+// MAX_ALLOWED_STEP is the largest sample-to-sample change that will
+// be considered "normal" for 250 Hz signals sampled at 8kHz.  This is
+// calibrated by the largest sample-to-sample change that is naturally
+// present in a 250 Hz sine wave with amplitude of 40000 (which is
+// actually 7804).
+#define MAX_ALLOWED_STEP 16000
+
+// This is the RMS value that is expected to be exceded by a sinusoid
+// with a peak amplitude of 32767 (actually 23169).
+#define SIGNAL_ON_RMS 12000.0
+
+static void findEndpoints(short* data, int n, int step, int* start, int* end) {
+    int size = step;
+    *start = *end = 0;
+    int last_frame = n - size;
+    for (int frame = 0; frame < last_frame; frame += step) {
+        double sum = 0.0;
+        for (int i=0; i < size; ++i) {
+            float val = data[i + frame];
+            sum += (val * val);
+        }
+        float rms = sqrt(sum / size);
+        if (! *start) {
+            if (rms >= SIGNAL_ON_RMS) {
+                *start = frame + size;
+            }
+            continue;
+        } else {
+            if (rms < SIGNAL_ON_RMS) {
+                *end = frame - size;
+                return;
+            }
+        }
+    }
+    if ((*start > 0) && (! *end)) {
+        *end = n - size - 1;
+    }
+}
+
+static void checkExcursions(short* data, int start, int end, int* numJumps,
+                            int* maxPeak, int* minPeak) {
+    *numJumps = 0;
+    int endm = end - 1;
+    if ((endm - start) < 3) {
+        *numJumps = -1;
+        return;
+    }
+    *maxPeak = *minPeak = data[start];
+    for (int i = start; i < endm; ++i) {
+        int v1 = data[i];
+        int v2 = data[i+1];
+        if (v1 > *maxPeak)
+            *maxPeak = v1;
+        if (v1 < *minPeak)
+            *minPeak = v1;
+        int diff = v2 - v1;
+        if (diff < 0)
+            diff = -diff;
+        if (diff > MAX_ALLOWED_STEP)
+            (*numJumps) += 1;
+    }
+    return;
+}
+
+int overflowCheck(short* pcm, int numSamples, float sampleRate,
+                  float* duration, int* numDeltas, int* onset, int* offset,
+                  int* maxPeak, int* minPeak) {
+    float windSize = 0.020;
+    int minBuff = int(2.0 * sampleRate); // must have 2 sec of data at least.
+
+    if(pcm && (numSamples >= minBuff)) {
+        int step = int(0.5 + (windSize * sampleRate));
+        *onset = 0;
+        *offset = 0;
+
+        findEndpoints(pcm, numSamples, step, onset, offset);
+        *numDeltas = -1;
+        checkExcursions(pcm, *onset, *offset, numDeltas, maxPeak, minPeak);
+        *duration = (*offset - *onset) / sampleRate;
+        return 1; // true/success
+    }
+    return 0; // failure
+}
diff --git a/apps/CtsVerifier/jni/audioquality/OverflowCheck.h b/apps/CtsVerifier/jni/audioquality/OverflowCheck.h
new file mode 100644
index 0000000..ea6d780
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/OverflowCheck.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef OVERFLOW_CHECK_H
+#define OVERFLOW_CHECK_H
+
+/* This is a clipping/overflow check.  This is designed to look for a
+   ~3-second duration ~250 Hz pure tone at a level sufficient to cause
+   about 20% clipping.  It examines the recorded waveform for possible
+   overflow/wraparound.  The input signal of numSamples total sampled at
+   sampleRate is in pcm. The expected signal contains silence - tone -
+   silence.  The approximate duration in seconds of the tone found is
+   returned in duration.  The number of unexpectedly-large jumps within
+   the tone is returned in numDeltas.  The approximate sample numbers of
+   the tone endpoints are returned in onset and offset.  The maximum
+   and minimum found in the signal located between onset and offset are
+   returned in maxPeak and minPeak, respectively.  The function
+   return is 1 if the operation was sucessful, 0 on failure. */
+int overflowCheck(short* pcm, int numSamples, float sampleRate,
+                  float* duration, int* numDeltas, int* onset, int* offset,
+                  int* maxPeak, int* minPeak);
+
+#endif // OVERFLOW_CHECK_H
diff --git a/apps/CtsVerifier/jni/audioquality/Window.cpp b/apps/CtsVerifier/jni/audioquality/Window.cpp
new file mode 100644
index 0000000..8e89ed4
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Window.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 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 "Window.h"
+
+#include <stdlib.h>
+#include <math.h>
+
+void Window::windowCreate(int size) {
+    if (mWindowWeights && (mWindowSize < size)) {
+        delete [] mWindowWeights;
+        mWindowWeights = NULL;
+        mWindowSize = 0;
+    }
+    if (!mWindowWeights) {
+        mWindowWeights = new float[size];
+    }
+    if (mWindowSize != size) {
+        double arg = M_PI * 2.0 / size;
+        mWindowSize = size;
+        for (int i = 0; i < size; ++i) {
+            mWindowWeights[i] = 0.5 - (0.5 * cos((i + 0.5) * arg));
+        }
+    }
+}
+
+void Window::windowCleanup() {
+    mWindowSize = 0;
+    delete [] mWindowWeights;
+    mWindowWeights = NULL;
+}
+
+/* Multiply the signal in data by the window weights.  Place the
+   resulting mWindowSize floating-point values in output.  If preemp
+   is != 0.0, apply a 1st-order preemphasis filter, and assume that
+   there are mWindowSize+1 samples available in data. */
+void Window::window(short* data, float* output, float preemp) {
+    if (preemp == 0.0) {
+        for (int i = 0; i < mWindowSize; ++i)
+            output[i] = data[i] * mWindowWeights[i];
+    } else {
+        for (int i = 0; i < mWindowSize; ++i)
+            output[i] = (data[i+1] - (preemp * data[i])) * mWindowWeights[i];
+    }
+}
+
diff --git a/apps/CtsVerifier/jni/audioquality/Window.h b/apps/CtsVerifier/jni/audioquality/Window.h
new file mode 100644
index 0000000..08298354
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Window.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#ifndef WINDOW_INTERFACE_H
+#define WINDOW_INTERFACE_H
+
+class Window {
+public:
+    Window(int size) : mWindowWeights(0), mWindowSize(0) {
+        windowCreate(size);
+    }
+
+    Window() : mWindowWeights(0), mWindowSize(0) {
+    }
+
+    virtual ~Window(void) {
+        windowCleanup();
+    }
+
+    /* Create a Hann window of length size.  This allocates memory that
+       should be freed using window_cleanup(). */
+    void windowCreate(int size);
+
+    /* Free up memory and reset size to 0. */
+    void windowCleanup(void);
+
+
+    /* Multiply the signal in data by the window weights.  Place the
+       resulting window_size floating-point values in output.  If preemp
+       is != 0.0, apply a 1st-order preemphasis filter, and assume that
+       there are window_size+1 samples available in data. */
+    void window(short* data, float* output, float preemp);
+
+    int getWindowSize() { return mWindowSize; }
+
+    float* getWindowWeights() { return mWindowWeights; }
+
+private:
+    float* mWindowWeights;
+    int mWindowSize;
+
+};
+
+#endif /* WINDOW_INTERFACE_H */
diff --git a/apps/CtsVerifier/jni/audioquality/Wrapper.cpp b/apps/CtsVerifier/jni/audioquality/Wrapper.cpp
new file mode 100644
index 0000000..bee15c6
--- /dev/null
+++ b/apps/CtsVerifier/jni/audioquality/Wrapper.cpp
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+// Wrapper to the native phone test signal processing library, which
+// exposes an interface suitable for calling via JNI.
+
+#include <stdlib.h>
+#include <jni.h>
+
+#include "GenerateSinusoid.h"
+#include "MeasureRms.h"
+#include "GlitchTest.h"
+#include "OverflowCheck.h"
+#include "CompareSpectra.h"
+#include "LinearityTest.h"
+
+typedef short *shortPtr;
+
+extern "C" {
+    JNIEXPORT jshortArray JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_generateSinusoid(
+            JNIEnv *env, jobject obj,
+            jfloat freq, jfloat duration,
+            jfloat sampleRate, jfloat amplitude, jfloat ramp);
+    JNIEXPORT jfloatArray JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_measureRms(
+            JNIEnv *env, jobject obj,
+            jshortArray jpcm, jfloat sampleRate, jfloat onsetThresh);
+    JNIEXPORT jfloatArray JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_glitchTest(
+            JNIEnv *env, jobject obj,
+            jfloat sampleRate, jfloat stimFreq, jfloat onsetThresh,
+            jfloat dbSnrThresh, jshortArray jpcm);
+    JNIEXPORT jfloatArray JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_overflowCheck(
+            JNIEnv *env, jobject obj,
+            jshortArray jpcm, jfloat sampleRate);
+    JNIEXPORT jfloatArray JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_compareSpectra(
+            JNIEnv *env, jobject obj,
+            jshortArray jpcm, jshortArray jrefPcm, jfloat sampleRate);
+    JNIEXPORT jfloat JNICALL
+        Java_com_android_cts_verifier_audioquality_Native_linearityTest(
+            JNIEnv *env, jobject obj,
+            jobjectArray jpcms,
+            jfloat sampleRate, jfloat dbStepSize, jint referenceStim);
+};
+
+/* Returns an array of sinusoidal samples.
+   If the arguments are invalid, returns an empty array. */
+JNIEXPORT jshortArray JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_generateSinusoid(
+        JNIEnv *env, jobject obj,
+        jfloat freq, jfloat duration,
+        jfloat sampleRate, jfloat amplitude, jfloat ramp) {
+    short *wave = NULL;
+    int numSamples = 0;
+
+    generateSinusoid(freq, duration, sampleRate, amplitude, ramp,
+            &numSamples, &wave);
+
+    jshortArray ja;
+    if (!numSamples) {
+        ja = env->NewShortArray(0);
+    } else {
+        ja = env->NewShortArray(numSamples);
+        env->SetShortArrayRegion(ja, 0, numSamples, wave);
+        delete[] wave;
+    }
+    return ja;
+}
+
+/* Returns an array of four floats.
+   ret[0] = RMS
+   ret[1] = standard deviation of the RMS
+   ret[2] = non-silent region duration
+   ret[3] = mean value
+*/
+JNIEXPORT jfloatArray JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_measureRms(
+        JNIEnv *env, jobject obj,
+        jshortArray jpcm, jfloat sampleRate, jfloat onsetThresh) {
+    float ret[4];
+    ret[0] = ret[1] = ret[2] = ret[3] = -1.0;
+    int numSamples = env->GetArrayLength(jpcm);
+    short *pcm = new short[numSamples];
+    env->GetShortArrayRegion(jpcm, 0, numSamples, pcm);
+
+    measureRms(pcm, numSamples, sampleRate, onsetThresh, ret, ret + 1,
+            ret + 3, ret + 2);
+
+    jfloatArray ja = env->NewFloatArray(4);
+    env->SetFloatArrayRegion(ja, 0, 4, ret);
+    return ja;
+}
+
+/* Returns an array of three floats.
+   ret[0] = #bad frames
+   ret[1] = error code
+   ret[2] = duration
+   Error code = 1 for success,
+               -1 if initialization failed,
+               -2 if insufficient samples
+               -3 if tone signal onset not found
+               -4 if tone signal end not found
+*/
+JNIEXPORT jfloatArray JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_glitchTest(
+        JNIEnv *env, jobject obj,
+        jfloat sampleRate, jfloat stimFreq, jfloat onsetThresh,
+        jfloat dbSnrThresh, jshortArray jpcm) {
+    float ret[3];
+    int numSamples = env->GetArrayLength(jpcm);
+    short *pcm = new short[numSamples];
+    env->GetShortArrayRegion(jpcm, 0, numSamples, pcm);
+
+    GlitchTest gt;
+    gt.init(sampleRate, stimFreq, onsetThresh, dbSnrThresh);
+    float duration = -1.0;
+    int badFrames = -1;
+    int success = gt.checkToneSnr(pcm, numSamples, &duration, &badFrames);
+    ret[0] = badFrames;
+    ret[1] = success;
+    ret[2] = duration;
+    jfloatArray ja = env->NewFloatArray(3);
+    env->SetFloatArrayRegion(ja, 0, 3, ret);
+    return ja;
+}
+
+/* Returns an array of seven floats.
+   ret[0] = num deltas
+   ret[1] = error code
+   ret[2] = duration
+   ret[3] = onset
+   ret[4] = offset
+   ret[5] = max peak
+   ret[6] = min peak
+   Error code = 1 for success, -1 for failure. */
+JNIEXPORT jfloatArray JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_overflowCheck(
+        JNIEnv *env, jobject obj,
+        jshortArray jpcm, jfloat sampleRate) {
+    float ret[7];
+    int numSamples = env->GetArrayLength(jpcm);
+    short *pcm = new short[numSamples];
+    env->GetShortArrayRegion(jpcm, 0, numSamples, pcm);
+
+    float duration = -1.0;
+    int numDeltas = -1, onset = -1, offset = -1;
+    int maxPeak = 0, minPeak = 0;
+    int success = overflowCheck(pcm, numSamples, sampleRate,
+            &duration, &numDeltas, &onset, &offset, &maxPeak, &minPeak);
+    ret[0] = numDeltas;
+    ret[1] = success ? 1 : -1;
+    ret[2] = duration;
+    ret[3] = onset;
+    ret[4] = offset;
+    ret[5] = maxPeak;
+    ret[6] = minPeak;
+    jfloatArray ja = env->NewFloatArray(7);
+    env->SetFloatArrayRegion(ja, 0, 7, ret);
+    return ja;
+}
+
+/* Returns an array of three floats.
+   ret[0] = max deviation,
+   ret[1] = error code,
+   ret[2] = rms deviation.
+   Error code = 1 for success, -1 for failure. */
+JNIEXPORT jfloatArray JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_compareSpectra(
+        JNIEnv *env, jobject obj,
+        jshortArray jpcm, jshortArray jrefPcm, jfloat sampleRate) {
+    float ret[3];
+    int numSamples = env->GetArrayLength(jpcm);
+    short *pcm = new short[numSamples];
+    env->GetShortArrayRegion(jpcm, 0, numSamples, pcm);
+    int nRefSamples = env->GetArrayLength(jrefPcm);
+    short *refPcm = new short[nRefSamples];
+    env->GetShortArrayRegion(jrefPcm, 0, nRefSamples, refPcm);
+
+    float maxDeviation = -1.0, rmsDeviation = -1.0;
+    int success = compareSpectra(pcm, numSamples, refPcm, nRefSamples,
+            sampleRate, &maxDeviation, &rmsDeviation);
+    ret[1] = success ? 1 : -1;
+
+    ret[0] = maxDeviation;
+    ret[2] = rmsDeviation;
+    jfloatArray ja = env->NewFloatArray(3);
+    env->SetFloatArrayRegion(ja, 0, 3, ret);
+    return ja;
+}
+
+/* Return maximum deviation from linearity in dB.
+   On failure returns:
+      -1.0 The input signals or sample counts are missing.
+      -2.0 The number of input signals is < 2.
+      -3.0 The specified sample rate is <= 4000.0
+      -4.0 The dB step size for the increase in stimulus level is <= 0.0
+      -5.0 The specified reverence stimulus number is out of range.
+      -6.0 One or more of the stimuli is too short in duration.
+*/
+JNIEXPORT jfloat JNICALL
+    Java_com_android_cts_verifier_audioquality_Native_linearityTest(
+        JNIEnv *env, jobject obj,
+        jobjectArray jpcms,
+        jfloat sampleRate, jfloat dbStepSize, jint referenceStim) {
+    int numSignals = env->GetArrayLength(jpcms);
+    int *sampleCounts = new int[numSignals];
+    short **pcms = new shortPtr[numSignals];
+    jshortArray ja;
+    for (int i = 0; i < numSignals; i++) {
+        ja = (jshortArray) env->GetObjectArrayElement(jpcms, i);
+        sampleCounts[i] = env->GetArrayLength(ja);
+        pcms[i] = new short[sampleCounts[i]];
+        env->GetShortArrayRegion(ja, 0, sampleCounts[i], pcms[i]);
+    }
+
+    float maxDeviation = -1.0;
+    int ret = linearityTest(pcms, sampleCounts, numSignals,
+            sampleRate, dbStepSize, referenceStim, &maxDeviation);
+    delete[] sampleCounts;
+    for (int i = 0; i < numSignals; i++) {
+        delete[] pcms[i];
+    }
+    delete[] pcms;
+    if (ret < 1) return ret;
+
+    return maxDeviation;
+}
diff --git a/apps/CtsVerifier/jni/verifier/Android.mk b/apps/CtsVerifier/jni/verifier/Android.mk
new file mode 100644
index 0000000..98a4678
--- /dev/null
+++ b/apps/CtsVerifier/jni/verifier/Android.mk
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2010 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := libctsverifier_jni
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRELINK_MODULE := false
+
+LOCAL_SRC_FILES := \
+		CtsVerifierJniOnLoad.cpp \
+		com_android_cts_verifier_os_FileUtils.cpp	
+
+LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/apps/CtsVerifier/jni/CtsVerifierJniOnLoad.cpp b/apps/CtsVerifier/jni/verifier/CtsVerifierJniOnLoad.cpp
similarity index 100%
rename from apps/CtsVerifier/jni/CtsVerifierJniOnLoad.cpp
rename to apps/CtsVerifier/jni/verifier/CtsVerifierJniOnLoad.cpp
diff --git a/apps/CtsVerifier/jni/com_android_cts_verifier_os_FileUtils.cpp b/apps/CtsVerifier/jni/verifier/com_android_cts_verifier_os_FileUtils.cpp
similarity index 100%
rename from apps/CtsVerifier/jni/com_android_cts_verifier_os_FileUtils.cpp
rename to apps/CtsVerifier/jni/verifier/com_android_cts_verifier_os_FileUtils.cpp
diff --git a/apps/CtsVerifier/res/drawable-hdpi/fs_error.png b/apps/CtsVerifier/res/drawable-hdpi/fs_error.png
new file mode 100644
index 0000000..8270104
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/fs_error.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/fs_good.png b/apps/CtsVerifier/res/drawable-hdpi/fs_good.png
new file mode 100644
index 0000000..7786ac7
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/fs_good.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/fs_indeterminate.png b/apps/CtsVerifier/res/drawable-hdpi/fs_indeterminate.png
new file mode 100644
index 0000000..68f51ba
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/fs_indeterminate.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/fs_warning.png b/apps/CtsVerifier/res/drawable-hdpi/fs_warning.png
new file mode 100644
index 0000000..12baca5
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/fs_warning.png
Binary files differ
diff --git a/apps/CtsVerifier/res/layout/aq_row.xml b/apps/CtsVerifier/res/layout/aq_row.xml
new file mode 100644
index 0000000..5db33f3
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/aq_row.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:orientation="horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:background="#FF000000"
+    >
+    <TextView
+        android:id="@+id/testName"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="22px"
+        android:textColor="#FFFFFF"
+        android:layout_margin="10px"
+        />
+    <TextView
+        android:id="@+id/testScore"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="right"
+        android:textSize="22px"
+        android:textColor="#FFFFFF"
+        android:layout_margin="10px"
+        />
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/aq_sound_level_meter.xml b/apps/CtsVerifier/res/layout/aq_sound_level_meter.xml
new file mode 100644
index 0000000..c3cc8f9
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/aq_sound_level_meter.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:background="#FF000000"
+    >
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20px"
+        android:gravity="center_horizontal"
+        android:textSize="18px"
+        android:textColor="#FFFFFF"
+        android:text="@string/aq_calibrate_volume_instructions"
+        />
+    <ProgressBar
+        android:id="@+id/slider"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:max="2000"
+        android:progress="1000"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:padding="10px"
+        />
+    <TextView
+        android:id="@+id/status"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textSize="18px"
+        android:textColor="#FFFFFF"
+        />
+    <Button
+        android:id="@+id/doneButton"
+        android:text="@string/aq_done"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20px"
+        />
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/aq_verifier_activity.xml b/apps/CtsVerifier/res/layout/aq_verifier_activity.xml
new file mode 100644
index 0000000..2bc4a65
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/aq_verifier_activity.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:background="#FF000000"
+    >
+    <FrameLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1.0"
+        >
+        <ListView
+            android:id="@+id/list"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            />
+        <ProgressBar
+            android:id="@+id/progress"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            android:layout_gravity="center"
+            android:visibility="invisible"
+            />
+    </FrameLayout>
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10px"
+        android:layout_marginBottom="10px"
+        >
+        <Button
+            android:id="@+id/calibrateButton"
+            android:text="@string/aq_calibrate"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+        <Button
+            android:id="@+id/runAllButton"
+            android:text="@string/aq_run_all"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+        <Button
+            android:id="@+id/stopButton"
+            android:text="@string/aq_stop"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+        <Button
+            android:id="@+id/viewResultsButton"
+            android:text="@string/aq_view_results"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+        <Button
+            android:id="@+id/clearButton"
+            android:text="@string/aq_clear"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+    </LinearLayout>
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/aq_view_results.xml b/apps/CtsVerifier/res/layout/aq_view_results.xml
new file mode 100644
index 0000000..b8f020d0
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/aq_view_results.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:background="#FF000000"
+    >
+    <ScrollView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1.0"
+        >
+        <TextView
+            android:id="@+id/textView"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:textSize="18px"
+            android:textColor="#FFFFFF"
+            />
+    </ScrollView>
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10px"
+        android:layout_marginBottom="10px"
+        >
+        <Button
+            android:id="@+id/dismissButton"
+            android:text="@string/aq_dismiss"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+        <Button
+            android:id="@+id/sendResultsButton"
+            android:text="@string/aq_email_results"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+    </LinearLayout>
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index c386a1b..0c9b198 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -31,6 +31,7 @@
 
     <!-- Strings for TestListActivity -->
     <string name="test_list_title">Manual Test List</string>
+    <string name="test_category_audio">Audio</string>
     <string name="test_category_sensors">Sensors</string>
     <string name="test_category_security">Security</string>
     <string name="test_category_features">Features</string>
@@ -67,4 +68,66 @@
     <string name="no_file_status">Could not stat file...</string>
     <string name="congratulations">Congratulations!</string>
     <string name="no_suid_files">No unauthorized suid files detected!</string>
+
+    <!--  Strings for Audio Quality Verifier -->
+    
+     <!-- Title for Audio Quality Verifier activity -->
+    <string name="aq_verifier">Audio Quality Verifier</string>
+    
+    <!-- Button labels for VerifierActivity -->
+    <string name="aq_calibrate">Calibrate</string>
+    <string name="aq_run_all">Run All</string>
+    <string name="aq_stop">Stop</string>
+    <string name="aq_view_results">Results</string>
+    <string name="aq_email_results">Send by email</string>
+    <string name="aq_clear">Clear</string>
+    
+    <!-- Title for ViewResultsActivity -->
+    <string name="aq_view_results_name">Audio Quality Results</string>
+    <!-- Button label for ViewResultsActivity -->
+    <string name="aq_dismiss">Dismiss</string>
+    <!-- E-mail subject line for test results -->
+    <string name="aq_subject">Android Audio Quality Verifier Test Results</string>
+    
+    <!--  Title for CalibrateVolumeActivity -->
+    <string name="aq_calibrate_volume_name">Calibrate Volume</string>
+    <!--  Instructions for calibrating the volume -->
+    <string name="aq_calibrate_volume_instructions">Adjust volume to the central point</string>
+    <!-- Button label for CalibrateVolumeActivity -->
+    <string name="aq_done">Done</string>
+    <!--  Status values for CalibrateVolumeActivity -->
+    <string name="aq_status_unknown">Status: unknown</string>
+    <string name="aq_status_low">Volume too low</string>
+    <string name="aq_status_high">Volume too high</string>
+    <string name="aq_status_ok">Volume OK</string>
+    
+    <!-- Experiment names -->
+    <string name="aq_default_exp">Unnamed experiment</string>
+    <string name="aq_sound_level_exp">Sound level check</string>
+    <string name="aq_spectrum_shape_exp">Spectrum shape test</string>
+    <string name="aq_glitch_exp">Glitch test</string>
+    <string name="aq_linearity_exp">Gain linearity test</string>
+    <string name="aq_clipping_exp">Clipping check</string>
+    <string name="aq_bias_exp">Bias measurement</string>
+    
+    <!-- Experiment outcomes -->
+    <string name="aq_fail">Fail</string>
+    <string name="aq_pass">Pass</string>
+    <string name="aq_complete">Complete</string>
+    
+    <!-- Experiment reports -->
+    <string name="aq_loopback_report">Experiment ran successfully.</string>
+    <string name="aq_bias_report">Mean = %1$.3g, tolerance = +/- %2$.0f\nRMS = %3$.0f, duration = %4$.1fs</string>
+    <string name="aq_clipping_report_error">Overflow check unsuccessful</string>
+    <string name="aq_clipping_report_short">Insufficient tone detected.\nExpected %1$.1fs tone; observed %2$.1fs</string>
+    <string name="aq_clipping_report_fail">"Clipping check failed due to discontinuities.\nObserved %1$d bad frames\nTone duration %2$.1fs\nMin peak = %3$.0f, max = %4$.0f</string>
+    <string name="aq_clipping_report_pass">"Observed %1$d bad frames\nTone duration %2$.1fs\nMin peak = %3$.0f, max = %4$.0f</string>
+    <string name="aq_linearity_report_error">Experiment failed, error code %1$g</string>
+    <string name="aq_linearity_report_normal">Deviation from linearity = %1$.3g dB\nMax allowed = %2$.1f dB</string>
+    <string name="aq_glitch_report_error">Error performing Glitch test.</string>
+    <string name="aq_glitch_report_exact">%1$d glitches detected; expected %2$d, duration %3$.1fs</string>
+    <string name="aq_glitch_report_range">%1$d glitches detected; expected %2$d-%3$d, duration %4$.1fs</string>
+    <string name="aq_level_report">RMS = %1$.0f, target = %2$.0f\nTolerance = %3$.1f%%\nDuration = %4$.1fs</string>
+    <string name="aq_spectrum_report_error">Cannot perform test.\nCheck volume is sufficiently high?</string>
+    <string name="aq_spectrum_report_normal">RMS deviation = %1$.2f\nMax allowed deviation = %2$.1f</string>
 </resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioAssets.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioAssets.java
new file mode 100644
index 0000000..9725a4c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioAssets.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AudioAssets {
+
+    private static final String TAG = "AudioQualityVerifier";
+
+    private static final String STIM_BASENAME = "audioquality/stim_dt_";
+
+    public static byte[] getStim(Context context, int which) {
+        return readAsset(context, STIM_BASENAME + which);
+    }
+
+    public static byte[] getPinkNoise(Context context, int ampl, int duration) {
+        return readAsset(context, "pink_" + ampl + "_" + duration + "s");
+    }
+
+    private static byte[] readAsset(Context context, String filename) {
+        AssetManager assetManager = context.getAssets();
+        InputStream ais;
+        try {
+            ais = assetManager.open(filename);
+        } catch (IOException e) {
+            Log.e(TAG, "Cannot load asset " + filename);
+            return null;
+        }
+        byte[] buffer = Utils.readFile(ais);
+        return buffer;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java
new file mode 100644
index 0000000..9487a50
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/AudioQualityVerifierActivity.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import com.android.cts.verifier.R;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.media.AudioFormat;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+/**
+ * Main UI for the Android Audio Quality Verifier.
+ */
+public class AudioQualityVerifierActivity extends Activity implements View.OnClickListener,
+        OnItemClickListener {
+    public static final String TAG = "AudioQualityVerifier";
+
+    public static final int SAMPLE_RATE = 16000;
+    public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
+    public static final int BYTES_PER_SAMPLE = 2;
+
+    // Intent Extra definitions, which must match those in
+    // com.google.android.voicesearch.speechservice.RecognitionController
+    public static final String EXTRA_RAW_AUDIO =
+            "android.speech.extras.RAW_AUDIO";
+
+    // Communication with ExperimentService
+    public static final String ACTION_EXP_STARTED =
+            "com.android.cts.verifier.audioquality.EXP_STARTED";
+    public static final String ACTION_EXP_FINISHED =
+            "com.android.cts.verifier.audioquality.EXP_FINISHED";
+    public static final String EXTRA_ABORTED =
+            "com.android.cts.verifier.audioquality.ABORTED";
+    public static final String EXTRA_EXP_ID =
+            "com.android.cts.verifier.audioquality.EXP_ID";
+    public static final String EXTRA_RUN_ALL =
+            "com.android.cts.verifier.audioquality.RUN_ALL";
+
+    // Communication with ViewResultsActivity
+    public static final String EXTRA_RESULTS =
+            "com.android.cts.verifier.audioquality.RESULTS";
+
+    private Button mCalibrateButton;
+    private Button mRunAllButton;
+    private Button mStopButton;
+    private Button mViewResultsButton;
+    private Button mClearButton;
+
+    private ListView mList;
+    private TwoColumnAdapter mAdapter;
+
+    private ProgressBar mProgress;
+
+    private ArrayList<Experiment> mExperiments;
+
+    private boolean mRunningExperiment;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.aq_verifier_activity);
+
+        mCalibrateButton = (Button) findViewById(R.id.calibrateButton);
+        mRunAllButton = (Button) findViewById(R.id.runAllButton);
+        mStopButton = (Button) findViewById(R.id.stopButton);
+        mViewResultsButton = (Button) findViewById(R.id.viewResultsButton);
+        mClearButton = (Button) findViewById(R.id.clearButton);
+
+        mCalibrateButton.setOnClickListener(this);
+        mRunAllButton.setOnClickListener(this);
+        mStopButton.setOnClickListener(this);
+        mViewResultsButton.setOnClickListener(this);
+        mClearButton.setOnClickListener(this);
+
+        mStopButton.setEnabled(false);
+
+        mProgress = (ProgressBar) findViewById(R.id.progress);
+
+        mList = (ListView) findViewById(R.id.list);
+        mAdapter = new TwoColumnAdapter(this);
+
+        mExperiments = VerifierExperiments.getExperiments(this);
+
+        BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                experimentReplied(intent);
+            }
+        };
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_EXP_STARTED);
+        filter.addAction(ACTION_EXP_FINISHED);
+        registerReceiver(receiver, filter);
+
+        fillAdapter();
+        mList.setAdapter(mAdapter);
+        mList.setOnItemClickListener(this);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mAdapter.notifyDataSetChanged(); // Update List UI
+    }
+
+    // Called when an experiment has completed
+    private void experimentReplied(Intent intent) {
+        String action = intent.getAction();
+        if (ACTION_EXP_STARTED.equals(action)) {
+            mStopButton.setEnabled(true);
+            mRunAllButton.setEnabled(false);
+        } else if (ACTION_EXP_FINISHED.equals(action)) {
+            boolean mRunAll = intent.getBooleanExtra(EXTRA_RUN_ALL, false);
+            boolean aborted = intent.getBooleanExtra(EXTRA_ABORTED, true);
+            int expID = intent.getIntExtra(EXTRA_EXP_ID, -1);
+            if (mRunAll && !aborted) {
+                while (expID < mExperiments.size() - 1) {
+                    if (runExperiment(++expID, true)) {
+                        // OK, experiment is running
+                        mAdapter.notifyDataSetChanged();
+                        return;
+                    }
+                    // Otherwise, loop back and try the next experiment
+                }
+            }
+            mStopButton.setEnabled(false);
+            mRunAllButton.setEnabled(true);
+            mRunningExperiment = false;
+            mProgress.setVisibility(ProgressBar.INVISIBLE);
+        }
+        mAdapter.notifyDataSetChanged();
+    }
+
+    // Implements AdapterView.OnItemClickListener
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        if (mRunningExperiment) return;
+        runExperiment(position, false);
+    }
+
+    // Begin an experiment. Returns false if the experiment is not enabled.
+    private boolean runExperiment(int which, boolean all) {
+        Experiment exp = mExperiments.get(which);
+        if (!exp.isEnabled()) return false;
+        Intent intent = new Intent(this, ExperimentService.class);
+        intent.putExtra(EXTRA_EXP_ID, which);
+        intent.putExtra(EXTRA_RUN_ALL, all);
+        startService(intent);
+        mRunningExperiment = true;
+        mAdapter.notifyDataSetChanged();
+        mProgress.setVisibility(ProgressBar.VISIBLE);
+        return true;
+    }
+
+    // Implements View.OnClickListener:
+    public void onClick(View v) {
+        if (v == mCalibrateButton) {
+            Intent intent = new Intent(this, CalibrateVolumeActivity.class);
+            startActivity(intent);
+        } else if (v == mRunAllButton) {
+            if (mRunningExperiment) return;
+            int expID = -1;
+            while (expID < mExperiments.size() - 1) {
+                if (runExperiment(++expID, true)) break;
+            }
+        } else if (v == mStopButton) {
+            Intent intent = new Intent(this, ExperimentService.class);
+            stopService(intent);
+        } else if (v == mViewResultsButton) {
+            Intent intent = new Intent(this, ViewResultsActivity.class);
+            intent.putExtra(EXTRA_RESULTS, genReport());
+            startActivity(intent);
+        } else if (v == mClearButton) {
+            clear();
+        }
+    }
+
+    private void fillAdapter() {
+        mAdapter.clear();
+        for (Experiment exp : mExperiments) {
+            mAdapter.add(exp.getName());
+        }
+    }
+
+    class TwoColumnAdapter extends ArrayAdapter<String> {
+        TwoColumnAdapter(Context context) {
+            super(context, R.layout.aq_row);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            LayoutInflater inflater = getLayoutInflater();
+            View row = inflater.inflate(R.layout.aq_row, parent, false);
+            TextView nameField = (TextView) row.findViewById(R.id.testName);
+            TextView scoreField = (TextView) row.findViewById(R.id.testScore);
+            Experiment exp = mExperiments.get(position);
+            nameField.setText(exp.getName());
+            scoreField.setText(exp.getScore());
+            if (exp.isRunning()) {
+                Typeface tf = nameField.getTypeface();
+                nameField.setTypeface(tf, 1);
+            }
+            if (!exp.isEnabled()) {
+                nameField.setTextColor(Color.GRAY);
+            }
+            return row;
+        }
+    }
+
+    private String genReport() {
+        StringBuilder sb = new StringBuilder();
+        for (Experiment exp : mExperiments) {
+            exp.getReport(sb);
+        }
+        return sb.toString();
+    }
+
+    private void clear() {
+        if (mRunningExperiment) {
+            Intent intent = new Intent(this, ExperimentService.class);
+            stopService(intent);
+        }
+        for (Experiment exp : mExperiments) {
+            exp.reset();
+        }
+        mAdapter.notifyDataSetChanged();
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java
new file mode 100644
index 0000000..067a3d5
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/BackgroundAudio.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Continuously play background noise in a loop, until halt() is called.
+ * Used to simulate noisy environments, and for sound level calibration.
+ */
+public class BackgroundAudio extends Thread {
+    public static final String TAG = "AudioQualityVerifier";
+
+    private static final int BUFFER_TIME = 100; // Time in ms to buffer for
+
+    private boolean mProceed;
+    private AudioTrack mAudioTrack;
+    private byte[] mData;
+    private int mPos;
+    private int mBufferSize;
+
+    public void halt() {
+        mProceed = false;
+    }
+
+    public BackgroundAudio(byte[] data) {
+        mProceed = true;
+        mData = data;
+        mPos = 0;
+
+        // Calculate suitable buffer size:
+        final int minBufferSize = (BUFFER_TIME * AudioQualityVerifierActivity.SAMPLE_RATE
+                * AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000;
+        final int minHardwareBufferSize =
+                AudioTrack.getMinBufferSize(AudioQualityVerifierActivity.SAMPLE_RATE,
+                        AudioFormat.CHANNEL_OUT_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT);
+        mBufferSize = Math.max(minHardwareBufferSize, minBufferSize);
+        Log.i(TAG, "minBufferSize = " + minBufferSize + ", minHWSize = " + minHardwareBufferSize
+                + ", bufferSize = " + mBufferSize);
+
+        // Start playback:
+        Log.i(TAG, "Looping " + data.length + " bytes of audio, buffer size " + mBufferSize);
+        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
+                AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
+                AudioQualityVerifierActivity.AUDIO_FORMAT, mBufferSize, AudioTrack.MODE_STREAM);
+        if (mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
+            writeAudio();
+            start(); // Begin background thread to push audio data
+            mAudioTrack.play();
+        } else {
+            Log.e(TAG, "Error initializing audio track.");
+        }
+    }
+
+    @Override
+    public void run() {
+        while (true) {
+            if (!mProceed) {
+                mAudioTrack.stop();
+                return; // End thread
+            }
+            writeAudio();
+        }
+    }
+
+    private void writeAudio() {
+        int len = mData.length;
+        int count;
+        int maxBytes = Math.min(mBufferSize, len - mPos);
+
+        count = mAudioTrack.write(mData, mPos, maxBytes);
+        if (count < 0) {
+            Log.e(TAG, "Error writing looped audio data");
+            halt();
+            return;
+        }
+        mPos += count;
+        if (mPos == len) {
+            mPos = 0; // Wraparound
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java
new file mode 100644
index 0000000..c6cf34a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/CalibrateVolumeActivity.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import com.android.cts.verifier.R;
+
+import android.app.Activity;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * Play a continuous sound and allow the user to monitor the sound level
+ * at the mic in real time, relative to the range the phone can detect.
+ * This is not an absolute sound level meter, but lets the speaker volume
+ * and position be adjusted so that the clipping point is known.
+ */
+public class CalibrateVolumeActivity extends Activity implements View.OnClickListener {
+    public static final String TAG = "AudioQualityVerifier";
+
+    public static final int OUTPUT_AMPL = 5000;
+    public static final float TARGET_RMS = 5000.0f;
+    public static final float TARGET_AMPL = (float) (TARGET_RMS * Math.sqrt(2.0));
+    private static final float FREQ = 625.0f;
+
+    private static final float TOLERANCE = 1.03f;
+
+    private static final int DEBOUNCE_TIME = 500; // Minimum time in ms between status text changes
+    public static final boolean USE_PINK = false;
+
+    private ProgressBar mSlider;
+    private Button mDoneButton;
+    private TextView mStatus;
+    private BackgroundAudio mBackgroundAudio;
+    private Monitor mMonitor;
+    private Handler mHandler;
+
+    private Native mNative;
+
+    enum Status { LOW, OK, HIGH, UNDEFINED }
+    private static int[] mStatusText = {
+        R.string.aq_status_low, R.string.aq_status_ok, R.string.aq_status_high };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.aq_sound_level_meter);
+
+        mSlider = (ProgressBar) findViewById(R.id.slider);
+        mStatus = (TextView) findViewById(R.id.status);
+        mStatus.setText(R.string.aq_status_unknown);
+
+        mDoneButton = (Button) findViewById(R.id.doneButton);
+        mDoneButton.setOnClickListener(this);
+
+        mNative = Native.getInstance();
+        mHandler = new UpdateHandler();
+    }
+
+    // Implements View.OnClickListener
+    public void onClick(View v) {
+        if (v == mDoneButton) {
+            finish();
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        final int DURATION = 1;
+        final float RAMP = 0.0f;
+        byte[] noise;
+        if (USE_PINK) {
+            noise = Utils.getPinkNoise(this, OUTPUT_AMPL, DURATION);
+        } else {
+            short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
+                    AudioQualityVerifierActivity.SAMPLE_RATE, OUTPUT_AMPL, RAMP);
+            noise = Utils.shortToByteArray(sinusoid);
+        }
+        float[] results = mNative.measureRms(Utils.byteToShortArray(noise),
+                AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
+        float rms = results[Native.MEASURE_RMS_RMS];
+        Log.i(TAG, "Stimulus amplitude " + OUTPUT_AMPL + ", RMS " + rms);
+        mBackgroundAudio = new BackgroundAudio(noise);
+        mMonitor = new Monitor();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mBackgroundAudio.halt();
+        mMonitor.halt();
+    }
+
+    private class UpdateHandler extends Handler {
+        private Status mState = Status.UNDEFINED;
+        private long mTimestamp = 0; // Time of last status change in ms
+
+        @Override
+        public void handleMessage(Message msg) {
+            int rms = msg.arg1;
+            int max = mSlider.getMax();
+            int progress = (max / 2 * rms) / Math.round(TARGET_RMS);
+            if (progress > max) progress = max;
+            mSlider.setProgress(progress);
+
+            Status state;
+            if (rms * TOLERANCE < TARGET_RMS) state = Status.LOW;
+            else if (rms > TARGET_RMS * TOLERANCE) state = Status.HIGH;
+            else state = Status.OK;
+            if (state != mState) {
+                long timestamp = System.currentTimeMillis();
+                if (timestamp - mTimestamp > DEBOUNCE_TIME) {
+                    mStatus.setText(mStatusText[state.ordinal()]);
+                    mState = state;
+                    mTimestamp = timestamp;
+                }
+            }
+        }
+    }
+
+    class Monitor extends Thread {
+        private static final int BUFFER_TIME = 100; // Min time in ms to buffer for
+        private static final int READ_TIME = 25;    // Max length of time in ms to read in one chunk
+        private static final boolean DEBUG = false;
+
+        private AudioRecord mRecord;
+        private int mSamplesToRead;
+        private byte[] mBuffer;
+        private boolean mProceed;
+
+        Monitor() {
+            mProceed = true;
+
+            mSamplesToRead = (READ_TIME * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
+            mBuffer = new byte[mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE];
+
+            final int minBufferSize = (BUFFER_TIME * AudioQualityVerifierActivity.SAMPLE_RATE *
+                    AudioQualityVerifierActivity.BYTES_PER_SAMPLE) / 1000;
+            final int minHardwareBufferSize = AudioRecord.getMinBufferSize(
+                    AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
+                    AudioQualityVerifierActivity.AUDIO_FORMAT);
+            final int bufferSize = Math.max(minHardwareBufferSize, minBufferSize);
+
+            mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
+                    AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
+                    AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
+            if (mRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+                Log.e(TAG, "Couldn't open audio for recording");
+                return;
+            }
+            mRecord.startRecording();
+            if (mRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+                Log.e(TAG, "Couldn't record");
+                return;
+            }
+
+            start(); // Begin background thread
+        }
+
+        public void halt() {
+            mProceed = false;
+        }
+
+        @Override
+        public void run() {
+            int maxBytes = mSamplesToRead * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+            int bytes;
+            while (true) {
+                if (!mProceed) {
+                    mRecord.stop();
+                    mRecord.release();
+                    return; // End thread
+                }
+                bytes = mRecord.read(mBuffer, 0, maxBytes);
+                if (bytes < 0) {
+                    if (bytes == AudioRecord.ERROR_INVALID_OPERATION) {
+                        Log.e(TAG, "Recording object not initalized");
+                    } else if (bytes == AudioRecord.ERROR_BAD_VALUE) {
+                        Log.e(TAG, "Invalid recording parameters");
+                    } else {
+                        Log.e(TAG, "Error during recording");
+                    }
+                    return;
+                }
+                if (bytes >= 2) {
+                    // Note: this won't work well if bytes is small (we should check)
+                    short[] samples = Utils.byteToShortArray(mBuffer, 0, bytes);
+                    float[] results = mNative.measureRms(samples,
+                            AudioQualityVerifierActivity.SAMPLE_RATE, -1.0f);
+                    float rms = results[Native.MEASURE_RMS_RMS];
+                    float duration = results[Native.MEASURE_RMS_DURATION];
+                    float mean = results[Native.MEASURE_RMS_MEAN];
+                    if (DEBUG) {
+                        // Confirm the RMS calculation
+                        float verifyRms = 0.0f;
+                        for (short x : samples) {
+                            verifyRms += x * x;
+                        }
+                        verifyRms /= samples.length;
+                        Log.i(TAG, "RMS: " + rms + ", bytes: " + bytes
+                                + ", duration: " + duration + ", mean: " + mean
+                                + ", manual RMS: " + Math.sqrt(verifyRms));
+                    }
+                    Message msg = mHandler.obtainMessage();
+                    msg.arg1 = Math.round(rms);
+                    mHandler.sendMessage(msg);
+                }
+            }
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Experiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Experiment.java
new file mode 100644
index 0000000..18727f4
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Experiment.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import com.android.cts.verifier.R;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * The base class for all audio experiments.
+ */
+public class Experiment implements Runnable {
+    protected static final String TAG = "AudioQualityVerifier";
+
+    private static final int DEFAULT_TIMEOUT = 5; // In seconds
+
+    private String mName;
+    private String mScore;
+    private String mReport;
+    private String mAudioFileName;
+
+    enum Status { NotStarted, Running, Stopped, Completed }
+    private Status mStatus;
+    private boolean mEnabled;
+
+    protected Native mNative;
+    protected Context mContext;
+    protected Terminator mTerminator;
+
+    public Experiment(boolean enable) {
+        mEnabled = enable;
+        mNative = Native.getInstance();
+        reset();
+    }
+
+    public void init(Context context) {
+        mName = lookupName(context);
+    }
+
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_default_exp);
+    }
+
+    protected String getString(int resId) {
+        return mContext.getString(resId);
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    public void reset() {
+        mStatus = Status.NotStarted;
+        mScore = "";
+        mReport = "";
+        mAudioFileName = null;
+    }
+
+    public void start() {
+        mStatus = Status.Running;
+    }
+
+    protected void setScore(String score) {
+        mScore = score;
+    }
+
+    protected void setReport(String report) {
+        mReport = report;
+    }
+
+    // Implements Runnable
+    public void run() {}
+
+    public void run(Context context, Terminator t) {
+        mContext = context;
+        mTerminator = t;
+        Thread thread = new Thread(this);
+        thread.start();
+    }
+
+    public void setRecording(byte[] data) {
+        // Save captured data to file
+        mAudioFileName = Utils.getExternalDir(mContext, this) + "/"
+            + Utils.cleanString(getName()) + ".raw";
+        Log.i(TAG, "Saving recorded data to " + mAudioFileName);
+        Utils.saveFile(mAudioFileName, data);
+    }
+
+    public String getAudioFileName() {
+        return mAudioFileName;
+    }
+
+    // Timeout in seconds
+    public int getTimeout() {
+        return DEFAULT_TIMEOUT;
+    }
+
+    public void cancel() {
+        mStatus = Status.Stopped;
+    }
+
+    public void stop() {
+        mStatus = Status.Completed;
+    }
+
+    public boolean isRunning() {
+        return mStatus == Status.Running;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getScore() {
+        switch (mStatus) {
+            case NotStarted:
+                return "-";
+            case Running:
+                return "...";
+            case Stopped:
+                return "-";
+            case Completed:
+                return mScore;
+        }
+        return "";
+    }
+
+    public void getReport(StringBuilder sb) {
+        sb.append(getName());
+        sb.append(": ");
+        sb.append(getScore());
+        sb.append("\n");
+        sb.append(mReport);
+        sb.append("\n\n");
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ExperimentService.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ExperimentService.java
new file mode 100644
index 0000000..95fd3b5
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ExperimentService.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Launch an experiment.
+ *
+ * Experiments are decoupled from the UI both so that they can run on a
+ * background thread without freezing the UI, and to support experiments
+ * which take over the screen from the UI (such as invoking Voice Search).
+ */
+public class ExperimentService extends Service implements Terminator {
+    private static final String TAG = "AudioQualityVerifier";
+    private static int BACKGROUND_LOAD = 0;
+
+    private ArrayList<Experiment> mExperiments;
+    private Experiment mExp;
+
+    private TimeoutHandler mHandler = null;
+    private PowerManager.WakeLock mWakeLock = null;
+
+    private boolean mRunAll;
+    private int mExpID;
+
+    private boolean mActive;
+    private LoadGenerator[] mLoadGenerator = null;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public void onCreate() {
+        mExperiments = VerifierExperiments.getExperiments(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.i(TAG, "Service destroyed");
+        terminate(true);
+    }
+
+    /**
+     * Implements Terminator, to clean up when the experiment indicates it
+     * has completed.
+     */
+    public void terminate(boolean aborted) {
+        if (mLoadGenerator != null) {
+            for (LoadGenerator generator : mLoadGenerator) {
+                generator.halt();
+            }
+            mLoadGenerator = null;
+        }
+        if (!mActive) return;
+        mActive = false;
+        if (mHandler != null) mHandler.clear();
+        if (aborted) {
+            mExp.cancel();
+        } else {
+            mExp.stop();
+        }
+        Intent intent = new Intent(AudioQualityVerifierActivity.ACTION_EXP_FINISHED);
+        intent.putExtra(AudioQualityVerifierActivity.EXTRA_EXP_ID, mExpID);
+        intent.putExtra(AudioQualityVerifierActivity.EXTRA_RUN_ALL, mRunAll);
+        intent.putExtra(AudioQualityVerifierActivity.EXTRA_ABORTED, aborted);
+        sendBroadcast(intent);
+        if (mWakeLock != null) {
+            mWakeLock.release();
+            mWakeLock = null;
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        mActive = true;
+
+        // Obtain wakelock
+        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, TAG);
+        mWakeLock.acquire();
+
+        if (BACKGROUND_LOAD > 0) {
+            mLoadGenerator = new LoadGenerator[BACKGROUND_LOAD];
+            for (int i = 0; i < BACKGROUND_LOAD; i++) {
+                mLoadGenerator[i] = new LoadGenerator();
+            }
+        }
+
+        mExpID = intent.getIntExtra(AudioQualityVerifierActivity.EXTRA_EXP_ID, -1);
+        if (mExpID == -1) {
+            Log.e(TAG, "Invalid test ID");
+            System.exit(0);
+        }
+        mRunAll = intent.getBooleanExtra(AudioQualityVerifierActivity.EXTRA_RUN_ALL, false);
+        mExp = mExperiments.get(mExpID);
+        mExp.start();
+
+        // Inform the VerifierActivity Activity that we have started:
+        Intent feedback = new Intent(AudioQualityVerifierActivity.ACTION_EXP_STARTED);
+        feedback.putExtra(AudioQualityVerifierActivity.EXTRA_EXP_ID, mExpID);
+        sendBroadcast(feedback);
+
+        mHandler = new TimeoutHandler();
+        mHandler.delay(mExp.getTimeout());
+        mExp.run(this, this);
+
+        return START_NOT_STICKY;
+    }
+
+    class TimeoutHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            terminate(true);
+            stopSelf();
+        }
+
+        public void delay(int secs) {
+            removeMessages(0);
+            sendMessageDelayed(obtainMessage(0), secs * 1000);
+        }
+
+        public void clear() {
+            removeMessages(0);
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/LoadGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/LoadGenerator.java
new file mode 100644
index 0000000..f19359c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/LoadGenerator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+/**
+ * Simulate a simple load on the CPU, to determine the effect on recording.
+ * A better approach would be to generate network activity, to
+ * trigger interrupts as well.
+ */
+public class LoadGenerator extends Thread {
+    private boolean mProceed;
+
+    public LoadGenerator() {
+        mProceed = true;
+        start(); // Begin thread
+    }
+
+    public void halt() {
+        mProceed = false;
+    }
+
+    @Override
+    public void run() {
+        long counter = 0;
+        while (mProceed) {
+            counter++; // Busy loop
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Native.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Native.java
new file mode 100644
index 0000000..87b11d1
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Native.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+/**
+ * Interface to native (C++) DSP code.
+ */
+public class Native {
+    public native short[] generateSinusoid(float freq, float duration,
+            float sampleRate, float amplitude, float ramp);
+    public native float[] measureRms(short[] pcm, float sampleRate,
+            float onsetThresh);
+    public native float[] glitchTest(float sampleRate, float stimFreq,
+            float onsetThresh, float dbSnrThresh, short[] pcm);
+    public native float[] overflowCheck(short[] pcm, float sampleRate);
+    public native float[] compareSpectra(short[] pcm, short[] refPcm,
+            float sampleRate);
+    public native float linearityTest(short[][] pcms,
+        float sampleRate, float dbStepSize, int referenceStim);
+
+    // The following indexes must match those in wrapper.cc
+    public static final int MEASURE_RMS_RMS = 0;
+    public static final int MEASURE_RMS_STD_DEV = 1;
+    public static final int MEASURE_RMS_DURATION = 2;
+    public static final int MEASURE_RMS_MEAN = 3;
+
+    public static final int OVERFLOW_DELTAS = 0;
+    public static final int OVERFLOW_ERROR = 1;
+    public static final int OVERFLOW_DURATION = 2;
+    public static final int OVERFLOW_ONSET = 3;
+    public static final int OVERFLOW_OFFSET = 4;
+    public static final int OVERFLOW_MAX = 5;
+    public static final int OVERFLOW_MIN = 6;
+
+    public static final int GLITCH_COUNT = 0;
+    public static final int GLITCH_ERROR = 1;
+    public static final int GLITCH_DURATION = 2;
+
+    public static final int SPECTRUM_MAX_DEVIATION = 0;
+    public static final int SPECTRUM_ERROR = 1;
+    public static final int SPECTRUM_RMS_DEVIATION = 2;
+
+    private static Native mInstance = null;
+
+    static {
+        System.loadLibrary("audioquality");
+    }
+
+    private Native() {}
+
+    public static Native getInstance() {
+        if (mInstance == null) {
+            mInstance = new Native();
+        }
+        return mInstance;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Terminator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Terminator.java
new file mode 100644
index 0000000..b0edc90
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Terminator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+/**
+ * Interface passed to each Experiment.
+ */
+public interface Terminator {
+    /**
+     * Indicate that the experiment has finished.
+     *
+     * @param aborted is true if the experiment terminated abnormally.
+     */
+    public void terminate(boolean aborted);
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java
new file mode 100644
index 0000000..a65373c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/Utils.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * File and data utilities for the Audio Verifier.
+ */
+public class Utils {
+    public static final String TAG = "AudioQualityVerifier";
+    public static final ByteOrder BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
+
+    /**
+     *  Time delay.
+     *
+     *  @param ms time in milliseconds to pause for
+     */
+    public static void delay(int ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {}
+    }
+
+    public static String getExternalDir(Context context, Object exp) {
+        checkExternalStorageAvailable();
+        // API level 8:
+        // return context.getExternalFilesDir(null).getAbsolutePath();
+        // API level < 8:
+        String dir = Environment.getExternalStorageDirectory().getAbsolutePath();
+        dir += "/Android/data/" + exp.getClass().getPackage().getName() + "/files";
+        checkMakeDir(dir);
+        return dir;
+    }
+
+    private static void checkExternalStorageAvailable() {
+        String state = Environment.getExternalStorageState();
+        if (!Environment.MEDIA_MOUNTED.equals(state)) {
+            // TODO: Raise a Toast and supply internal storage instead
+        }
+    }
+
+    private static void checkMakeDir(String dir) {
+        File f = new File(dir);
+        if (!f.exists()) {
+            f.mkdirs();
+        }
+    }
+
+    /**
+     * Convert a string (e.g. the name of an experiment) to something more suitable
+     * for use as a filename.
+     *
+     * @param s the string to be cleaned
+     * @return a string which is similar (not necessarily unique) and safe for filename use
+     */
+    public static String cleanString(String s) {
+        StringBuilder sb = new StringBuilder();
+        for (char c : s.toCharArray()) {
+            if (Character.isWhitespace(c)) sb.append('_');
+            else if (Character.isLetterOrDigit(c)) sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Convert a sub-array from bytes to shorts.
+     *
+     * @param data array of bytes to be converted
+     * @param start first index to convert (should be even)
+     * @param len number of bytes to convert (should be even)
+     * @return an array of half the length, containing shorts
+     */
+    public static short[] byteToShortArray(byte[] data, int start, int len) {
+        short[] samples = new short[len / 2];
+        ByteBuffer bb = ByteBuffer.wrap(data, start, len);
+        bb.order(BYTE_ORDER);
+        for (int i = 0; i < len / 2; i++) {
+            samples[i] = bb.getShort();
+        }
+        return samples;
+    }
+
+    /**
+     * Convert a byte array to an array of shorts (suitable for the phone test
+     * native library's audio sample data).
+     *
+     * @param data array of bytes to be converted
+     * @return an array of half the length, containing shorts
+     */
+    public static short[] byteToShortArray(byte[] data) {
+        int len = data.length / 2;
+        short[] samples = new short[len];
+        ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.order(BYTE_ORDER);
+        for (int i = 0; i < len; i++) {
+            samples[i] = bb.getShort();
+        }
+        return samples;
+    }
+
+    /**
+     * Convert a short array (as returned by the phone test native library)
+     * to an array of bytes.
+     *
+     * @param samples array of shorts to be converted
+     * @return an array of twice the length, broken out into bytes
+     */
+    public static byte[] shortToByteArray(short[] samples) {
+        int len = samples.length;
+        byte[] data = new byte[len * 2];
+        ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.order(BYTE_ORDER);
+        for (int i = 0; i < len; i++) {
+            bb.putShort(samples[i]);
+        }
+        return data;
+    }
+
+    /**
+     * Scale the amplitude of an array of samples.
+     *
+     * @param samples to be scaled
+     * @param db decibels to scale up by (may be negative)
+     * @return the scaled samples
+     */
+    public static short[] scale(short[] samples, float db) {
+        short[] scaled = new short[samples.length];
+        // Convert decibels to a linear ratio:
+        double ratio = Math.pow(10.0, db / 20.0);
+        for (int i = 0; i < samples.length; i++) {
+            scaled[i] = (short) (samples[i] * ratio);
+        }
+        return scaled;
+    }
+
+    /**
+     * Read an entire file into memory.
+     *
+     * @param filename to be opened
+     * @return the file data, or null in case of error
+     */
+    private static byte[] readFile(String filename) {
+        FileInputStream fis;
+        try {
+            fis = new FileInputStream(filename);
+        } catch (FileNotFoundException e1) {
+            return null;
+        }
+
+        File file = new File(filename);
+        int len = (int) file.length();
+        byte[] data = new byte[len];
+
+        int pos = 0;
+        int bytes = 0;
+        int count;
+        while (pos < len) {
+            try {
+                count = fis.read(data, pos, len - pos);
+            } catch (IOException e) {
+                return null;
+            }
+            if (count < 1) return null;
+            pos += count;
+        }
+
+        try {
+            fis.close();
+        } catch (IOException e) {}
+        return data;
+    }
+
+    /**
+     * Read an entire file from an InputStream.
+     * Useful as AssetManager returns these.
+     *
+     * @param stream to read file contents from
+     * @return file data
+     */
+    public static byte[] readFile(InputStream stream) {
+        final int CHUNK_SIZE = 10000;
+        ByteArrayBuilder bab = new ByteArrayBuilder();
+        byte[] buf = new byte[CHUNK_SIZE];
+        int count;
+        while (true) {
+            try {
+                count = stream.read(buf, 0, CHUNK_SIZE);
+            } catch (IOException e) {
+                return null;
+            }
+            if (count == -1) break; // EOF
+            bab.append(buf, count);
+        }
+        return bab.toByteArray();
+    }
+
+    /**
+     * Save binary (audio) data to a file.
+     *
+     * @param filename to be written
+     * @param data contents
+     */
+    public static void saveFile(String filename, byte[] data) {
+        try {
+            FileOutputStream fos = new FileOutputStream(filename);
+            fos.write(data);
+            fos.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Error writing to file " + filename);
+        }
+    }
+
+    /**
+     * Push an entire array of audio data to an AudioTrack.
+     *
+     * @param at destination
+     * @param data to be written
+     * @return true if successful, or false on error
+     */
+    public static boolean writeAudio(AudioTrack at, byte[] data) {
+        int pos = 0;
+        int len = data.length;
+        int count;
+
+        while (pos < len) {
+            count = at.write(data, pos, len - pos);
+            if (count < 0) return false;
+            pos += count;
+        }
+        at.flush();
+        return true;
+    }
+
+    /**
+     * Determine the number of audio samples in a file
+     *
+     * @param filename file containing audio data
+     * @return number of samples in file, or 0 if file does not exist
+     */
+    public static int duration(String filename) {
+        File file = new File(filename);
+        int len = (int) file.length();
+        return len / AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+    }
+
+    /**
+     * Determine the number of audio samples in a stimulus asset
+     *
+     * @param context to look up stimulus
+     * @param stimNum index number of this stimulus
+     * @return number of samples in stimulus
+     */
+    public static int duration(Context context, int stimNum) {
+        byte[] data = AudioAssets.getStim(context, stimNum);
+        return data.length / AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+    }
+
+    public static void playRawFile(String filename) {
+        byte[] data = readFile(filename);
+        if (data == null) {
+            Log.e(TAG, "Cannot read " + filename);
+            return;
+        }
+        playRaw(data);
+    }
+
+    public static void playStim(Context context, int stimNum) {
+        Utils.playRaw(getStim(context, stimNum));
+    }
+
+    public static byte[] getStim(Context context, int stimNum) {
+        return AudioAssets.getStim(context, stimNum);
+    }
+
+    public static byte[] getPinkNoise(Context context, int ampl, int duration) {
+        return AudioAssets.getPinkNoise(context, ampl, duration);
+    }
+
+    public static void playRaw(byte[] data) {
+        Log.i(TAG, "Playing " + data.length + " bytes of pre-recorded audio");
+        AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC, AudioQualityVerifierActivity.SAMPLE_RATE,
+                AudioFormat.CHANNEL_OUT_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT,
+                data.length, AudioTrack.MODE_STREAM);
+        writeAudio(at, data);
+        at.play();
+    }
+
+    /**
+     * The equivalent of a simplified StringBuilder, but for bytes.
+     */
+    public static class ByteArrayBuilder {
+        private byte[] buf;
+        private int capacity, size;
+
+        public ByteArrayBuilder() {
+            capacity = 100;
+            size = 0;
+            buf = new byte[capacity];
+        }
+
+        public void append(byte[] b, int nBytes) {
+            if (nBytes < 1) return;
+            if (size + nBytes > capacity) expandCapacity(size + nBytes);
+            System.arraycopy(b, 0, buf, size, nBytes);
+            size += nBytes;
+        }
+
+        public byte[] toByteArray() {
+            byte[] result = new byte[size];
+            System.arraycopy(buf, 0, result, 0, size);
+            return result;
+        }
+
+        private void expandCapacity(int min) {
+            capacity *= 2;
+            if (capacity < min) capacity = min;
+            byte[] expanded = new byte[capacity];
+            System.arraycopy(buf, 0, expanded, 0, size);
+            buf = expanded;
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java
new file mode 100644
index 0000000..f0f0aa8
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/VerifierExperiments.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import com.android.cts.verifier.audioquality.experiments.BiasExperiment;
+import com.android.cts.verifier.audioquality.experiments.ClippingExperiment;
+import com.android.cts.verifier.audioquality.experiments.GainLinearityExperiment;
+import com.android.cts.verifier.audioquality.experiments.GlitchExperiment;
+import com.android.cts.verifier.audioquality.experiments.SoundLevelExperiment;
+import com.android.cts.verifier.audioquality.experiments.SpectrumShapeExperiment;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+
+/**
+ * Data shared between the VerifierActivity and ExperimentService
+ */
+public class VerifierExperiments {
+
+    private static ArrayList<Experiment> mExperiments = null;
+
+    private VerifierExperiments() {
+    }
+
+    public static ArrayList<Experiment> getExperiments(Context context) {
+        if (mExperiments == null) {
+            mExperiments = new ArrayList<Experiment>();
+            mExperiments.add(new SoundLevelExperiment());
+            mExperiments.add(new BiasExperiment());
+            mExperiments.add(new ClippingExperiment());
+            mExperiments.add(new GainLinearityExperiment());
+            mExperiments.add(new SpectrumShapeExperiment());
+            mExperiments.add(new GlitchExperiment(0));
+            mExperiments.add(new GlitchExperiment(7));
+            // mExperiments.add(new VoiceRecognitionExperiment());
+            for (Experiment exp : mExperiments) {
+                exp.init(context);
+            }
+        }
+        return mExperiments;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ViewResultsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ViewResultsActivity.java
new file mode 100644
index 0000000..87901b2
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/ViewResultsActivity.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 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.audioquality;
+
+import com.android.cts.verifier.R;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * This Activity allows the user to examine the results of the
+ * experiments which have been run so far.
+ */
+public class ViewResultsActivity extends Activity implements View.OnClickListener {
+    private TextView mTextView;
+    private Button mDismissButton;
+    private Button mSendResultsButton;
+    private String mResults;
+
+    private ArrayList<Experiment> mExperiments;
+
+    // The package of the Gmail application
+    private static final String GMAIL_PACKAGE = "com.google.android.gm";
+
+    // The Gmail compose activity name
+    private static final String GMAIL_ACTIVITY = GMAIL_PACKAGE
+            + ".ComposeActivityGmail";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.aq_view_results);
+
+        mDismissButton = (Button) findViewById(R.id.dismissButton);
+        mDismissButton.setOnClickListener(this);
+
+        mSendResultsButton = (Button) findViewById(R.id.sendResultsButton);
+        mSendResultsButton.setOnClickListener(this);
+
+        mTextView = (TextView) findViewById(R.id.textView);
+
+        Intent intent = getIntent();
+        mResults = intent.getStringExtra(AudioQualityVerifierActivity.EXTRA_RESULTS);
+        mTextView.setText(mResults);
+
+        mExperiments = VerifierExperiments.getExperiments(this);
+    }
+
+    private void sendResults() {
+        Intent intent = new Intent();
+        intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+        intent.setComponent(new ComponentName(GMAIL_PACKAGE, GMAIL_ACTIVITY));
+        intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.aq_subject));
+        intent.putExtra(Intent.EXTRA_TEXT, mResults);
+
+        ArrayList<Parcelable> attachments = new ArrayList<Parcelable>();
+        for (Experiment exp : mExperiments) {
+            String filename = exp.getAudioFileName();
+            if (filename != null) {
+                attachments.add(Uri.fromFile(new File(filename)));
+            }
+        }
+        intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
+
+        startActivity(intent);
+    }
+
+    // Implements View.OnClickListener
+    public void onClick(View v) {
+        if (v == mDismissButton) {
+            finish();
+        } else if (v == mSendResultsButton) {
+            sendResults();
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/BiasExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/BiasExperiment.java
new file mode 100644
index 0000000..bb6f8bf
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/BiasExperiment.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * Experiment to look for excessive DC bias in the recordings.
+ * It calculates the mean of the observed samples.
+ */
+public class BiasExperiment extends LoopbackExperiment {
+    private static final float ONSET_THRESH = 10.0f;
+    private static final float TOLERANCE = 200.0f;
+
+    public BiasExperiment() {
+        super(true);
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_bias_exp);
+    }
+
+    @Override
+    protected void compare(byte[] stim, byte[] record) {
+        short[] pcm = Utils.byteToShortArray(record);
+        float[] results = mNative.measureRms(pcm, AudioQualityVerifierActivity.SAMPLE_RATE, ONSET_THRESH);
+        float rms = results[Native.MEASURE_RMS_RMS];
+        float duration = results[Native.MEASURE_RMS_DURATION];
+        float mean = results[Native.MEASURE_RMS_MEAN];
+        if (mean < -TOLERANCE || mean > TOLERANCE) {
+            setScore(getString(R.string.aq_fail));
+        } else {
+            setScore(getString(R.string.aq_pass));
+        }
+        setReport(String.format(getString(R.string.aq_bias_report),
+                mean, TOLERANCE, rms, duration));
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ClippingExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ClippingExperiment.java
new file mode 100644
index 0000000..deb3709
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/ClippingExperiment.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.CalibrateVolumeActivity;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * Experiment to test the clipping behaviour of the microphone.
+ *
+ * The stimulus is sinusoidal, and calculated to cause clipping for
+ * part of the waveform. The experiment looks for strange clipping behaviour
+ * by checking if the signal has any discontinuities (which might indicate
+ * wraparound, for example).
+ */
+public class ClippingExperiment extends LoopbackExperiment {
+    private static final float FREQ = 250.0f;
+    private static final float AMPL = 32768.0f * 1.1f * CalibrateVolumeActivity.OUTPUT_AMPL
+            / CalibrateVolumeActivity.TARGET_AMPL;
+    private static final float DURATION = 3.0f; // Duration of tone in seconds
+    private static final float MIN_DURATION = DURATION * 0.9f;
+    private static final float RAMP = 0.01f;
+
+    public ClippingExperiment() {
+        super(true);
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_clipping_exp);
+    }
+
+    @Override
+    protected byte[] getStim(Context context) {
+        short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
+                AudioQualityVerifierActivity.SAMPLE_RATE, AMPL, RAMP);
+        return Utils.shortToByteArray(sinusoid);
+    }
+
+    @Override
+    protected void compare(byte[] stim, byte[] record) {
+        short[] pcm = Utils.byteToShortArray(record);
+        float[] ret = mNative.overflowCheck(pcm, AudioQualityVerifierActivity.SAMPLE_RATE);
+        int numDeltas = Math.round(ret[0]);
+        float error = ret[Native.OVERFLOW_ERROR];
+        float duration = ret[Native.OVERFLOW_DURATION];
+        float minPeak = ret[Native.OVERFLOW_MIN];
+        float maxPeak = ret[Native.OVERFLOW_MAX];
+
+        if (error < 0.0f) {
+            setScore(getString(R.string.aq_fail));
+            setReport(getString(R.string.aq_clipping_report_error));
+        } else if (duration < MIN_DURATION) {
+            setScore(getString(R.string.aq_fail));
+            setReport(String.format(getString(R.string.aq_clipping_report_short),
+                    DURATION, duration));
+        } else if (numDeltas > 0) {
+            setScore(getString(R.string.aq_fail));
+            setReport(String.format(getString(R.string.aq_clipping_report_fail),
+                    numDeltas, duration, minPeak, maxPeak));
+        } else {
+            setScore(getString(R.string.aq_pass));
+            setReport(String.format(getString(R.string.aq_clipping_report_pass),
+                    numDeltas, duration, minPeak, maxPeak));
+        }
+   }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GainLinearityExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GainLinearityExperiment.java
new file mode 100644
index 0000000..05f1602
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GainLinearityExperiment.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * Experiment to test the linearity of the microphone gain response.
+ *
+ * This plays a sequence of identical stimuli at increasing volumes and then
+ * analyzes the set of recordings.
+ */
+public class GainLinearityExperiment extends SequenceExperiment {
+    private static final int LEVELS = 4;
+    private static final float DB_STEP_SIZE = 10.0f;
+    private static final float TOLERANCE = 2.0f; // Maximum allowed deviation from linearity in dB
+    private static final int STIM_NUM = 31;
+
+    private short[] mReference = null;
+
+    public GainLinearityExperiment() {
+        super(true);
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_linearity_exp);
+    }
+
+    @Override
+    protected int getTrials() {
+        return LEVELS;
+    }
+
+    @Override
+    protected byte[] getStim(Context context, int trial) {
+        float db = (trial - (LEVELS - 1)) * DB_STEP_SIZE;
+        if (mReference == null) {
+            mReference = Utils.byteToShortArray(Utils.getStim(context, STIM_NUM));
+        }
+        short[] samples = Utils.scale(mReference, db);
+        return Utils.shortToByteArray(samples);
+    }
+
+    @Override
+    protected void compare(byte[][] stim, byte[][] record) {
+        short[][] pcms = new short[LEVELS][];
+        for (int i = 0; i < LEVELS; i++) {
+            pcms[i] = Utils.byteToShortArray(record[i]);
+        }
+        // We specify the middle stimulus (LEVELS / 2) as the "reference":
+        float deviation = mNative.linearityTest(pcms, AudioQualityVerifierActivity.SAMPLE_RATE,
+                DB_STEP_SIZE, LEVELS / 2);
+        if (deviation < 0.0f) {
+            setScore(getString(R.string.aq_fail));
+            setReport(String.format(getString(R.string.aq_linearity_report_error), deviation));
+        } else if (deviation > TOLERANCE) {
+            setScore(getString(R.string.aq_fail));
+            setReport(String.format(getString(R.string.aq_linearity_report_normal),
+                    deviation, TOLERANCE));
+        } else {
+            setScore(getString(R.string.aq_pass));
+            setReport(String.format(getString(R.string.aq_linearity_report_normal),
+                    deviation, TOLERANCE));
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GlitchExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GlitchExperiment.java
new file mode 100644
index 0000000..143c4a0
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/GlitchExperiment.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+import java.util.Random;
+
+/**
+ * Experiment to detect glitches or dropouts in the signal.
+ * A number of artificial glitches can be optionally introduced.
+ */
+public class GlitchExperiment extends LoopbackExperiment {
+    private static final float FREQ = 625.0f;
+    private static final float AMPL = 10000.0f;
+    private static final float ONSET_THRESH = 80.0f;
+    private static final float SNR_THRESH = 20.0f;
+    private static final float RAMP = 0.01f;
+    private static final float DURATION = 3.0f;
+
+    private int mArtificialGlitches;
+
+    public GlitchExperiment(int artificialGlitches) {
+        super(true);
+        mArtificialGlitches = artificialGlitches;
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        String s = context.getString(R.string.aq_glitch_exp);
+        if (mArtificialGlitches > 0) {
+            s += " (" + mArtificialGlitches + ")";
+        }
+        return s;
+    }
+
+    @Override
+    protected byte[] getStim(Context context) {
+        short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
+                AudioQualityVerifierActivity.SAMPLE_RATE, AMPL, RAMP);
+        addGlitches(sinusoid);
+        return Utils.shortToByteArray(sinusoid);
+    }
+
+    private void addGlitches(short[] samples) {
+        Random random = new Random();
+        for (int i = 0; i < mArtificialGlitches; i++) {
+            samples[random.nextInt(samples.length)] = 0;
+        }
+    }
+
+    @Override
+    protected void compare(byte[] stim, byte[] record) {
+        int targetMin = mArtificialGlitches > 0 ? 1 : 0;
+        int targetMax = mArtificialGlitches;
+        short[] pcm = Utils.byteToShortArray(record);
+        float[] ret = mNative.glitchTest(AudioQualityVerifierActivity.SAMPLE_RATE, FREQ,
+                ONSET_THRESH, SNR_THRESH, pcm);
+        int glitches = Math.round(ret[Native.GLITCH_COUNT]);
+        float error = ret[Native.GLITCH_ERROR];
+        float duration = ret[Native.GLITCH_DURATION];
+        if (error < 0.0f) {
+            setScore(getString(R.string.aq_fail));
+            setReport(getString(R.string.aq_glitch_report_error));
+        } else {
+            if (glitches > targetMax || glitches < targetMin) {
+                setScore(getString(R.string.aq_fail));
+            } else {
+                setScore(getString(R.string.aq_pass));
+            }
+            if (targetMin == targetMax) {
+                setReport(String.format(getString(R.string.aq_glitch_report_exact),
+                        glitches, targetMax, duration));
+            } else {
+                setReport(String.format(getString(R.string.aq_glitch_report_range),
+                        glitches, targetMin, targetMax, duration));
+            }
+        }
+   }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java
new file mode 100644
index 0000000..f4a1e1f
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/LoopbackExperiment.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Experiment;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+/**
+ * LoopbackExperiment represents a general class of experiments, all of which
+ * comprise playing an audio stimulus of some kind, whilst simultaneously
+ * recording from the microphone. The recording is then analyzed to determine
+ * the test results (score and report).
+ */
+public class LoopbackExperiment extends Experiment {
+    protected static final int TIMEOUT = 10;
+
+    // Amount of silence in ms before and after playback
+    protected static final int END_DELAY_MS = 500;
+
+    private Recorder mRecorder = null;
+
+    public LoopbackExperiment(boolean enable) {
+        super(enable);
+    }
+
+    protected byte[] getStim(Context context) {
+        int stimNum = 2;
+        byte[] data = Utils.getStim(context, stimNum);
+        return data;
+    }
+
+    @Override
+    public void run() {
+        byte[] playbackData = getStim(mContext);
+        byte[] recordedData = loopback(playbackData);
+
+        compare(playbackData, recordedData);
+        setRecording(recordedData);
+        mTerminator.terminate(false);
+    }
+
+    protected byte[] loopback(byte[] playbackData) {
+        int samples = playbackData.length / 2;
+        int duration = (samples * 1000) / AudioQualityVerifierActivity.SAMPLE_RATE; // In ms
+        int padSamples = (END_DELAY_MS * AudioQualityVerifierActivity.SAMPLE_RATE) / 1000;
+        int totalSamples = samples + 2 * padSamples;
+        byte[] recordedData = new byte[totalSamples * 2];
+
+        mRecorder = new Recorder(recordedData, totalSamples);
+        mRecorder.start();
+        Utils.delay(END_DELAY_MS);
+
+        Utils.playRaw(playbackData);
+
+        int timeout = duration + 2 * END_DELAY_MS;
+        try {
+            mRecorder.join(timeout);
+        } catch (InterruptedException e) {}
+
+        return recordedData;
+    }
+
+    protected void compare(byte[] stim, byte[] record) {
+        setScore(getString(R.string.aq_complete));
+        setReport(getString(R.string.aq_loopback_report));
+    }
+
+    private void halt() {
+        if (mRecorder != null) {
+            mRecorder.halt();
+        }
+    }
+
+    @Override
+    public void cancel() {
+        super.cancel();
+        halt();
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        halt();
+    }
+
+    @Override
+    public int getTimeout() {
+        return TIMEOUT;
+    }
+
+    /* Class which records audio in a background thread, to fill the supplied buffer. */
+    class Recorder extends Thread {
+        private AudioRecord mRecord;
+        private int mSamples;
+        private byte[] mBuffer;
+        private boolean mProceed;
+
+        Recorder(byte[] buffer, int samples) {
+            mBuffer = buffer;
+            mSamples = samples;
+            mProceed = true;
+        }
+
+        public void halt() {
+            mProceed = false;
+        }
+
+        @Override
+        public void run() {
+            final int minBufferSize = AudioQualityVerifierActivity.SAMPLE_RATE
+                    * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+            final int minHardwareBufferSize = AudioRecord.getMinBufferSize(
+                    AudioQualityVerifierActivity.SAMPLE_RATE,
+                    AudioFormat.CHANNEL_IN_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT);
+            final int bufferSize = Math.max(minHardwareBufferSize, minBufferSize);
+
+            mRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION,
+                    AudioQualityVerifierActivity.SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
+                    AudioQualityVerifierActivity.AUDIO_FORMAT, bufferSize);
+            if (mRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+                Log.e(TAG, "Couldn't open audio for recording");
+                return;
+            }
+            mRecord.startRecording();
+            if (mRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+                Log.e(TAG, "Couldn't record");
+                return;
+            }
+
+            captureLoop();
+
+            mRecord.stop();
+            mRecord.release();
+            mRecord = null;
+        }
+
+        private void captureLoop() {
+            int totalBytes = mSamples * AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
+            Log.i(TAG, "Recording " + totalBytes + " bytes");
+            int position = 0;
+            int bytes;
+            while (position < totalBytes && mProceed) {
+                bytes = mRecord.read(mBuffer, position, totalBytes - position);
+                if (bytes < 0) {
+                    if (bytes == AudioRecord.ERROR_INVALID_OPERATION) {
+                        Log.e(TAG, "Recording object not initalized");
+                    } else if (bytes == AudioRecord.ERROR_BAD_VALUE) {
+                        Log.e(TAG, "Invalid recording parameters");
+                    } else {
+                        Log.e(TAG, "Error during recording");
+                    }
+                    return;
+                }
+                position += bytes;
+            }
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SequenceExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SequenceExperiment.java
new file mode 100644
index 0000000..26a9a1d
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SequenceExperiment.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * An extension to LoopbackExperiment, in which the "playback and record"
+ * cycle is repeated several times. A family of stimuli is defined, and
+ * the experiment outcome may depend on the whole sequence of recordings.
+ */
+public class SequenceExperiment extends LoopbackExperiment {
+    public SequenceExperiment(boolean enable) {
+        super(enable);
+    }
+
+    protected int getTrials() {
+        return 1;
+    }
+
+    protected byte[] getStim(Context context, int trial) {
+        int stimNum = 2;
+        byte[] data = Utils.getStim(context, stimNum);
+        return data;
+    }
+
+    protected void compare(byte[][] stim, byte[][] record) {
+        setScore(getString(R.string.aq_complete));
+        setReport(getString(R.string.aq_loopback_report));
+    }
+
+    @Override
+    public void run() {
+        int n = getTrials();
+        byte[][] playbackData = new byte[n][];
+        byte[][] recordedData = new byte[n][];
+        for (int trial = 0; trial < n; trial++) {
+            playbackData[trial] = getStim(mContext, trial);
+            recordedData[trial] = loopback(playbackData[trial]);
+        }
+        compare(playbackData, recordedData);
+        mTerminator.terminate(false);
+    }
+
+    @Override
+    public int getTimeout() {
+        return TIMEOUT * getTrials();
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SoundLevelExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SoundLevelExperiment.java
new file mode 100644
index 0000000..4435f31
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SoundLevelExperiment.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.CalibrateVolumeActivity;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * Experiment to verify that the sound level has been correctly set.
+ *
+ * This experiment should be run first; if it fails the others may
+ * fail also.
+ */
+public class SoundLevelExperiment extends LoopbackExperiment {
+    private static final float ONSET_THRESH = 10.0f;
+    private static final int DURATION = 2;
+    private static final float TOLERANCE = 1.05f;
+    private static final float FREQ = 625.0f;
+    private static final float RAMP = 0.0f;
+
+    public SoundLevelExperiment() {
+        super(true);
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_sound_level_exp);
+    }
+
+    @Override
+    protected byte[] getStim(Context context) {
+        if (CalibrateVolumeActivity.USE_PINK) {
+            return Utils.getPinkNoise(context, CalibrateVolumeActivity.OUTPUT_AMPL, DURATION);
+        } else {
+            short[] sinusoid = mNative.generateSinusoid(FREQ, DURATION,
+                    AudioQualityVerifierActivity.SAMPLE_RATE, CalibrateVolumeActivity.OUTPUT_AMPL, RAMP);
+            return Utils.shortToByteArray(sinusoid);
+        }
+    }
+
+    @Override
+    protected void compare(byte[] stim, byte[] record) {
+        short[] pcm = Utils.byteToShortArray(record);
+        float[] results = mNative.measureRms(pcm, AudioQualityVerifierActivity.SAMPLE_RATE, ONSET_THRESH);
+        float rms = results[Native.MEASURE_RMS_RMS];
+        float duration = results[Native.MEASURE_RMS_DURATION];
+        String delta;
+        if (rms * TOLERANCE < CalibrateVolumeActivity.TARGET_RMS) {
+            setScore(getString(R.string.aq_fail));
+            delta = getString(R.string.aq_status_low);
+        } else if (rms > CalibrateVolumeActivity.TARGET_RMS * TOLERANCE) {
+            setScore(getString(R.string.aq_fail));
+            delta = getString(R.string.aq_status_high);
+        } else {
+            setScore(getString(R.string.aq_pass));
+            delta = getString(R.string.aq_status_ok);
+        }
+        setReport(delta + ".\n" + String.format(getString(R.string.aq_level_report),
+                rms, CalibrateVolumeActivity.TARGET_RMS,
+                100.0f * (TOLERANCE - 1.0f), duration));
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SpectrumShapeExperiment.java b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SpectrumShapeExperiment.java
new file mode 100644
index 0000000..148fb471
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audioquality/experiments/SpectrumShapeExperiment.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 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.audioquality.experiments;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.audioquality.AudioQualityVerifierActivity;
+import com.android.cts.verifier.audioquality.Native;
+import com.android.cts.verifier.audioquality.Utils;
+
+import android.content.Context;
+
+/**
+ * Experiment to check that the frequency profile of the recorded signal
+ * does not differ too much from the stimulus.
+ */
+public class SpectrumShapeExperiment extends LoopbackExperiment {
+    private static final float MAX_RMS_DEVIATION = 7.0f;
+    private static final int AMPL = 10000;
+    private static final int DURATION = 3;
+
+    public SpectrumShapeExperiment() {
+        super(true);
+    }
+
+    @Override
+    protected String lookupName(Context context) {
+        return context.getString(R.string.aq_spectrum_shape_exp);
+    }
+
+   @Override
+    protected byte[] getStim(Context context) {
+        return Utils.getPinkNoise(context, AMPL, DURATION);
+    }
+
+    @Override
+    protected void compare(byte[] stim, byte[] record) {
+        short[] pcm = Utils.byteToShortArray(record);
+        short[] refPcm = Utils.byteToShortArray(stim);
+        float[] ret = mNative.compareSpectra(pcm, refPcm, AudioQualityVerifierActivity.SAMPLE_RATE);
+        float maxDeviation = ret[Native.SPECTRUM_MAX_DEVIATION];
+        float error = ret[Native.SPECTRUM_ERROR];
+        float rmsDeviation = ret[Native.SPECTRUM_RMS_DEVIATION];
+        if (error < 0.0f) {
+            setScore(getString(R.string.aq_fail));
+            setReport(getString(R.string.aq_spectrum_report_error));
+        } else if (rmsDeviation > MAX_RMS_DEVIATION) {
+            setScore(getString(R.string.aq_fail));
+            setReport(String.format(getString(R.string.aq_spectrum_report_normal),
+                    rmsDeviation, MAX_RMS_DEVIATION));
+        } else {
+            setScore(getString(R.string.aq_pass));
+            setReport(String.format(getString(R.string.aq_spectrum_report_normal),
+                    rmsDeviation, MAX_RMS_DEVIATION));
+        }
+    }
+}