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.BuilderBase; 26 import org.hyphonate.megaaudio.common.StreamBase; 27 import org.hyphonate.megaaudio.common.StreamState; 28 import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider; 29 30 /** 31 * Implementation of abstract Recorder class implemented for the Android Java-based audio record 32 * API, i.e. AudioRecord. 33 */ 34 public class JavaRecorder extends Recorder { 35 @SuppressWarnings("unused") 36 private static final String TAG = JavaRecorder.class.getSimpleName(); 37 @SuppressWarnings("unused") 38 private static final boolean LOG = true; 39 40 /** 41 * The buffer to receive the recorder samples 42 */ 43 private float[] mRecorderBuffer; 44 45 /* The AudioRecord for recording the audio stream */ 46 private AudioRecord mAudioRecord = null; 47 48 private AudioSink mAudioSink; 49 50 @Override getRoutedDeviceId()51 public int getRoutedDeviceId() { 52 if (mAudioRecord != null) { 53 AudioDeviceInfo routedDevice = mAudioRecord.getRoutedDevice(); 54 return routedDevice != null 55 ? routedDevice.getId() : BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 56 } else { 57 return BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 58 } 59 } 60 61 /** 62 * The listener to receive notifications of recording events 63 */ 64 private JavaSinkHandler mListener = null; 65 JavaRecorder(RecorderBuilder builder, AudioSinkProvider sinkProvider)66 public JavaRecorder(RecorderBuilder builder, AudioSinkProvider sinkProvider) { 67 super(sinkProvider); 68 setupStream(builder); 69 } 70 71 // 72 // Attributes 73 // 74 @Override getSharingMode()75 public int getSharingMode() { 76 // JAVA Audio API does not support a sharing mode 77 return BuilderBase.SHARING_MODE_NOTSUPPORTED; 78 } 79 80 @Override getChannelCount()81 public int getChannelCount() { 82 return mAudioRecord != null ? mAudioRecord.getChannelCount() : -1; 83 } 84 85 @Override isMMap()86 public boolean isMMap() { 87 // Java Streams are never MMAP 88 return false; 89 } 90 91 /** 92 * The buff to receive the recorder samples 93 */ getFloatBuffer()94 public float[] getFloatBuffer() { 95 return mRecorderBuffer; 96 } 97 98 // JavaRecorder-specific extension getAudioRecord()99 public AudioRecord getAudioRecord() { 100 return mAudioRecord; 101 } 102 setupStream(RecorderBuilder builder)103 private int setupStream(RecorderBuilder builder) { 104 mChannelCount = builder.getChannelCount(); 105 mSampleRate = builder.getSampleRate(); 106 mNumExchangeFrames = builder.getNumExchangeFrames(); 107 mSharingMode = builder.getSharingMode(); 108 mPerformanceMode = builder.getPerformanceMode(); 109 mInputPreset = builder.getInputPreset(); 110 111 if (LOG) { 112 Log.i(TAG, "setupStream()"); 113 Log.i(TAG, " chans:" + mChannelCount); 114 Log.i(TAG, " rate: " + mSampleRate); 115 Log.i(TAG, " frames: " + mNumExchangeFrames); 116 Log.i(TAG, " perf mode: " + mPerformanceMode); 117 Log.i(TAG, " route device: " + builder.getRouteDeviceId()); 118 Log.i(TAG, " preset: " + mInputPreset); 119 } 120 121 try { 122 // int bufferSizeInBytes = mNumExchangeFrames * mChannelCount 123 // * sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT); 124 // Log.i(TAG, " bufferSizeInBytes:" + bufferSizeInBytes); 125 // Log.i(TAG, " (in frames)" + (bufferSizeInBytes / 4 / mChannelCount)); 126 127 AudioFormat.Builder formatBuilder = new AudioFormat.Builder(); 128 formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 129 .setSampleRate(mSampleRate) 130 .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount)); 131 132 AudioRecord.Builder recordBuilder = new AudioRecord.Builder(); 133 recordBuilder.setAudioFormat(formatBuilder.build()) 134 /*.setBufferSizeInBytes(bufferSizeInBytes)*/; 135 if (mInputPreset != Recorder.INPUT_PRESET_NONE) { 136 recordBuilder.setAudioSource(mInputPreset); 137 } 138 mAudioRecord = recordBuilder.build(); 139 mNumExchangeFrames = mAudioRecord.getBufferSizeInFrames(); 140 if (LOG) { 141 Log.i(TAG, " mAudioRecord.getBufferSizeInFrames(): " 142 + mAudioRecord.getBufferSizeInFrames()); 143 } 144 mAudioRecord.setPreferredDevice(builder.getRouteDevice()); 145 146 mRecorderBuffer = new float[mNumExchangeFrames * mChannelCount]; 147 148 if (mSinkProvider == null) { 149 mSinkProvider = new NopAudioSinkProvider(); 150 } 151 mAudioSink = mSinkProvider.allocJavaSink(); 152 mAudioSink.init(mNumExchangeFrames, mChannelCount); 153 mListener = new JavaSinkHandler(this, mAudioSink, Looper.getMainLooper()); 154 return OK; 155 } catch (UnsupportedOperationException ex) { 156 if (LOG) { 157 Log.e(TAG, "Couldn't open AudioRecord: " + ex); 158 } 159 return ERROR_UNSUPPORTED; 160 } catch (java.lang.IllegalArgumentException ex) { 161 if (LOG) { 162 Log.e(TAG, "Invalid arguments to AudioRecord.Builder: " + ex); 163 } 164 return ERROR_UNSUPPORTED; 165 } 166 } 167 168 @Override teardownStream()169 public int teardownStream() { 170 if (LOG) { 171 Log.i(TAG, "teardownStream()"); 172 } 173 stopStream(); 174 175 waitForStreamThreadToExit(); 176 177 if (mAudioRecord != null) { 178 mAudioRecord.release(); 179 mAudioRecord = null; 180 } 181 182 mChannelCount = 0; 183 mSampleRate = 0; 184 185 //TODO Retrieve errors from above 186 return OK; 187 } 188 189 @Override startStream()190 public int startStream() { 191 if (LOG) { 192 Log.i(TAG, "startStream() mAudioRecord:" + mAudioRecord); 193 } 194 if (mAudioRecord == null) { 195 return ERROR_INVALID_STATE; 196 } 197 if (mListener != null) { 198 mListener.sendEmptyMessage(JavaSinkHandler.MSG_START); 199 } 200 201 try { 202 mAudioRecord.startRecording(); 203 } catch (IllegalStateException ex) { 204 Log.e(TAG, "startRecording exception: " + ex); 205 } 206 207 waitForStreamThreadToExit(); // just to be sure. 208 209 mStreamThread = new Thread(new RecorderRunnable(), "JavaRecorder Thread"); 210 mRecording = true; 211 mStreamThread.start(); 212 213 return OK; 214 } 215 216 /** 217 * Marks the stream for stopping on the next callback from the underlying system. 218 * 219 * Returns immediately, though a call to AudioSource.push() may be in progress. 220 */ 221 @Override stopStream()222 public int stopStream() { 223 mRecording = false; 224 return OK; 225 } 226 227 /** 228 * @return See StreamState constants 229 */ getStreamState()230 public int getStreamState() { 231 //TODO - track state so we can return something meaningful here. 232 return StreamState.UNKNOWN; 233 } 234 235 /** 236 * @return The last error callback result (these must match Oboe). See Oboe constants 237 */ getLastErrorCallbackResult()238 public int getLastErrorCallbackResult() { 239 //TODO - track errors so we can return something meaningful here. 240 return ERROR_UNKNOWN; 241 } 242 243 // @Override 244 // Used in JavaSinkHandler getDataBuffer()245 public float[] getDataBuffer() { 246 return mRecorderBuffer; 247 } 248 249 /* 250 * Recorder Thread 251 */ 252 /** 253 * Implements the <code>run</code> method for the record thread. 254 * Starts the AudioRecord, then continuously reads audio data 255 * until the flag <code>mRecording</code> is set to false (in the stop() method). 256 */ 257 private class RecorderRunnable implements Runnable { 258 @Override run()259 public void run() { 260 final int numRecordSamples = mNumExchangeFrames * mChannelCount; 261 if (LOG) { 262 Log.i(TAG, "numRecordSamples: " + numRecordSamples); 263 } 264 265 int numReadSamples = 0; 266 while (mRecording) { 267 numReadSamples = mAudioRecord.read( 268 mRecorderBuffer, 0, numRecordSamples, AudioRecord.READ_BLOCKING); 269 if (numReadSamples < 0) { 270 // error 271 if (LOG) { 272 Log.e(TAG, "AudioRecord write error - numReadSamples: " + numReadSamples); 273 } 274 stopStream(); 275 } else if (numReadSamples < numRecordSamples) { 276 // got less than requested? 277 if (LOG) { 278 Log.e(TAG, "AudioRecord Underflow: " + numReadSamples + 279 " vs. " + numRecordSamples); 280 } 281 stopStream(); 282 } 283 284 if (mListener != null) { 285 // TODO: on error or underrun we may be send bogus data. 286 mListener.sendEmptyMessage(JavaSinkHandler.MSG_BUFFER_FILL); 287 } 288 } 289 290 if (mListener != null) { 291 // TODO: on error or underrun we may be send bogus data. 292 Message message = new Message(); 293 message.what = JavaSinkHandler.MSG_STOP; 294 message.arg1 = numReadSamples; 295 mListener.sendMessage(message); 296 } 297 mAudioRecord.stop(); 298 } 299 } 300 } 301