1 /* 2 * Copyright 2020 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 package org.hyphonate.megaaudio.recorder; 17 18 import android.media.AudioDeviceInfo; 19 import android.media.AudioFormat; 20 import android.media.AudioRecord; 21 import android.os.Looper; 22 import android.os.Message; 23 import android.util.Log; 24 25 import org.hyphonate.megaaudio.common.StreamBase; 26 import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider; 27 28 /** 29 * Implementation of abstract Recorder class implemented for the Android Java-based audio record 30 * API, i.e. AudioRecord. 31 */ 32 public class JavaRecorder extends Recorder { 33 @SuppressWarnings("unused") private static final String TAG = JavaRecorder.class.getSimpleName(); 34 @SuppressWarnings("unused") private static final boolean LOG = true; 35 36 /** The buffer to receive the recorder samples */ 37 private float[] mRecorderBuffer; 38 39 /** The number of FRAMES of audio data in the record buffer */ 40 private int mNumBuffFrames; 41 42 // Recording state 43 /** <code>true</code> if currently recording audio data */ 44 private boolean mRecording = false; 45 46 /* The AudioRecord for recording the audio stream */ 47 private AudioRecord mAudioRecord = null; 48 49 private AudioSink mAudioSink; 50 51 private int mInputPreset = INPUT_PRESET_NONE; 52 53 @Override getRoutedDeviceId()54 public int getRoutedDeviceId() { 55 if (mAudioRecord != null) { 56 AudioDeviceInfo routedDevice = mAudioRecord.getRoutedDevice(); 57 return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID; 58 } else { 59 return ROUTED_DEVICE_ID_INVALID; 60 } 61 } 62 63 /** 64 * The listener to receive notifications of recording events 65 * @see {@link JavaSinkHandler} 66 */ 67 private JavaSinkHandler mListener = null; 68 JavaRecorder(AudioSinkProvider sinkProvider)69 public JavaRecorder(AudioSinkProvider sinkProvider) { 70 super(sinkProvider); 71 } 72 73 // 74 // Attributes 75 // 76 /** The buff to receive the recorder samples */ getFloatBuffer()77 public float[] getFloatBuffer() { return mRecorderBuffer; } 78 79 // JavaRecorder-specific extension getAudioRecord()80 public AudioRecord getAudioRecord() { return mAudioRecord; } 81 82 @Override setInputPreset(int preset)83 public void setInputPreset(int preset) { mInputPreset = preset; } 84 85 /* 86 * State 87 */ 88 @Override isRecording()89 public boolean isRecording() { 90 return mRecording; 91 } 92 93 @Override setupStream(int channelCount, int sampleRate, int numBurstFrames)94 public int setupStream(int channelCount, int sampleRate, int numBurstFrames) { 95 if (LOG) { 96 Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate + 97 ", frames:" + numBurstFrames); 98 } 99 mChannelCount = channelCount; 100 mSampleRate = sampleRate; 101 102 try { 103 int frameSize = calcFrameSizeInBytes(mChannelCount); 104 105 AudioRecord.Builder builder = new AudioRecord.Builder(); 106 107 builder.setAudioFormat(new AudioFormat.Builder() 108 .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 109 .setSampleRate(mSampleRate) 110 .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount)) 111 .build()); 112 // .setBufferSizeInBytes(numBurstFrames * frameSize) 113 if (mInputPreset != Recorder.INPUT_PRESET_NONE) { 114 builder.setAudioSource(mInputPreset); 115 } 116 mAudioRecord = builder.build(); 117 mAudioRecord.setPreferredDevice(mRouteDevice); 118 119 mNumBuffFrames = mAudioRecord.getBufferSizeInFrames(); 120 121 mRecorderBuffer = new float[mNumBuffFrames * mChannelCount]; 122 123 if (mSinkProvider == null) { 124 mSinkProvider = new NopAudioSinkProvider(); 125 } 126 mAudioSink = mSinkProvider.allocJavaSink(); 127 mAudioSink.init(mNumBuffFrames, mChannelCount); 128 mListener = new JavaSinkHandler(this, mAudioSink, Looper.getMainLooper()); 129 return OK; 130 } catch (UnsupportedOperationException ex) { 131 if (LOG) { 132 Log.i(TAG, "Couldn't open AudioRecord: " + ex); 133 } 134 mAudioRecord = null; 135 mNumBuffFrames = 0; 136 mRecorderBuffer = null; 137 138 return ERROR_UNSUPPORTED; 139 } 140 } 141 142 @Override teardownStream()143 public int teardownStream() { 144 stopStream(); 145 146 waitForStreamThreadToExit(); 147 148 if (mAudioRecord != null) { 149 mAudioRecord.release(); 150 mAudioRecord = null; 151 } 152 153 mChannelCount = 0; 154 mSampleRate = 0; 155 156 //TODO Retrieve errors from above 157 return OK; 158 } 159 160 @Override startStream()161 public int startStream() { 162 if (LOG) { 163 Log.i(TAG, "startStream() mAudioRecord:" + mAudioRecord); 164 } 165 if (mAudioRecord == null) { 166 return ERROR_INVALID_STATE; 167 } 168 // // Routing 169 // mAudioRecord.setPreferredDevice(mRoutingDevice); 170 171 if (mListener != null) { 172 mListener.sendEmptyMessage(JavaSinkHandler.MSG_START); 173 } 174 175 // if (mAudioSink != null) { 176 // mAudioSink.init(mNumBuffFrames, mChannelCount); 177 // } 178 try { 179 mAudioRecord.startRecording(); 180 } catch (IllegalStateException ex) { 181 Log.e(TAG, "startRecording exception: " + ex); 182 } 183 184 waitForStreamThreadToExit(); // just to be sure. 185 186 mStreamThread = new Thread(new RecorderRunnable(), "JavaRecorder Thread"); 187 mRecording = true; 188 mStreamThread.start(); 189 190 return OK; 191 } 192 193 /** 194 * Marks the stream for stopping on the next callback from the underlying system. 195 * 196 * Returns immediately, though a call to AudioSource.push() may be in progress. 197 */ 198 @Override stopStream()199 public int stopStream() { 200 mRecording = false; 201 return OK; 202 } 203 204 // @Override 205 // Used in JavaSinkHandler getDataBuffer()206 public float[] getDataBuffer() { 207 return mRecorderBuffer; 208 // System.arraycopy(mRecorderBuffer, 0, buffer, 0, mNumBuffFrames * mChannelCount); 209 } 210 211 @Override getNumBufferFrames()212 public int getNumBufferFrames() { 213 return mNumBuffFrames; 214 } 215 216 /* 217 * Recorder Thread 218 */ 219 /** 220 * Implements the <code>run</code> method for the record thread. 221 * Starts the AudioRecord, then continuously reads audio data 222 * until the flag <code>mRecording</code> is set to false (in the stop() method). 223 */ 224 private class RecorderRunnable implements Runnable { 225 @Override run()226 public void run() { 227 final int numBurstSamples = mNumBuffFrames * mChannelCount; 228 int numReadSamples = 0; 229 while (mRecording) { 230 numReadSamples = mAudioRecord.read( 231 mRecorderBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING); 232 233 if (numReadSamples < 0) { 234 // error 235 if (LOG) { 236 Log.e(TAG, "AudioRecord write error: " + numReadSamples); 237 } 238 stopStream(); 239 } else if (numReadSamples < numBurstSamples) { 240 // got less than requested? 241 if (LOG) { 242 Log.e(TAG, "AudioRecord Underflow: " + numReadSamples + 243 " vs. " + numBurstSamples); 244 } 245 stopStream(); 246 } 247 248 if (mListener != null) { 249 // TODO: on error or underrun we may be send bogus data. 250 mListener.sendEmptyMessage(JavaSinkHandler.MSG_BUFFER_FILL); 251 } 252 } 253 254 if (mListener != null) { 255 // TODO: on error or underrun we may be send bogus data. 256 Message message = new Message(); 257 message.what = JavaSinkHandler.MSG_STOP; 258 message.arg1 = numReadSamples; 259 mListener.sendMessage(message); 260 } 261 mAudioRecord.stop(); 262 } 263 } 264 } 265