Home | History | Annotate | Download | only in audiolib
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.cts.verifier.audio.audiolib;
     18 
     19 import android.media.AudioDeviceInfo;
     20 import android.media.AudioFormat;
     21 import android.media.AudioRecord;
     22 
     23 import android.util.Log;
     24 
     25 import java.lang.Math;
     26 
     27 /**
     28  * Records audio data to a stream.
     29  */
     30 public class StreamRecorder {
     31     @SuppressWarnings("unused")
     32     private static final String TAG = "StreamRecorder";
     33 
     34     // Sample Buffer
     35     private float[] mBurstBuffer;
     36     private int mNumBurstFrames;
     37     private int mNumChannels;
     38 
     39     // Recording attributes
     40     private int mSampleRate;
     41 
     42     // Recording state
     43     Thread mRecorderThread = null;
     44     private AudioRecord mAudioRecord = null;
     45     private boolean mRecording = false;
     46 
     47     private StreamRecorderListener mListener = null;
     48 
     49     private AudioDeviceInfo mRoutingDevice = null;
     50 
     51     public StreamRecorder() {}
     52 
     53     public int getNumBurstFrames() { return mNumBurstFrames; }
     54     public int getSampleRate() { return mSampleRate; }
     55 
     56     /*
     57      * State
     58      */
     59     public static int calcNumBufferBytes(int numChannels, int sampleRate, int encoding) {
     60         // NOTE: Special handling of 4-channels. There is currently no AudioFormat positional
     61         // constant for 4-channels of input, so in this case, calculate for 2 and double it.
     62         int numBytes = 0;
     63         if (numChannels == 4) {
     64             numBytes = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_STEREO,
     65                     encoding);
     66             numBytes *= 2;
     67         } else {
     68             numBytes = AudioRecord.getMinBufferSize(sampleRate,
     69                     AudioUtils.countToInPositionMask(numChannels), encoding);
     70         }
     71 
     72         return numBytes;
     73     }
     74 
     75     public static int calcNumBufferFrames(int numChannels, int sampleRate, int encoding) {
     76         return calcNumBufferBytes(numChannels, sampleRate, encoding) /
     77                 AudioUtils.calcFrameSizeInBytes(encoding, numChannels);
     78     }
     79 
     80     public boolean isInitialized() {
     81         return mAudioRecord != null && mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED;
     82     }
     83 
     84     public boolean isRecording() { return mRecording; }
     85 
     86     public void setRouting(AudioDeviceInfo routingDevice) {
     87         Log.i(TAG, "setRouting(" + (routingDevice != null ? routingDevice.getId() : -1) + ")");
     88         mRoutingDevice = routingDevice;
     89         if (mAudioRecord != null) {
     90             mAudioRecord.setPreferredDevice(mRoutingDevice);
     91         }
     92     }
     93 
     94     /*
     95      * Accessors
     96      */
     97     public float[] getBurstBuffer() { return mBurstBuffer; }
     98 
     99     public int getNumChannels() { return mNumChannels; }
    100 
    101     /*
    102      * Events
    103      */
    104     public void setListener(StreamRecorderListener listener) {
    105         mListener = listener;
    106     }
    107 
    108     private void waitForRecorderThreadToExit() {
    109         try {
    110             if (mRecorderThread != null) {
    111                 mRecorderThread.join();
    112                 mRecorderThread = null;
    113             }
    114         } catch (InterruptedException e) {
    115             e.printStackTrace();
    116         }
    117     }
    118 
    119     private boolean open_internal(int numChans, int sampleRate) {
    120         mNumChannels = numChans;
    121         mSampleRate = sampleRate;
    122 
    123         final int frameSize =
    124                 AudioUtils.calcFrameSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT, mNumChannels);
    125         final int bufferSizeInBytes = frameSize * 64;   // Some, non-critical value
    126 
    127         AudioFormat.Builder formatBuilder = new AudioFormat.Builder();
    128         formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT);
    129         formatBuilder.setSampleRate(mSampleRate);
    130 
    131         if (numChans <= 2) {
    132             // There is currently a bug causing channel INDEX masks to fail.
    133             // for channels counts of <= 2, use channel POSITION
    134             final int chanPosMask = AudioUtils.countToInPositionMask(numChans);
    135             formatBuilder.setChannelMask(chanPosMask);
    136         } else {
    137             // There are no INPUT channel-position masks for > 2 channels
    138             final int chanIndexMask = AudioUtils.countToIndexMask(numChans);
    139             formatBuilder.setChannelIndexMask(chanIndexMask);
    140         }
    141 
    142         AudioRecord.Builder builder = new AudioRecord.Builder();
    143         builder.setAudioFormat(formatBuilder.build());
    144 
    145         try {
    146             mAudioRecord = builder.build();
    147             return true;
    148         } catch (UnsupportedOperationException ex) {
    149             Log.e(TAG, "Couldn't open AudioRecord: " + ex);
    150             mAudioRecord = null;
    151             return false;
    152         }
    153     }
    154 
    155     public boolean open(int numChans, int sampleRate, int numBurstFrames) {
    156         boolean sucess = open_internal(numChans, sampleRate);
    157         if (sucess) {
    158             mNumBurstFrames = numBurstFrames;
    159             mBurstBuffer = new float[mNumBurstFrames * mNumChannels];
    160             // put some non-zero data in the burst buffer.
    161             // this is to verify that the record is putting SOMETHING into each channel.
    162             for(int index = 0; index < mBurstBuffer.length; index++) {
    163                 mBurstBuffer[index] = (float)(Math.random() * 2.0) - 1.0f;
    164             }
    165         }
    166 
    167         return sucess;
    168     }
    169 
    170     public void close() {
    171         stop();
    172 
    173         waitForRecorderThreadToExit();
    174 
    175         mAudioRecord.release();
    176         mAudioRecord = null;
    177     }
    178 
    179     public boolean start() {
    180         mAudioRecord.setPreferredDevice(mRoutingDevice);
    181 
    182         if (mListener != null) {
    183             mListener.sendEmptyMessage(StreamRecorderListener.MSG_START);
    184         }
    185 
    186         try {
    187             mAudioRecord.startRecording();
    188         } catch (IllegalStateException ex) {
    189             Log.i("", "ex: " + ex);
    190         }
    191         mRecording = true;
    192 
    193         waitForRecorderThreadToExit(); // just to be sure.
    194 
    195         mRecorderThread = new Thread(new StreamRecorderRunnable(), "StreamRecorder Thread");
    196         mRecorderThread.start();
    197 
    198         return true;
    199     }
    200 
    201     public void stop() {
    202         if (mRecording) {
    203             mRecording = false;
    204         }
    205     }
    206 
    207     /*
    208      * StreamRecorderRunnable
    209      */
    210     private class StreamRecorderRunnable implements Runnable {
    211         @Override
    212         public void run() {
    213             final int numBurstSamples = mNumBurstFrames * mNumChannels;
    214             while (mRecording) {
    215                 int numReadSamples = mAudioRecord.read(
    216                         mBurstBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING);
    217 
    218                 if (numReadSamples < 0) {
    219                     // error
    220                     Log.i(TAG, "AudioRecord write error: " + numReadSamples);
    221                     stop();
    222                 } else if (numReadSamples < numBurstSamples) {
    223                     // got less than requested?
    224                     Log.i(TAG, "AudioRecord Underflow: " + numReadSamples +
    225                             " vs. " + numBurstSamples);
    226                     stop();
    227                 }
    228 
    229                 if (mListener != null && numReadSamples == numBurstSamples) {
    230                     mListener.sendEmptyMessage(StreamRecorderListener.MSG_BUFFER_FILL);
    231                 }
    232             }
    233 
    234             if (mListener != null) {
    235                 // TODO: on error or underrun we may be send bogus data.
    236                 mListener.sendEmptyMessage(StreamRecorderListener.MSG_STOP);
    237             }
    238             mAudioRecord.stop();
    239         }
    240     }
    241 }
    242