-
-
Save kmark/d8b1b01fb0d2febf5770 to your computer and use it in GitHub Desktop.
/* | |
* 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(); | |
} | |
} | |
} | |
} | |
} |
Awesome code, thanks a lot for sharing!!
Good code, thank you. But.
Code will not work when ENCODING = AudioFormat.ENCODING_PCM_FLOAT.
So useful! Thanks
Can you please show how to record to audio files using "oboe" library :
https://github.com/google/oboe
Easy to use, you save my day!
Great !
You saved My life :)
Amazing code!!! Thanks a lot sharing!! :)
Good code
Great work !
Thank you
Thank you good sir!
no such directory exception happening - any tip or hint !!!!!
Nice example! You saved my day!
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
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;
}
This work for me huawey p8 lite 2017
Thank you for this very useful piece of code. Using it in my Android transcoder library: linkedin/LiTr#164
Nice piece of code, congrats!