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