Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An example of how to read in raw PCM data from Android's AudioRecord API (microphone input, for instance) and output it to a valid WAV file. Tested on API 21/23 on Android and API 23 on Android Wear (modified activity) where AudioRecord is the only available audio recording API. MediaRecorder doesn't work. Compiles against min API 15 and probabl…
/*
* Copyright 2016 Kevin Mark
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* --
* An example of how to read in raw PCM data from Android's AudioRecord API (microphone input, for
* instance) and output it to a valid WAV file. Tested on API 21/23 on Android and API 23 on
* Android Wear (modified activity) where AudioRecord is the only available audio recording API.
* MediaRecorder doesn't work. Compiles against min API 15 and probably even earlier.
*
* Many thanks to Craig Stuart Sapp for his invaluable WAV specification:
* http://soundfile.sapp.org/doc/WaveFormat/
*/
package io.kmark.gist;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Locale;
public class AudioRecordActivity extends AppCompatActivity {
private static final int PERMISSION_RECORD_AUDIO = 0;
private RecordWaveTask recordTask = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_record);
//noinspection ConstantConditions
findViewById(R.id.btnStart).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(AudioRecordActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
// Request permission
ActivityCompat.requestPermissions(AudioRecordActivity.this,
new String[] { Manifest.permission.RECORD_AUDIO },
PERMISSION_RECORD_AUDIO);
return;
}
// Permission already available
launchTask();
}
});
//noinspection ConstantConditions
findViewById(R.id.btnStop).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!recordTask.isCancelled() && recordTask.getStatus() == AsyncTask.Status.RUNNING) {
recordTask.cancel(false);
} else {
Toast.makeText(AudioRecordActivity.this, "Task not running.", Toast.LENGTH_SHORT).show();
}
}
});
// Restore the previous task or create a new one if necessary
recordTask = (RecordWaveTask) getLastCustomNonConfigurationInstance();
if (recordTask == null) {
recordTask = new RecordWaveTask(this);
} else {
recordTask.setContext(this);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSION_RECORD_AUDIO:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
launchTask();
} else {
// Permission denied
Toast.makeText(this, "\uD83D\uDE41", Toast.LENGTH_SHORT).show();
}
break;
}
}
private void launchTask() {
switch (recordTask.getStatus()) {
case RUNNING:
Toast.makeText(this, "Task already running...", Toast.LENGTH_SHORT).show();
return;
case FINISHED:
recordTask = new RecordWaveTask(this);
break;
case PENDING:
if (recordTask.isCancelled()) {
recordTask = new RecordWaveTask(this);
}
}
File wavFile = new File(getFilesDir(), "recording_" + System.currentTimeMillis() / 1000 + ".wav");
Toast.makeText(this, wavFile.getAbsolutePath(), Toast.LENGTH_LONG).show();
recordTask.execute(wavFile);
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
recordTask.setContext(null);
return recordTask;
}
private static class RecordWaveTask extends AsyncTask<File, Void, Object[]> {
// Configure me!
private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
private static final int SAMPLE_RATE = 44100; // Hz
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_MONO;
//
private static final int BUFFER_SIZE = 2 * AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_MASK, ENCODING);
private Context ctx;
private RecordWaveTask(Context ctx) {
setContext(ctx);
}
private void setContext(Context ctx) {
this.ctx = ctx;
}
/**
* Opens up the given file, writes the header, and keeps filling it with raw PCM bytes from
* AudioRecord until it reaches 4GB or is stopped by the user. It then goes back and updates
* the WAV header to include the proper final chunk sizes.
*
* @param files Index 0 should be the file to write to
* @return Either an Exception (error) or two longs, the filesize, elapsed time in ms (success)
*/
@Override
protected Object[] doInBackground(File... files) {
AudioRecord audioRecord = null;
FileOutputStream wavOut = null;
long startTime = 0;
long endTime = 0;
try {
// Open our two resources
audioRecord = new AudioRecord(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_MASK, ENCODING, BUFFER_SIZE);
wavOut = new FileOutputStream(files[0]);
// Write out the wav file header
writeWavHeader(wavOut, CHANNEL_MASK, SAMPLE_RATE, ENCODING);
// Avoiding loop allocations
byte[] buffer = new byte[BUFFER_SIZE];
boolean run = true;
int read;
long total = 0;
// Let's go
startTime = SystemClock.elapsedRealtime();
audioRecord.startRecording();
while (run && !isCancelled()) {
read = audioRecord.read(buffer, 0, buffer.length);
// WAVs cannot be > 4 GB due to the use of 32 bit unsigned integers.
if (total + read > 4294967295L) {
// Write as many bytes as we can before hitting the max size
for (int i = 0; i < read && total <= 4294967295L; i++, total++) {
wavOut.write(buffer[i]);
}
run = false;
} else {
// Write out the entire read buffer
wavOut.write(buffer, 0, read);
total += read;
}
}
} catch (IOException ex) {
return new Object[]{ex};
} finally {
if (audioRecord != null) {
try {
if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop();
endTime = SystemClock.elapsedRealtime();
}
} catch (IllegalStateException ex) {
//
}
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
audioRecord.release();
}
}
if (wavOut != null) {
try {
wavOut.close();
} catch (IOException ex) {
//
}
}
}
try {
// This is not put in the try/catch/finally above since it needs to run
// after we close the FileOutputStream
updateWavHeader(files[0]);
} catch (IOException ex) {
return new Object[] { ex };
}
return new Object[] { files[0].length(), endTime - startTime };
}
/**
* Writes the proper 44-byte RIFF/WAVE header to/for the given stream
* Two size fields are left empty/null since we do not yet know the final stream size
*
* @param out The stream to write the header to
* @param channelMask An AudioFormat.CHANNEL_* mask
* @param sampleRate The sample rate in hertz
* @param encoding An AudioFormat.ENCODING_PCM_* value
* @throws IOException
*/
private static void writeWavHeader(OutputStream out, int channelMask, int sampleRate, int encoding) throws IOException {
short channels;
switch (channelMask) {
case AudioFormat.CHANNEL_IN_MONO:
channels = 1;
break;
case AudioFormat.CHANNEL_IN_STEREO:
channels = 2;
break;
default:
throw new IllegalArgumentException("Unacceptable channel mask");
}
short bitDepth;
switch (encoding) {
case AudioFormat.ENCODING_PCM_8BIT:
bitDepth = 8;
break;
case AudioFormat.ENCODING_PCM_16BIT:
bitDepth = 16;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
bitDepth = 32;
break;
default:
throw new IllegalArgumentException("Unacceptable encoding");
}
writeWavHeader(out, channels, sampleRate, bitDepth);
}
/**
* Writes the proper 44-byte RIFF/WAVE header to/for the given stream
* Two size fields are left empty/null since we do not yet know the final stream size
*
* @param out The stream to write the header to
* @param channels The number of channels
* @param sampleRate The sample rate in hertz
* @param bitDepth The bit depth
* @throws IOException
*/
private static void writeWavHeader(OutputStream out, short channels, int sampleRate, short bitDepth) throws IOException {
// Convert the multi-byte integers to raw bytes in little endian format as required by the spec
byte[] littleBytes = ByteBuffer
.allocate(14)
.order(ByteOrder.LITTLE_ENDIAN)
.putShort(channels)
.putInt(sampleRate)
.putInt(sampleRate * channels * (bitDepth / 8))
.putShort((short) (channels * (bitDepth / 8)))
.putShort(bitDepth)
.array();
// Not necessarily the best, but it's very easy to visualize this way
out.write(new byte[]{
// RIFF header
'R', 'I', 'F', 'F', // ChunkID
0, 0, 0, 0, // ChunkSize (must be updated later)
'W', 'A', 'V', 'E', // Format
// fmt subchunk
'f', 'm', 't', ' ', // Subchunk1ID
16, 0, 0, 0, // Subchunk1Size
1, 0, // AudioFormat
littleBytes[0], littleBytes[1], // NumChannels
littleBytes[2], littleBytes[3], littleBytes[4], littleBytes[5], // SampleRate
littleBytes[6], littleBytes[7], littleBytes[8], littleBytes[9], // ByteRate
littleBytes[10], littleBytes[11], // BlockAlign
littleBytes[12], littleBytes[13], // BitsPerSample
// data subchunk
'd', 'a', 't', 'a', // Subchunk2ID
0, 0, 0, 0, // Subchunk2Size (must be updated later)
});
}
/**
* Updates the given wav file's header to include the final chunk sizes
*
* @param wav The wav file to update
* @throws IOException
*/
private static void updateWavHeader(File wav) throws IOException {
byte[] sizes = ByteBuffer
.allocate(8)
.order(ByteOrder.LITTLE_ENDIAN)
// There are probably a bunch of different/better ways to calculate
// these two given your circumstances. Cast should be safe since if the WAV is
// > 4 GB we've already made a terrible mistake.
.putInt((int) (wav.length() - 8)) // ChunkSize
.putInt((int) (wav.length() - 44)) // Subchunk2Size
.array();
RandomAccessFile accessWave = null;
//noinspection CaughtExceptionImmediatelyRethrown
try {
accessWave = new RandomAccessFile(wav, "rw");
// ChunkSize
accessWave.seek(4);
accessWave.write(sizes, 0, 4);
// Subchunk2Size
accessWave.seek(40);
accessWave.write(sizes, 4, 4);
} catch (IOException ex) {
// Rethrow but we still close accessWave in our finally
throw ex;
} finally {
if (accessWave != null) {
try {
accessWave.close();
} catch (IOException ex) {
//
}
}
}
}
@Override
protected void onCancelled(Object[] results) {
// Handling cancellations and successful runs in the same way
onPostExecute(results);
}
@Override
protected void onPostExecute(Object[] results) {
Throwable throwable = null;
if (results[0] instanceof Throwable) {
// Error
throwable = (Throwable) results[0];
Log.e(RecordWaveTask.class.getSimpleName(), throwable.getMessage(), throwable);
}
// If we're attached to an activity
if (ctx != null) {
if (throwable == null) {
// Display final recording stats
double size = (long) results[0] / 1000000.00;
long time = (long) results[1] / 1000;
Toast.makeText(ctx, String.format(Locale.getDefault(), "%.2f MB / %d seconds",
size, time), Toast.LENGTH_LONG).show();
} else {
// Error
Toast.makeText(ctx, throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
}
}
}
}
@kevinmcampos

