Skip to content

Instantly share code, notes, and snippets.

@nieldeokar
Last active April 23, 2023 02:25
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save nieldeokar/fbfcae08e5612bd7cc36a30254694ee3 to your computer and use it in GitHub Desktop.
Save nieldeokar/fbfcae08e5612bd7cc36a30254694ee3 to your computer and use it in GitHub Desktop.
Recording an Audio with .aac extension using AudioRecord android.
<?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;
}
}
}
@andorfermichael
Copy link

Awesome! Thank you for sharing this! :)

@ithebk
Copy link

ithebk commented Sep 14, 2018

Edit: Thanks for sharing
Is this method depends on device architecture? I have noticed that I'm getting onError() with Spreadtrum SC7731C processor.

@chitrang200889
Copy link

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.

@nieldeokar
Copy link
Author

nieldeokar commented Dec 9, 2019

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment