Last active
October 20, 2024 04:43
-
-
Save nieldeokar/fbfcae08e5612bd7cc36a30254694ee3 to your computer and use it in GitHub Desktop.
Recording an Audio with .aac extension using AudioRecord android.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Edit: Thanks for sharing
Is this method depends on device architecture? I have noticed that I'm getting onError() with Spreadtrum SC7731C processor.