Skip to content

Commit c45e135

Browse files
committed
Applied PR rmbrone#52
1 parent 266e82f commit c45e135

20 files changed

+1018
-420
lines changed

android/build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ android {
3232
lintOptions {
3333
disable 'InvalidPackage'
3434
}
35+
compileOptions {
36+
sourceCompatibility = '1.8'
37+
targetCompatibility = '1.8'
38+
}
3539
}
3640

3741
dependencies {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
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

Comments
 (0)