Add 8 bit and float format checking to AudioRecordTest

Change-Id: If2ba3c47ea3149edcd5d3a8cb55ff13fb56b42ac
diff --git a/tests/tests/media/src/android/media/cts/AudioHelper.java b/tests/tests/media/src/android/media/cts/AudioHelper.java
index 8e82bd6..833c235 100644
--- a/tests/tests/media/src/android/media/cts/AudioHelper.java
+++ b/tests/tests/media/src/android/media/cts/AudioHelper.java
@@ -16,6 +16,13 @@
 
 package android.media.cts;
 
+import java.nio.ByteBuffer;
+import org.junit.Assert;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
 import android.os.Looper;
 
 // Used for statistics and loopers in listener tests.
@@ -138,4 +145,147 @@
             mThread = null;
         }
     }
+
+    public static int outChannelMaskFromInChannelMask(int channelMask) {
+        switch (channelMask) {
+            case AudioFormat.CHANNEL_IN_MONO:
+                return AudioFormat.CHANNEL_OUT_MONO;
+            case AudioFormat.CHANNEL_IN_STEREO:
+                return AudioFormat.CHANNEL_OUT_STEREO;
+            default:
+                return AudioFormat.CHANNEL_INVALID;
+        }
+    }
+
+    /* AudioRecordAudit extends AudioRecord to allow concurrent playback
+     * of read content to an AudioTrack.
+     * This affects AudioRecord timing.
+     */
+    public static class AudioRecordAudit extends AudioRecord {
+        AudioRecordAudit(int audioSource, int sampleRate, int channelMask,
+                int format, int bufferSize) {
+            this(audioSource, sampleRate, channelMask, format, bufferSize,
+                    AudioManager.STREAM_MUSIC, 1000 /*delayMs*/);
+        }
+
+        AudioRecordAudit(int audioSource, int sampleRate, int channelMask,
+                int format, int bufferSize, int auditStreamType, int delayMs) {
+            super(audioSource, sampleRate, channelMask, format, bufferSize);
+
+            if (delayMs >= 0) { // create an AudioTrack
+                final int channelOutMask = outChannelMaskFromInChannelMask(channelMask);
+                final int bufferOutFrames = sampleRate * delayMs / 1000;
+                final int bufferOutSamples = bufferOutFrames
+                        * AudioFormat.channelCountFromOutChannelMask(channelOutMask);
+                final int bufferOutSize = bufferOutSamples
+                        * AudioFormat.getBytesPerSample(format);
+
+                mTrack = new AudioTrack(auditStreamType, sampleRate, channelOutMask, format,
+                        bufferOutSize, AudioTrack.MODE_STREAM);
+                mPosition = 0;
+                mFinishAtMs = 0;
+            }
+        }
+
+        @Override
+        public int read(byte[] audioData, int offsetInBytes, int sizeInBytes) {
+            // for byte array access we verify format is 8 bit PCM (typical use)
+            Assert.assertEquals(TAG + ": format mismatch",
+                    AudioFormat.ENCODING_PCM_8BIT, getAudioFormat());
+            int samples = super.read(audioData, offsetInBytes, sizeInBytes);
+            if (mTrack != null) {
+                Assert.assertEquals(samples, mTrack.write(audioData, offsetInBytes, samples));
+                mPosition += samples / mTrack.getChannelCount();
+            }
+            return samples;
+        }
+
+        @Override
+        public int read(short[] audioData, int offsetInShorts, int sizeInShorts) {
+            // for short array access we verify format is 16 bit PCM (typical use)
+            Assert.assertEquals(TAG + ": format mismatch",
+                    AudioFormat.ENCODING_PCM_16BIT, getAudioFormat());
+            int samples = super.read(audioData, offsetInShorts, sizeInShorts);
+            if (mTrack != null) {
+                Assert.assertEquals(samples, mTrack.write(audioData, offsetInShorts, samples));
+                mPosition += samples / mTrack.getChannelCount();
+            }
+            return samples;
+        }
+
+        @Override
+        public int read(float[] audioData, int offsetInFloats, int sizeInFloats, int readMode) {
+            // for float array access we verify format is float PCM (typical use)
+            Assert.assertEquals(TAG + ": format mismatch",
+                    AudioFormat.ENCODING_PCM_FLOAT, getAudioFormat());
+            int samples = super.read(audioData, offsetInFloats, sizeInFloats, readMode);
+            if (mTrack != null) {
+                Assert.assertEquals(samples, mTrack.write(audioData, offsetInFloats, samples,
+                        AudioTrack.WRITE_BLOCKING));
+                mPosition += samples / mTrack.getChannelCount();
+            }
+            return samples;
+        }
+
+        @Override
+        public int read(ByteBuffer audioBuffer, int sizeInBytes) {
+            int bytes = super.read(audioBuffer, sizeInBytes);
+            if (mTrack != null) {
+                // read does not affect position and limit of the audioBuffer.
+                // we make a duplicate to change that for writing to the output AudioTrack
+                // which does check position and limit.
+                ByteBuffer copy = audioBuffer.duplicate();
+                copy.position(0).limit(bytes);  // read places data at the start of the buffer.
+                Assert.assertEquals(bytes, mTrack.write(copy, bytes, AudioTrack.WRITE_BLOCKING));
+                mPosition += bytes /
+                        (mTrack.getChannelCount()
+                                * AudioFormat.getBytesPerSample(mTrack.getAudioFormat()));
+            }
+            return bytes;
+        }
+
+        @Override
+        public void startRecording() {
+            super.startRecording();
+            if (mTrack != null) {
+                mTrack.play();
+            }
+        }
+
+        @Override
+        public void stop() {
+            super.stop();
+            if (mTrack != null) {
+                if (mPosition > 0) { // stop may be called multiple times.
+                    final int remainingFrames = mPosition - mTrack.getPlaybackHeadPosition();
+                    mFinishAtMs = System.currentTimeMillis()
+                            + remainingFrames * 1000 / mTrack.getSampleRate();
+                    mPosition = 0;
+                }
+                mTrack.stop(); // allows remaining data to play out
+            }
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            if (mTrack != null) {
+                final long remainingMs = mFinishAtMs - System.currentTimeMillis();
+                if (remainingMs > 0) {
+                    try {
+                        Thread.sleep(remainingMs);
+                    } catch (InterruptedException e) {
+                        ;
+                    }
+                }
+                mTrack.release();
+                mTrack = null;
+            }
+        }
+
+        public AudioTrack mTrack;
+        private final static String TAG = "AudioRecordAudit";
+        private int mPosition;
+        private long mFinishAtMs;
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/AudioRecordTest.java b/tests/tests/media/src/android/media/cts/AudioRecordTest.java
index 5d995cd..2652ce8 100644
--- a/tests/tests/media/src/android/media/cts/AudioRecordTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioRecordTest.java
@@ -227,42 +227,56 @@
         assertEquals(AudioRecord.STATE_UNINITIALIZED, mAudioRecord.getState());
     }
 
