-
-
Save nieldeokar/fbfcae08e5612bd7cc36a30254694ee3 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?> | |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<Button | |
android:id="@+id/btnStart" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Start " | |
android:layout_centerInParent="true" | |
android:layout_margin="4dp" | |
android:padding="4dp" | |
/> | |
<Button | |
android:id="@+id/btnStop" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Stop " | |
android:layout_below="@+id/btnStart" | |
android:layout_centerHorizontal="true" | |
android:layout_margin="4dp" | |
android:padding="4dp" | |
/> | |
</RelativeLayout> |
package com.nieldeokar.whatsappaudiorecorder; | |
import android.util.Log; | |
import com.nieldeokar.whatsappaudiorecorder.recorder.AudioRecordThread; | |
import com.nieldeokar.whatsappaudiorecorder.recorder.OnAudioRecordListener; | |
import com.nieldeokar.whatsappaudiorecorder.recorder.RecordingItem; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
/* | |
~ Nilesh Deokar @nieldeokar on 09/18/18 6:24 PM | |
*/ | |
public class AudioRecording { | |
private static final String TAG = "AudioRecording"; | |
private File file; | |
private OnAudioRecordListener onAudioRecordListener; | |
private long mStartingTimeMillis = 0; | |
private static final int IO_ERROR = 1; | |
private static final int RECORDER_ERROR = 2; | |
public static final int FILE_NULL = 3; | |
private Thread mRecordingThread; | |
public AudioRecording() { | |
} | |
public void setOnAudioRecordListener(OnAudioRecordListener onAudioRecordListener) { | |
this.onAudioRecordListener = onAudioRecordListener; | |
} | |
public void setFile(String filePath) { | |
this.file = new File(filePath); | |
} | |
// Call this method from Activity onStartButton Click to start recording | |
public synchronized void startRecording() { | |
if(file == null) { | |
onAudioRecordListener.onError(FILE_NULL ); | |
return; | |
} | |
mStartingTimeMillis = System.currentTimeMillis(); | |
try { | |
if(mRecordingThread != null) stopRecording(true); | |
mRecordingThread = new Thread(new AudioRecordThread(outputStream(file),new AudioRecordThread.OnRecorderFailedListener() { | |
@Override | |
public void onRecorderFailed() { | |
onAudioRecordListener.onError(RECORDER_ERROR); | |
stopRecording(true); | |
} | |
@Override | |
public void onRecorderStarted() { | |
onAudioRecordListener.onRecordingStarted(); | |
} | |
})); | |
mRecordingThread.setName("AudioRecordingThread"); | |
mRecordingThread.start(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
// Call this method from Activity onStopButton Click to stop recording | |
public synchronized void stopRecording(Boolean cancel){ | |
Log.d(TAG, "Recording stopped "); | |
if(mRecordingThread != null){ | |
mRecordingThread.interrupt(); | |
mRecordingThread = null; | |
if (file.length() == 0L) { | |
onAudioRecordListener.onError(IO_ERROR); | |
return; | |
} | |
// total recorded time | |
long mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis); | |
if (!cancel) { | |
onAudioRecordListener.onRecordFinished(); | |
} else { | |
deleteFile(); | |
} | |
} | |
} | |
private void deleteFile() { | |
if (file != null && file.exists()) | |
Log.d(TAG, String.format("deleting file success %b ", file.delete())); | |
} | |
private OutputStream outputStream(File file) { | |
if (file == null) { | |
throw new RuntimeException("file is null !"); | |
} | |
OutputStream outputStream; | |
try { | |
outputStream = new FileOutputStream(file); | |
} catch (FileNotFoundException e) { | |
throw new RuntimeException( | |
"could not build OutputStream from" + " this file " + file.getName(), e); | |
} | |
return outputStream; | |
} | |
public interface OnAudioRecordListener { | |
void onRecordFinished(); | |
void onError(int errorCode); | |
void onRecordingStarted(); | |
} | |
} |
package com.nieldeokar.whatsappaudiorecorder.recorder; | |
import android.media.AudioFormat; | |
import android.media.AudioRecord; | |
import android.media.MediaCodec; | |
import android.media.MediaCodecInfo; | |
import android.media.MediaFormat; | |
import android.media.MediaRecorder; | |
import android.os.Build; | |
import android.util.Log; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
import java.nio.ByteBuffer; | |
/* | |
~ Nilesh Deokar @nieldeokar on 09/17/18 8:11 AM | |
*/ | |
public class AudioRecordThread implements Runnable { | |
private static final String TAG = AudioRecordThread.class.getSimpleName(); | |
private static final int SAMPLE_RATE = 44100; | |
private static final int SAMPLE_RATE_INDEX = 4; | |
private static final int CHANNELS = 1; | |
private static final int BIT_RATE = 32000; | |
private final int bufferSize; | |
private final MediaCodec mediaCodec; | |
private final AudioRecord audioRecord; | |
private final OutputStream outputStream; | |
private OnRecorderFailedListener onRecorderFailedListener; | |
AudioRecordThread(OutputStream outputStream, OnRecorderFailedListener onRecorderFailedListener) throws IOException { | |
this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); | |
this.audioRecord = createAudioRecord(this.bufferSize); | |
this.mediaCodec = createMediaCodec(this.bufferSize); | |
this.outputStream = outputStream; | |
this.onRecorderFailedListener = onRecorderFailedListener; | |
this.mediaCodec.start(); | |
try { | |
audioRecord.startRecording(); | |
} catch (Exception e) { | |
Log.w(TAG, e); | |
mediaCodec.release(); | |
throw new IOException(e); | |
} | |
} | |
@Override | |
public void run() { | |
if (onRecorderFailedListener != null) { | |
Log.d(TAG, "onRecorderStarted"); | |
onRecorderFailedListener.onRecorderStarted(); | |
} | |
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); | |
ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers(); | |
ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers(); | |
try { | |
while (!Thread.interrupted()) { | |
boolean success = handleCodecInput(audioRecord, mediaCodec, codecInputBuffers, Thread.currentThread().isAlive()); | |
if (success) | |
handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream); | |
} | |
} catch (IOException e) { | |
Log.w(TAG, e); | |
} finally { | |
mediaCodec.stop(); | |
audioRecord.stop(); | |
mediaCodec.release(); | |
audioRecord.release(); | |
try { | |
outputStream.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
private boolean handleCodecInput(AudioRecord audioRecord, | |
MediaCodec mediaCodec, ByteBuffer[] codecInputBuffers, | |
boolean running) throws IOException { | |
byte[] audioRecordData = new byte[bufferSize]; | |
int length = audioRecord.read(audioRecordData, 0, audioRecordData.length); | |
if (length == AudioRecord.ERROR_BAD_VALUE || | |
length == AudioRecord.ERROR_INVALID_OPERATION || | |
length != bufferSize) { | |
if (length != bufferSize) { | |
if (onRecorderFailedListener != null) { | |
Log.d(TAG, "length != BufferSize calling onRecordFailed"); | |
onRecorderFailedListener.onRecorderFailed(); | |
} | |
return false; | |
} | |
} | |
int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000); | |
if (codecInputBufferIndex >= 0) { | |
ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex]; | |
codecBuffer.clear(); | |
codecBuffer.put(audioRecordData); | |
mediaCodec.queueInputBuffer(codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); | |
} | |
return true; | |
} | |
private void handleCodecOutput(MediaCodec mediaCodec, | |
ByteBuffer[] codecOutputBuffers, | |
MediaCodec.BufferInfo bufferInfo, | |
OutputStream outputStream) | |
throws IOException { | |
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); | |
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { | |
if (codecOutputBufferIndex >= 0) { | |
ByteBuffer encoderOutputBuffer = codecOutputBuffers[codecOutputBufferIndex]; | |
encoderOutputBuffer.position(bufferInfo.offset); | |
encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size); | |
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { | |
byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset); | |
outputStream.write(header); | |
byte[] data = new byte[encoderOutputBuffer.remaining()]; | |
encoderOutputBuffer.get(data); | |
outputStream.write(data); | |
} | |
encoderOutputBuffer.clear(); | |
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false); | |
} else if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { | |
codecOutputBuffers = mediaCodec.getOutputBuffers(); | |
} | |
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); | |
} | |
} | |
private byte[] createAdtsHeader(int length) { | |
int frameLength = length + 7; | |
byte[] adtsHeader = new byte[7]; | |
adtsHeader[0] = (byte) 0xFF; // Sync Word | |
adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC | |
adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6); | |
adtsHeader[2] |= (((byte) SAMPLE_RATE_INDEX) << 2); | |
adtsHeader[2] |= (((byte) CHANNELS) >> 2); | |
adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03)); | |
adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF); | |
adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f); | |
adtsHeader[6] = (byte) 0xFC; | |
return adtsHeader; | |
} | |
private AudioRecord createAudioRecord(int bufferSize) { | |
AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, | |
AudioFormat.CHANNEL_IN_MONO, | |
AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10); | |
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { | |
Log.d(TAG, "Unable to initialize AudioRecord"); | |
throw new RuntimeException("Unable to initialize AudioRecord"); | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | |
if (android.media.audiofx.NoiseSuppressor.isAvailable()) { | |
android.media.audiofx.NoiseSuppressor noiseSuppressor = android.media.audiofx.NoiseSuppressor | |
.create(audioRecord.getAudioSessionId()); | |
if (noiseSuppressor != null) { | |
noiseSuppressor.setEnabled(true); | |
} | |
} | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | |
if (android.media.audiofx.AutomaticGainControl.isAvailable()) { | |
android.media.audiofx.AutomaticGainControl automaticGainControl = android.media.audiofx.AutomaticGainControl | |
.create(audioRecord.getAudioSessionId()); | |
if (automaticGainControl != null) { | |
automaticGainControl.setEnabled(true); | |
} | |
} | |
} | |
return audioRecord; | |
} | |
private MediaCodec createMediaCodec(int bufferSize) throws IOException { | |
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); | |
MediaFormat mediaFormat = new MediaFormat(); | |
mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); | |
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); | |
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); | |
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); | |
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); | |
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); | |
try { | |
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); | |
} catch (Exception e) { | |
Log.w(TAG, e); | |
mediaCodec.release(); | |
throw new IOException(e); | |
} | |
return mediaCodec; | |
} | |
interface OnRecorderFailedListener { | |
void onRecorderFailed(); | |
void onRecorderStarted(); | |
} | |
} |
package com.nieldeokar.whatsappaudiorecorder; | |
import android.os.Environment; | |
import android.support.v7.app.AppCompatActivity; | |
import android.os.Bundle; | |
import android.util.Log; | |
import android.view.View; | |
import android.widget.Button; | |
/* | |
~ Nilesh Deokar @nieldeokar on 09/18/18 6:25 PM | |
*/ | |
import java.io.File; | |
public class MainActivity extends AppCompatActivity implements View.OnClickListener { | |
AudioRecording mAudioRecording; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
Button btnStart = findViewById(R.id.btnStart); | |
Button btnStop = findViewById(R.id.btnStop); | |
btnStart.setOnClickListener(this); | |
mAudioRecording = new AudioRecording(); | |
} | |
private void startRecording() { | |
AudioRecording.OnAudioRecordListener onRecordListener = new AudioRecording.OnAudioRecordListener() { | |
@Override | |
public void onRecordFinished() { | |
Log.d("MAIN","onFinish "); | |
} | |
@Override | |
public void onError(int e) { | |
Log.d("MAIN","onError "+e); | |
} | |
@Override | |
public void onRecordingStarted() { | |
Log.d("MAIN","onStart "); | |
} | |
}; | |
String filePath = new File(Environment.getExternalStorageDirectory(),"Recorder") + "/" + System.currentTimeMillis() + ".aac"; | |
mAudioRecording.setOnAudioRecordListener(onRecordListener); | |
mAudioRecording.setFile(filePath); | |
} | |
private void stopRecording() { | |
if( mAudioRecording != null){ | |
mAudioRecording.stopRecording(false); | |
} | |
} | |
@Override | |
public void onClick(View view) { | |
switch (view.getId()){ | |
case R.id.btnStart: | |
startRecording(); | |
break; | |
case R.id.btnStop: | |
stopRecording(); | |
break; | |
} | |
} | |
} |
Edit: Thanks for sharing
Is this method depends on device architecture? I have noticed that I'm getting onError() with Spreadtrum SC7731C processor.
Hi Nilesh, thanks for sharing the code.
Can it be used for .m4a file?
If yes, then hod do I update createAdtsHeader method?
Thanks in advance.
@chitrang200889 : That was written long time ago. It's hard to recall it correctly. I would suggest try messing around headers of .m4a file. This link https://www.file-recovery.com/m4a-signature-format.htm gives good overview of headers of .m4a. I don't think so you even need to change headers for it. You can verify header bytes here. https://www.p23.nl/projects/aac-header/
This SO thread might be useful. https://stackoverflow.com/q/18862715/3746306
Hi Nilesh, thanks for sharing the code.
Can it be used for .m4a file?
If yes, then hod do I update createAdtsHeader method?Thanks in advance.
Awesome! Thank you for sharing this! :)