This comment has been minimized.

Copy link

@kevinmcampos kevinmcampos commented Jan 7, 2017

Nice piece of code, congrats!

@MihailoJoksimovic

This comment has been minimized.

Copy link

@MihailoJoksimovic MihailoJoksimovic commented Aug 6, 2017

Awesome code, thanks a lot for sharing!!

@firsthisname

This comment has been minimized.

Copy link

@firsthisname firsthisname commented Aug 21, 2017

Good code, thank you. But.
Code will not work when ENCODING = AudioFormat.ENCODING_PCM_FLOAT.

@kotex17

This comment has been minimized.

Copy link

@kotex17 kotex17 commented Oct 5, 2017

So useful! Thanks

@AndroidDeveloperLB

This comment has been minimized.

Copy link

@AndroidDeveloperLB AndroidDeveloperLB commented Jun 27, 2018

Can you please show how to record to audio files using "oboe" library :
https://github.com/google/oboe

@ninjachen

This comment has been minimized.

Copy link

@ninjachen ninjachen commented Jun 28, 2018

Easy to use, you save my day!

@myVanDev

This comment has been minimized.

Copy link

@myVanDev myVanDev commented Aug 14, 2018

Great !
You saved My life :)

@hyeinqq

This comment has been minimized.

Copy link