-    public void testAudioRecordRateFF() throws Exception {
-        doTest("Streaming Local Looper", false /*localRecord*/, false /*customHandler*/,
+    public void testAudioRecordResamplerStereo8Bit() throws Exception {
+        doTest("ResamplerStereo8Bit", false /*localRecord*/, false /*customHandler*/,
                 0 /*periodsPerSecond*/, 3 /*markerPeriodsPerSecond*/,
-                true /*useByteBuffer*/, 44100 /*TEST_SR*/,
-                AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
+                true /*useByteBuffer*/,  true /*blocking*/,
+                false /*auditRecording*/, 45000 /*TEST_SR*/,
+                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_8BIT);
     }
 
-    public void testAudioRecordRateTF() throws Exception {
-        doTest("Streaming Local Looper", true /*localRecord*/, false /*customHandler*/,
+    public void testAudioRecordLocalMono16Bit() throws Exception {
+        doTest("LocalMono16Bit", true /*localRecord*/, false /*customHandler*/,
                 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/,
-                false /*useByteBuffer*/, 8000 /*TEST_SR*/,
+                false /*useByteBuffer*/, true /*blocking*/,
+                false /*auditRecording*/, 8000 /*TEST_SR*/,
                 AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
     }
 
-    public void testAudioRecordRateFT() throws Exception {
-        doTest("Streaming Local Looper", false /*localRecord*/, true /*customHandler*/,
+    public void testAudioRecordMonoFloat() throws Exception {
+        doTest("MonoFloat", false /*localRecord*/, true /*customHandler*/,
                 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/,
-                false /*useByteBuffer*/, 32000 /*TEST_SR*/,
-                AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
+                false /*useByteBuffer*/, true /*blocking*/,
+                false /*auditRecording*/, 32000 /*TEST_SR*/,
+                AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT);
     }
 
-    public void testAudioRecordRateTT() throws Exception {
-        doTest("Streaming Local Looper", true /*localRecord*/, true /*customHandler*/,
+    public void testAudioRecordLocalNonblockingStereoFloat() throws Exception {
+        doTest("LocalNonblockingStereoFloat", true /*localRecord*/, true /*customHandler*/,
                 2 /*periodsPerSecond*/, 0 /*markerPeriodsPerSecond*/,
-                false /*useByteBuffer*/, 48000 /*TEST_SR*/,
-                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
+                false /*useByteBuffer*/, false /*blocking*/,
+                false /*auditRecording*/, 48000 /*TEST_SR*/,
+                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
+    }
+
+    public void testAudioRecordByteBufferAuditResamplerStereoFloat() throws Exception {
+        doTest("testAudioRecordByteBufferAuditResamplerStereoFloat",
+                false /*localRecord*/, true /*customHandler*/,
+                2 /*periodsPerSecond*/, 0 /*markerPeriodsPerSecond*/,
+                true /*useByteBuffer*/, true /*blocking*/,
+                true /*auditRecording*/, 17000 /*TEST_SR*/,
+                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
     }
 
     private void doTest(String reportName, boolean localRecord, boolean customHandler,
             int periodsPerSecond, int markerPeriodsPerSecond,
-            boolean useByteBuffer, final int TEST_SR,
-            final int TEST_CONF, final int TEST_FORMAT) throws Exception {
+            boolean useByteBuffer, boolean blocking, boolean auditRecording,
+            final int TEST_SR, final int TEST_CONF, final int TEST_FORMAT) throws Exception {
         if (!hasMicrophone()) {
             return;
         }
-        final int TEST_TIME_MS = 2000;
+        // audit recording plays back recorded audio, so use longer test timing
+        final int TEST_TIME_MS = auditRecording ? 10000 : 2000;
         final int TEST_SOURCE = MediaRecorder.AudioSource.DEFAULT;
         mIsHandleMessageCalled = false;
 
@@ -271,25 +285,51 @@
         assertTrue(bufferSizeInBytes > 0);
 
         final AudioRecord record;
-        final AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioRecord> makeSomething;
-        if (localRecord) {
-            makeSomething = null;
-            record = new AudioRecord(TEST_SOURCE, TEST_SR, TEST_CONF,
-                    TEST_FORMAT, bufferSizeInBytes);
+        final AudioHelper
+                .MakeSomethingAsynchronouslyAndLoop<AudioRecord> makeSomething;
+
+        if (auditRecording) {
+            if (localRecord) {
+                makeSomething = null;
+                record = new AudioHelper.AudioRecordAudit(
+                        TEST_SOURCE,
+                        TEST_SR, TEST_CONF,
+                        TEST_FORMAT, bufferSizeInBytes);
+            } else {
+                makeSomething =
+                        new AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioRecord>(
+                                new AudioHelper.MakesSomething<AudioRecord>() {
+                                    @Override
+                                    public AudioRecord makeSomething() {
+                                        return new AudioHelper.AudioRecordAudit(
+                                                TEST_SOURCE,
+                                                TEST_SR, TEST_CONF,
+                                                TEST_FORMAT, bufferSizeInBytes);
+                                    }
+                                }
+                                );
+                // create AudioRecord on different thread's looper.
+                record = makeSomething.make();
+            }
         } else {
-            makeSomething =
-                    new AudioHelper
-                            .MakeSomethingAsynchronouslyAndLoop<AudioRecord>(
-                    new AudioHelper.MakesSomething<AudioRecord>() {
-                        @Override
-                        public AudioRecord makeSomething() {
-                            return new AudioRecord(TEST_SOURCE, TEST_SR, TEST_CONF,
-                                TEST_FORMAT, bufferSizeInBytes);
-                        }
-                    }
-                );
-           // create AudioRecord on different thread's looper.
-           record = makeSomething.make();
+            if (localRecord) {
+                makeSomething = null;
+                record = new AudioRecord(TEST_SOURCE, TEST_SR, TEST_CONF,
+                        TEST_FORMAT, bufferSizeInBytes);
+            } else {
+                makeSomething =
+                        new AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioRecord>(
+                                new AudioHelper.MakesSomething<AudioRecord>() {
+                                    @Override
+                                    public AudioRecord makeSomething() {
+                                        return new AudioRecord(TEST_SOURCE, TEST_SR, TEST_CONF,
+                                                TEST_FORMAT, bufferSizeInBytes);
+                                    }
+                                }
+                                );
+               // create AudioRecord on different thread's looper.
+               record = makeSomething.make();
+            }
         }
         // AudioRecord creation may have silently failed, check state now
         assertEquals(AudioRecord.STATE_INITIALIZED, record.getState());
@@ -363,8 +403,26 @@
             }
         } else {
             switch (TEST_FORMAT) {
+            case AudioFormat.ENCODING_PCM_8BIT: {
+                // For 8 bit data, use bytes
+                assertTrue(blocking); // always blocking (for now)
+                byte[] byteData = new byte[BUFFER_SAMPLES];
+                while (samplesRead < targetSamples) {
+                    // the first time through, we read a single frame.
+                    // this sets the recording anchor position.
+                    int amount = samplesRead == 0 ? numChannels :
+                        Math.min(BUFFER_SAMPLES, targetSamples - samplesRead);
+                    int ret = record.read(byteData, 0, amount);
+                    assertEquals(amount, ret); // blocking
+                    if (samplesRead == 0) {
+                        firstSampleTime = System.currentTimeMillis();
+                    }
+                    samplesRead += ret;
+                }
+            } break;
             case AudioFormat.ENCODING_PCM_16BIT: {
                 // For 16 bit data, use shorts
+                assertTrue(blocking); // always blocking (for now)
                 short[] shortData = new short[BUFFER_SAMPLES];
                 while (samplesRead < targetSamples) {
                     // the first time through, we read a single frame.
@@ -379,17 +437,37 @@
                     samplesRead += ret;
                 }
             } break;
+            case AudioFormat.ENCODING_PCM_FLOAT: {
+                float[] floatData = new float[BUFFER_SAMPLES];
+                while (samplesRead < targetSamples) {
+                    // the first time through, we read a single frame.
+                    // this sets the recording anchor position.
+                    int amount = samplesRead == 0 ? numChannels :
+                        Math.min(BUFFER_SAMPLES, targetSamples - samplesRead);
+                    int ret = record.read(floatData, 0, amount, blocking ?
+                            AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
+                    if (blocking) {
+                        assertEquals(amount, ret); // blocking
+                    } else {
+                        assertTrue(ret >= 0 && ret <= amount);
+                    }
+                    if (samplesRead == 0) {
+                        firstSampleTime = System.currentTimeMillis();
+                    }
+                    samplesRead += ret;
+                }
+            } break;
             }
         }
 
         // We've read all the frames, now check the record timing.
         final long endTime = System.currentTimeMillis();
-        // Log.d(TAG, "first sample time " + (firstSampleTime - startTime)
+        //Log.d(TAG, "first sample time " + (firstSampleTime - startTime)
         //        + " test time " + (endTime - firstSampleTime));
         // Verify recording starts within 200 ms of record.startRecording() (typical 100ms)
         // Verify recording completes within 50 ms of expected test time (typical 20ms)
         assertEquals(0, firstSampleTime - startTime, 200);
-        assertEquals(TEST_TIME_MS, endTime - firstSampleTime, 50);
+        assertEquals(TEST_TIME_MS, endTime - firstSampleTime, auditRecording ? 1000 : 50);
 
         // Even though we've read all the frames we want, the events may not be sent to
         // the listeners (events are handled through a separate internal callback thread).
@@ -398,6 +476,9 @@
 
         record.stop();
         assertEquals(AudioRecord.RECORDSTATE_STOPPED, record.getRecordingState());
+
+        final long stopTime = System.currentTimeMillis();
+
         // stop listening - we should be done.
         // Caution M behavior and likely much earlier:
         // we assume no events can happen after stop(), but this may not
@@ -414,9 +495,15 @@
         }
         listener.release();
         record.release();
-
-        int markerPeriods = markerPeriodsPerSecond * TEST_TIME_MS / 1000;
-        int updatePeriods = periodsPerSecond * TEST_TIME_MS / 1000;
+        if (auditRecording) { // don't check timing if auditing (messes up timing)
+            return;
+        }
+        final int markerPeriods = markerPeriodsPerSecond * TEST_TIME_MS / 1000;
+        final int updatePeriods = periodsPerSecond * TEST_TIME_MS / 1000;
+        final int markerPeriodsMax =
+                markerPeriodsPerSecond * (int)(stopTime - firstSampleTime) / 1000 + 1;
+        final int updatePeriodsMax =
+                periodsPerSecond * (int)(stopTime - firstSampleTime) / 1000 + 1;
 
         // collect statistics
         final ArrayList<Integer> markerList = listener.getMarkerList();
@@ -424,10 +511,14 @@
         // verify count of markers and periodic notifications.
         // there could be an extra notification since we don't stop() immediately
         // rather wait for potential events to come in.
+        //Log.d(TAG, "markerPeriods " + markerPeriods +
+        //        " markerPeriodsReceived " + markerList.size());
+        //Log.d(TAG, "updatePeriods " + updatePeriods +
+        //        " updatePeriodsReceived " + periodicList.size());
         assertTrue(markerPeriods <= markerList.size()
-                && markerList.size() <= markerPeriods + 1);
+                && markerList.size() <= markerPeriodsMax);
         assertTrue(updatePeriods <= periodicList.size()
-                && periodicList.size() <= updatePeriods + 1);
+                && periodicList.size() <= updatePeriodsMax);
 
         // Since we don't have accurate positioning of the start time of the recorder,
         // and there is no record.getPosition(), we consider only differential timing
@@ -438,8 +529,9 @@
         for (int i = 1; i < markerList.size(); ++i) {
             final int expected = mMarkerPeriodInFrames * i;
             final int actual = markerList.get(i) - markerList.get(0);
-            // Log.d(TAG, "Marker: " + i + " expected(" + expected + ")  actual(" + actual
-            //        + ")  diff(" + (actual - expected) + ")");
+            //Log.d(TAG, "Marker: " + i + " expected(" + expected + ")  actual(" + actual
+            //        + ")  diff(" + (actual - expected) + ")"
+            //        + " tolerance " + toleranceInFrames);
             assertEquals(expected, actual, toleranceInFrames);
             markerStat.add((double)(actual - expected) * 1000 / TEST_SR);
         }
@@ -448,14 +540,29 @@
         for (int i = 1; i < periodicList.size(); ++i) {
             final int expected = updatePeriodInFrames * i;
             final int actual = periodicList.get(i) - periodicList.get(0);
-            // Log.d(TAG, "Update: " + i + " expected(" + expected + ")  actual(" + actual
-            //        + ")  diff(" + (actual - expected) + ")");
+            //Log.d(TAG, "Update: " + i + " expected(" + expected + ")  actual(" + actual
+            //        + ")  diff(" + (actual - expected) + ")"
+            //        + " tolerance " + toleranceInFrames);
             assertEquals(expected, actual, toleranceInFrames);
             periodicStat.add((double)(actual - expected) * 1000 / TEST_SR);
         }
 
         // report this
         ReportLog log = getReportLog();
+        log.printValue(reportName + ": startRecording lag", firstSampleTime - startTime,
+                ResultType.LOWER_BETTER, ResultUnit.MS);
+        log.printValue(reportName + ": Total record time expected", TEST_TIME_MS,
+                ResultType.NEUTRAL, ResultUnit.MS);
+        log.printValue(reportName + ": Total record time actual", (endTime - firstSampleTime),
+                ResultType.NEUTRAL, ResultUnit.MS);
+        log.printValue(reportName + ": Total markers expected", markerPeriods,
+                ResultType.NEUTRAL, ResultUnit.COUNT);
+        log.printValue(reportName + ": Total markers actual", markerList.size(),
+                ResultType.NEUTRAL, ResultUnit.COUNT);
+        log.printValue(reportName + ": Total periods expected", updatePeriods,
+                ResultType.NEUTRAL, ResultUnit.COUNT);
+        log.printValue(reportName + ": Total periods actual", periodicList.size(),
+                ResultType.NEUTRAL, ResultUnit.COUNT);
         log.printValue(reportName + ": Average Marker diff", markerStat.getAvg(),
                 ResultType.LOWER_BETTER, ResultUnit.MS);
         log.printValue(reportName + ": Maximum Marker abs diff", markerStat.getMaxAbs(),