|
| 1 | +package com.zeno.flutter_audio_recorder; |
| 2 | + |
| 3 | +/* |
| 4 | +~ Nilesh Deokar @nieldeokar on 09/17/18 8:11 AM |
| 5 | +*/ |
| 6 | + |
| 7 | +import android.media.AudioFormat; |
| 8 | +import android.media.AudioRecord; |
| 9 | +import android.media.MediaCodec; |
| 10 | +import android.media.MediaCodecInfo; |
| 11 | +import android.media.MediaFormat; |
| 12 | +import android.media.MediaRecorder; |
| 13 | +import android.os.Build; |
| 14 | +import android.util.Log; |
| 15 | + |
| 16 | +import java.io.File; |
| 17 | +import java.io.FileNotFoundException; |
| 18 | +import java.io.FileOutputStream; |
| 19 | +import java.io.IOException; |
| 20 | +import java.io.OutputStream; |
| 21 | +import java.nio.ByteBuffer; |
| 22 | +import java.nio.ByteOrder; |
| 23 | +import java.util.Arrays; |
| 24 | +import java.util.HashMap; |
| 25 | + |
| 26 | +public class AACRecordThread extends RecordThread { |
| 27 | + private static final String TAG = AACRecordThread.class.getSimpleName(); |
| 28 | + |
| 29 | + private static final int SAMPLE_RATE = 44100; |
| 30 | + private static final int SAMPLE_RATE_INDEX = 4; |
| 31 | + private static final int CHANNELS = 1; |
| 32 | + private static final int BIT_RATE = 32000; |
| 33 | + |
| 34 | + private MediaCodec mediaCodec = null; |
| 35 | + private AudioRecord audioRecord = null; |
| 36 | + private FileOutputStream fileOutputStream = null; |
| 37 | + private double peakPower = -120; |
| 38 | + private double averagePower = -120; |
| 39 | + private Thread recordingThread = null; |
| 40 | + private long dataSize = 0; |
| 41 | + |
| 42 | + |
| 43 | + AACRecordThread(int sampleRate, String filePath, String extension) { |
| 44 | + super(sampleRate, filePath, extension); |
| 45 | + // TODO: find a way to change sample rate |
| 46 | + this.sampleRate = SAMPLE_RATE; |
| 47 | + } |
| 48 | + |
| 49 | + @Override |
| 50 | + public void start() throws IOException { |
| 51 | + this.audioRecord = createAudioRecord(this.bufferSize); |
| 52 | + this.mediaCodec = createMediaCodec(this.bufferSize); |
| 53 | + |
| 54 | + fileOutputStream = new FileOutputStream(this.filePath); |
| 55 | + |
| 56 | + this.mediaCodec.start(); |
| 57 | + |
| 58 | + try { |
| 59 | + audioRecord.startRecording(); |
| 60 | + } catch (Exception e) { |
| 61 | + Log.w(TAG, e); |
| 62 | + mediaCodec.release(); |
| 63 | + throw new IOException(e); |
| 64 | + } |
| 65 | + |
| 66 | + status = "recording"; |
| 67 | + startThread(); |
| 68 | + } |
| 69 | + |
| 70 | + @Override |
| 71 | + public void pause() { |
| 72 | + status = "paused"; |
| 73 | + peakPower = -120; |
| 74 | + averagePower = -120; |
| 75 | + audioRecord.stop(); |
| 76 | + recordingThread = null; |
| 77 | + } |
| 78 | + |
| 79 | + @Override |
| 80 | + public void resume() { |
| 81 | + status = "recording"; |
| 82 | + audioRecord.startRecording(); |
| 83 | + startThread(); |
| 84 | + } |
| 85 | + |
| 86 | + @Override |
| 87 | + public HashMap<String, Object> stop() { |
| 88 | + status = "stopped"; |
| 89 | + |
| 90 | + // Return Recording Object |
| 91 | + HashMap<String, Object> currentResult = new HashMap<>(); |
| 92 | + currentResult.put("duration", getDuration() * 1000); |
| 93 | + currentResult.put("path", filePath); |
| 94 | + currentResult.put("audioFormat", extension); |
| 95 | + currentResult.put("peakPower", peakPower); |
| 96 | + currentResult.put("averagePower", averagePower); |
| 97 | + currentResult.put("isMeteringEnabled", true); |
| 98 | + currentResult.put("status", status); |
| 99 | + |
| 100 | + |
| 101 | + resetRecorder(); |
| 102 | + recordingThread = null; |
| 103 | + |
| 104 | + mediaCodec.stop(); |
| 105 | + audioRecord.stop(); |
| 106 | + |
| 107 | + mediaCodec.release(); |
| 108 | + audioRecord.release(); |
| 109 | + |
| 110 | + try { |
| 111 | + fileOutputStream.close(); |
| 112 | + } catch (IOException e) { |
| 113 | + e.printStackTrace(); |
| 114 | + } |
| 115 | + |
| 116 | + return currentResult; |
| 117 | + } |
| 118 | + |
| 119 | + @Override |
| 120 | + public int getDuration() { |
| 121 | + long duration = dataSize / (sampleRate * 2 * 1); |
| 122 | + return (int) duration; |
| 123 | + } |
| 124 | + |
| 125 | + @Override |
| 126 | + public double getPeakPower() { |
| 127 | + return peakPower; |
| 128 | + } |
| 129 | + |
| 130 | + @Override |
| 131 | + public double getAveragePower() { |
| 132 | + return averagePower; |
| 133 | + } |
| 134 | + |
| 135 | + @Override |
| 136 | + public String getFilePath() { |
| 137 | + return filePath; |
| 138 | + } |
| 139 | + |
| 140 | + @Override |
| 141 | + public void run() { |
| 142 | + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); |
| 143 | + ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers(); |
| 144 | + ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers(); |
| 145 | + |
| 146 | + while (status == "recording") { |
| 147 | + boolean success = handleCodecInput(audioRecord, mediaCodec, codecInputBuffers, Thread.currentThread().isAlive()); |
| 148 | + if (success) { |
| 149 | + try { |
| 150 | + handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, fileOutputStream); |
| 151 | + } catch (IOException e) { |
| 152 | + e.printStackTrace(); |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + private void startThread() { |
| 159 | + recordingThread = new Thread(this, "Audio Processing Thread"); |
| 160 | + recordingThread.start(); |
| 161 | + } |
| 162 | + |
| 163 | + private void resetRecorder() { |
| 164 | + peakPower = -120; |
| 165 | + averagePower = -120; |
| 166 | + dataSize = 0; |
| 167 | + } |
| 168 | + |
| 169 | + private boolean handleCodecInput(AudioRecord audioRecord, |
| 170 | + MediaCodec mediaCodec, ByteBuffer[] codecInputBuffers, |
| 171 | + boolean running) { |
| 172 | + byte[] audioRecordData = new byte[bufferSize]; |
| 173 | + int length = audioRecord.read(audioRecordData, 0, audioRecordData.length); |
| 174 | + dataSize += audioRecordData.length; |
| 175 | + updatePowers(audioRecordData); |
| 176 | + |
| 177 | + if (length == AudioRecord.ERROR_BAD_VALUE || |
| 178 | + length == AudioRecord.ERROR_INVALID_OPERATION || |
| 179 | + length != bufferSize) { |
| 180 | + |
| 181 | + if (length != bufferSize) { |
| 182 | + Log.d(TAG, "length != bufferSize"); |
| 183 | + return false; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000); |
| 188 | + |
| 189 | + if (codecInputBufferIndex >= 0) { |
| 190 | + ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex]; |
| 191 | + codecBuffer.clear(); |
| 192 | + codecBuffer.put(audioRecordData); |
| 193 | + mediaCodec.queueInputBuffer(codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
| 194 | + } |
| 195 | + |
| 196 | + return true; |
| 197 | + } |
| 198 | + |
| 199 | + private void handleCodecOutput(MediaCodec mediaCodec, |
| 200 | + ByteBuffer[] codecOutputBuffers, |
| 201 | + MediaCodec.BufferInfo bufferInfo, |
| 202 | + OutputStream outputStream) |
| 203 | + throws IOException { |
| 204 | + int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); |
| 205 | + |
| 206 | + while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { |
| 207 | + if (codecOutputBufferIndex >= 0) { |
| 208 | + ByteBuffer encoderOutputBuffer = codecOutputBuffers[codecOutputBufferIndex]; |
| 209 | + |
| 210 | + encoderOutputBuffer.position(bufferInfo.offset); |
| 211 | + encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size); |
| 212 | + |
| 213 | + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { |
| 214 | + byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset); |
| 215 | + |
| 216 | + |
| 217 | + outputStream.write(header); |
| 218 | + |
| 219 | + byte[] data = new byte[encoderOutputBuffer.remaining()]; |
| 220 | + encoderOutputBuffer.get(data); |
| 221 | + outputStream.write(data); |
| 222 | + } |
| 223 | + |
| 224 | + encoderOutputBuffer.clear(); |
| 225 | + |
| 226 | + mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false); |
| 227 | + } else if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { |
| 228 | + codecOutputBuffers = mediaCodec.getOutputBuffers(); |
| 229 | + } |
| 230 | + |
| 231 | + codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + |
| 236 | + private void updatePowers(byte[] bdata) { |
| 237 | + short[] data = byte2short(bdata); |
| 238 | + short sampleVal = data[data.length - 1]; |
| 239 | + String[] escapeStatusList = new String[]{"paused", "stopped", "initialized", "unset"}; |
| 240 | + |
| 241 | + if (sampleVal == 0 || Arrays.asList(escapeStatusList).contains(status)) { |
| 242 | + averagePower = -120; // to match iOS silent case |
| 243 | + } else { |
| 244 | + // iOS factor : to match iOS power level |
| 245 | + double iOSFactor = 0.25; |
| 246 | + averagePower = 20 * Math.log(Math.abs(sampleVal) / 32768.0) * iOSFactor; |
| 247 | + } |
| 248 | + |
| 249 | + peakPower = averagePower; |
| 250 | + // Log.d(LOG_NAME, "Peak: " + mPeakPower + " average: "+ mAveragePower); |
| 251 | + } |
| 252 | + |
| 253 | + private short[] byte2short(byte[] bData) { |
| 254 | + short[] out = new short[bData.length / 2]; |
| 255 | + ByteBuffer.wrap(bData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(out); |
| 256 | + return out; |
| 257 | + } |
| 258 | + |
| 259 | + private byte[] createAdtsHeader(int length) { |
| 260 | + int frameLength = length + 7; |
| 261 | + byte[] adtsHeader = new byte[7]; |
| 262 | + |
| 263 | + adtsHeader[0] = (byte) 0xFF; // Sync Word |
| 264 | + adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC |
| 265 | + adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6); |
| 266 | + adtsHeader[2] |= (((byte) SAMPLE_RATE_INDEX) << 2); |
| 267 | + adtsHeader[2] |= (((byte) CHANNELS) >> 2); |
| 268 | + adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03)); |
| 269 | + adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF); |
| 270 | + adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f); |
| 271 | + adtsHeader[6] = (byte) 0xFC; |
| 272 | + |
| 273 | + return adtsHeader; |
| 274 | + } |
| 275 | + |
| 276 | + private AudioRecord createAudioRecord(int bufferSize) { |
| 277 | + AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, |
| 278 | + AudioFormat.CHANNEL_IN_MONO, |
| 279 | + AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10); |
| 280 | + |
| 281 | + if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { |
| 282 | + Log.d(TAG, "Unable to initialize AudioRecord"); |
| 283 | + throw new RuntimeException("Unable to initialize AudioRecord"); |
| 284 | + } |
| 285 | + |
| 286 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { |
| 287 | + if (android.media.audiofx.NoiseSuppressor.isAvailable()) { |
| 288 | + android.media.audiofx.NoiseSuppressor noiseSuppressor = android.media.audiofx.NoiseSuppressor |
| 289 | + .create(audioRecord.getAudioSessionId()); |
| 290 | + if (noiseSuppressor != null) { |
| 291 | + noiseSuppressor.setEnabled(true); |
| 292 | + } |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + |
| 297 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { |
| 298 | + if (android.media.audiofx.AutomaticGainControl.isAvailable()) { |
| 299 | + android.media.audiofx.AutomaticGainControl automaticGainControl = android.media.audiofx.AutomaticGainControl |
| 300 | + .create(audioRecord.getAudioSessionId()); |
| 301 | + if (automaticGainControl != null) { |
| 302 | + automaticGainControl.setEnabled(true); |
| 303 | + } |
| 304 | + } |
| 305 | + } |
| 306 | + |
| 307 | + |
| 308 | + return audioRecord; |
| 309 | + } |
| 310 | + |
| 311 | + private MediaCodec createMediaCodec(int bufferSize) throws IOException { |
| 312 | + MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); |
| 313 | + MediaFormat mediaFormat = new MediaFormat(); |
| 314 | + |
| 315 | + mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); |
| 316 | + mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate); |
| 317 | + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); |
| 318 | + mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); |
| 319 | + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); |
| 320 | + mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); |
| 321 | + |
| 322 | + try { |
| 323 | + mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
| 324 | + } catch (Exception e) { |
| 325 | + Log.w(TAG, e); |
| 326 | + mediaCodec.release(); |
| 327 | + throw new IOException(e); |
| 328 | + } |
| 329 | + |
| 330 | + return mediaCodec; |
| 331 | + } |
| 332 | +} |
0 commit comments