javaandroidaudioencodingaudiorecord

Record Raw Audio Bytes to Local Variable in Android


I'm recording audio as raw bytes on Android into a variable that can be played by an instance of AudioTrack.

That is, NOT as a file.

I've chosen 8-bit mono PCM with a sample rate of 44100 because this audio data should be uncompressed... and in a predictable, platform-agnostic format.

Don't worry... we're only dealing with small audio snippets here... and besides, people these days have so much RAM that it megahurtz.

I have tried the approach below... and although it apparently works, the pertinent section, writeAudioToLocalVariable(), was a modified cut n' paste from the internet and I don't really understand the contents enough.

Considering we can't predict the hardware capabilities of the end user's device, can someone more knowledgable tell me if this is the most stable and dependable way to do things inside the writeAudioToLocalVariable() function?

Here's what I've got:

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.io.ByteArrayOutputStream;

public class MainActivity extends AppCompatActivity {
    private static final int NOT_USED = 999999999;
    private static final int SAMPLERATE = 44100;
    private static final int REC_CHANNELS = AudioFormat.CHANNEL_IN_MONO;
    private static final int PLAYBACK_CHANNELS = AudioFormat.CHANNEL_OUT_MONO;
    private static final int ENCODING = AudioFormat.ENCODING_PCM_8BIT;
    // !!! needs to be large enough for all devices
    private static final int MY_CHOSEN_BUFFER_SIZE = 8192;
    private AudioRecord recorder = null;
    private Thread recordingThread = null;
    boolean isRecording = false;

    byte[] recordedAudioAsBytes; // <------ this is where recorded audio ends up

    AudioTrack player;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int minBufferSize = AudioRecord.getMinBufferSize(SAMPLERATE,
                REC_CHANNELS, ENCODING);
        Log.i("ABC", "FYI, min buffer size for this device is : " + minBufferSize);
    }

    private void startRecording() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, NOT_USED);
            Log.i("ABC", "permission fail, returning.");
            return;
        }
        recorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
                SAMPLERATE, REC_CHANNELS,
                ENCODING, MY_CHOSEN_BUFFER_SIZE);
        recorder.startRecording();
        isRecording = true;
        recordingThread = new Thread(new Runnable() {
            public void run() {
                Log.i("ABC", "the thread is running");
                writeAudioToLocalVariable();
            }
        }, "AudioRecorder Thread");
        recordingThread.start();
    }

    private void writeAudioToLocalVariable() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] temporaryChunkOfBytes = new byte[MY_CHOSEN_BUFFER_SIZE];
        while (isRecording) {
            recorder.read(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE);
            try {
                System.out.println("Appending to baos : " + temporaryChunkOfBytes);
                //printBytes(temporaryChunkOfBytes);
                baos.write(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE);
            } catch (Exception e) {
                Log.i("ABC", "Exception while appending bytes : " + e); // <----- this is not called and that is good.
            }
        }
        recordedAudioAsBytes = baos.toByteArray();
    }

    private void stopRecording() {
        // stops the recording activity
        if (null != recorder) {
            isRecording = false;
            recorder.stop();
            recorder.release();
            recorder = null;
            recordingThread = null;
        }
    }

    public void recordButtonClicked(View v) {
        isRecording = true;
        startRecording();
    }

    public void stopButtonClicked(View v) {
        isRecording = false;
        stopRecording();
    }

    public void playButtonPressed(View v) {

        // this verifies that audio data exists as expected
        for (int i=0; i<recordedAudioAsBytes.length; i++) {
            Log.i("ABC", "byte[" + i + "] = " + recordedAudioAsBytes[i]);
        }

        // STREAM MODE ACTUALLY WORKS!! (STATIC MODE DOES NOT WORK)
        AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLERATE, PLAYBACK_CHANNELS,
                ENCODING, MY_CHOSEN_BUFFER_SIZE, AudioTrack.MODE_STREAM);
        player.play();
        player.write(recordedAudioAsBytes, 0, recordedAudioAsBytes.length);

    }

}

Thankyou... from me and so many others in the future of plentiful RAM!


Solution

  • The idea of the implementation is correct, it has some bugs though. A ByteArrayOutputStream is an output stream that stores the memory as bytes that can be grabbed later, and takes care of growing arrays as necessary. You can assume that worst case it will take 2*sound_data_size in terms of storage to do so (it may need to make a copy at some point).

    All the loop is doing is reading 8K at a time from the audio data and writing it to the baos in a loop until its done. I do see a few bugs though- it can write extra data on the last write (read can return less than a full buffer). It will loop rapidly if the stream closes before isRecording is done (read will return -1 instantly), and will probably send you OOM in that case. It can lose data near the end (if the loop variable goes false before the data is read). Try this instead:

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] temporaryChunkOfBytes = new byte[MY_CHOSEN_BUFFER_SIZE];
        int read = 0;
        while((read = recorder.read(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE) >= 0){
            try {
                System.out.println("Appending to baos : " + temporaryChunkOfBytes);
                //printBytes(temporaryChunkOfBytes);
                baos.write(temporaryChunkOfBytes, 0, read);
            } catch (Exception e) {
                Log.i("ABC", "Exception while appending bytes : " + e); // <----- this is not called and that is good.
            }
        }
        recordedAudioAsBytes = baos.toByteArray();
    

    With this version, you shouldn't just have the thread always running. Launch the thread when the recording is started. This fixes all 3 bugs above- if -1 is returned by read, it breaks the loop. If the amount read is less than a buffer, it writes only the amount read. And it reads until the file is closed, not until a variable is set to false so data in the buffer but not read will be processed.

    You will have some memory concerns eventually. Basically you'll take 44K of memory per second. If you're recording for a few seconds, no problem. If you're recording for a few minutes, you'll need to write it to a file instead and then stream in the file when processing it.