@hyeinqq hyeinqq commented Aug 22, 2018

Amazing code!!! Thanks a lot sharing!! :)

@hoachen

This comment has been minimized.

Copy link

@hoachen hoachen commented Mar 14, 2019

Good code

@rajyvan

This comment has been minimized.

Copy link

@rajyvan rajyvan commented Apr 3, 2019

Great work !

@callmemaxi

This comment has been minimized.

Copy link

@callmemaxi callmemaxi commented Jun 2, 2019

Thank you

@solarisn

This comment has been minimized.

Copy link

@solarisn solarisn commented Sep 14, 2019

Thank you good sir!

@exutic

This comment has been minimized.

Copy link

@exutic exutic commented Oct 20, 2019

no such directory exception happening - any tip or hint !!!!!

@19Site

This comment has been minimized.

Copy link

@19Site 19Site commented Oct 29, 2019

Nice example! You saved my day!

@Anushree278

This comment has been minimized.

Copy link

@Anushree278 Anushree278 commented Jan 15, 2020

Can anyone tell me how to play audio which we have recorded as when i am trying to play it with media player, it is showing unknown error.Please help

@ciromelody

This comment has been minimized.

Copy link

@ciromelody ciromelody commented Apr 18, 2020

private void launchTask() {
    switch (recordTask.getStatus()) {
        case RUNNING:
            Toast.makeText(this, "Task already running...", Toast.LENGTH_SHORT).show();
            return;
        case FINISHED:
            recordTask = new RecordWaveTask(this);
            break;
        case PENDING:
            if (recordTask.isCancelled()) {
                recordTask = new RecordWaveTask(this);
            }
    }
    File wavFile = generaFile();
   // File wavFile = new File(getFilesDir(), "recording_" + System.currentTimeMillis() / 1000 + ".wav");
    Toast.makeText(this, wavFile.getAbsolutePath(), Toast.LENGTH_LONG).show();
    recordTask.execute(wavFile);
}

private File generaFile() {

    File directory = new File(getApplicationContext().getExternalFilesDir(null).getAbsolutePath(), "recordAudio") ;
    if(!directory.exists())
    {
        directory.mkdir();
        if(!directory.exists())
        {
            Log.e("RECORD","WARNING! Directory does not exists !!!! Creation problems");
        }
    }
    String filename = "recording_" + System.currentTimeMillis() / 1000 + ".wav";
    File newFile = new File(directory, filename);
    if(!newFile.exists()){
        try {
            newFile.createNewFile();
        } catch (IOException e) {
            Log.e("RECORD",e.getMessage());
        }
    }
    return newFile;
}
@ciromelody

This comment has been minimized.

Copy link

@ciromelody ciromelody commented Apr 18, 2020

This work for me huawey p8 lite 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.