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 int chanPosMask = AudioUtils.countToInPositionMask(numChans); 124 int bufferSizeInBytes = 2048; // Some, non-critical value 125 126 try { 127 mAudioRecord = new AudioRecord.Builder() 128 .setAudioFormat(new AudioFormat.Builder() 129 .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 130 .setSampleRate(mSampleRate) 131 .setChannelMask(chanPosMask) 132 .build()) 133 .setBufferSizeInBytes(bufferSizeInBytes) 134 .build(); 135 136 return true; 137 } catch (UnsupportedOperationException ex) { 138 Log.i(TAG, "Couldn't open AudioRecord: " + ex); 139 mAudioRecord = null; 140 return false; 141 } 142 } 143 144 public boolean open(int numChans, int sampleRate, int numBurstFrames) { 145 boolean sucess = open_internal(numChans, sampleRate); 146 if (sucess) { 147 mNumBurstFrames = numBurstFrames; 148 mBurstBuffer = new float[mNumBurstFrames * mNumChannels]; 149 // put some non-zero data in the burst buffer. 150 // this is to verify that the record is putting SOMETHING into each channel. 151 for(int index = 0; index < mBurstBuffer.length; index++) { 152 mBurstBuffer[index] = (float)(Math.random() * 2.0) - 1.0f; 153 } 154 } 155 156 return sucess; 157 } 158 159 public void close() { 160 stop(); 161 162 waitForRecorderThreadToExit(); 163 164 mAudioRecord.release(); 165 mAudioRecord = null; 166 } 167 168 public boolean start() { 169 mAudioRecord.setPreferredDevice(mRoutingDevice); 170 171 if (mListener != null) { 172 mListener.sendEmptyMessage(StreamRecorderListener.MSG_START); 173 } 174 175 try { 176 mAudioRecord.startRecording(); 177 } catch (IllegalStateException ex) { 178 Log.i("", "ex: " + ex); 179 } 180 mRecording = true; 181 182 waitForRecorderThreadToExit(); // just to be sure. 183 184 mRecorderThread = new Thread(new StreamRecorderRunnable(), "StreamRecorder Thread"); 185 mRecorderThread.start(); 186 187 return true; 188 } 189 190 public void stop() { 191 if (mRecording) { 192 mRecording = false; 193 } 194 } 195 196 /* 197 * StreamRecorderRunnable 198 */ 199 private class StreamRecorderRunnable implements Runnable { 200 @Override 201 public void run() { 202 final int numBurstSamples = mNumBurstFrames * mNumChannels; 203 while (mRecording) { 204 int numReadSamples = mAudioRecord.read( 205 mBurstBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING); 206 207 if (numReadSamples < 0) { 208 // error 209 Log.i(TAG, "AudioRecord write error: " + numReadSamples); 210 stop(); 211 } else if (numReadSamples < numBurstSamples) { 212 // got less than requested? 213 Log.i(TAG, "AudioRecord Underflow: " + numReadSamples + 214 " vs. " + numBurstSamples); 215 stop(); 216 } 217 218 if (mListener != null && numReadSamples == numBurstSamples) { 219 mListener.sendEmptyMessage(StreamRecorderListener.MSG_BUFFER_FILL); 220 } 221 } 222 223 if (mListener != null) { 224 // TODO: on error or underrun we may be send bogus data. 225 mListener.sendEmptyMessage(StreamRecorderListener.MSG_STOP); 226 } 227 mAudioRecord.stop(); 228 } 229 } 230 } 231