Android Recording with AudioRecord

  • 2021-12-11 08:46:40
  • OfStack

In the development of audio and video, recording is of course essential. First of all, we should learn the separate recording function. Of course, the recording here refers to recording with AudioRecord, reading the original recording data, and reading the so-called PCM data. For recording, the most important parameters should be understood:

1, simpleRate sampling rate, sampling rate is the sampling frequency, how many samples are recorded per second.

2. channelConfig channel configuration is actually the so-called single channel and dual channel, such as AudioFormat.CHANNEL_IN_MONO single channel and AudioFormat.CHANNEL_IN_STEREO dual channel. Only these two are listed here, and others can be consulted by yourself.

3. audioFormat audio format is actually the sampling accuracy and the number of bits per sample. AudioFormat.ENCODING_PCM_8BIT accounts for 8 bits per sample, and AudioFormat.ENCODING_PCM_16BIT accounts for 16 bits per sample. These two are only used here, and nothing else is studied.

I encapsulate a class here for 1 parameter that will be used in the learning process, as follows


public class AudioParams {

  enum Format {
    SINGLE_8_BIT, DOUBLE_8_BIT, SINGLE_16_BIT, DOUBLE_16_BIT
  }

  private Format format;
  int simpleRate;

  AudioParams(int simpleRate, Format f) {
    this.simpleRate = simpleRate;
    this.format = f;
  }

  AudioParams(int simpleRate, int channelCount, int bits) {
    this.simpleRate = simpleRate;
    set(channelCount, bits);
  }

  int getBits() {
    return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ? 8 : 16;
  }

  int getEncodingFormat() {
    return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ? AudioFormat.ENCODING_PCM_8BIT :
      AudioFormat.ENCODING_PCM_16BIT;
  }

  int getChannelCount() {return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? 1 : 2;}

  int getChannelConfig() {
    return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? AudioFormat.CHANNEL_IN_MONO :
      AudioFormat.CHANNEL_IN_STEREO;
  }

  int getOutChannelConfig() {
    return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? AudioFormat.CHANNEL_OUT_MONO :
      AudioFormat.CHANNEL_OUT_STEREO;
  }

  void set(int channelCount, int bits) {
    if ((channelCount != 1 && channelCount != 2) || (bits != 8 && bits != 16)) {
      throw new IllegalArgumentException(" Other formats are not supported  channelCount=$channelCount bits=$bits");
    }
    if (channelCount == 1) {
      if (bits == 8) {
        format = Format.SINGLE_8_BIT;
      } else {
        format = Format.SINGLE_16_BIT;
      }
    } else {
      if (bits == 8) {
        format = Format.DOUBLE_8_BIT;
      } else {
        format = Format.DOUBLE_16_BIT;
      }
    }
  }
}

Here, single channel 8-bit, dual channel 8-bit, single channel 16-bit and dual channel 16-bit are fixed, so enumeration is used to limit them.

In order to conveniently display and store the recorded data, a callback method is written here as follows


public interface RecordCallback {
    /**
     *  Data callback 
     *
     * @param bytes  Data 
     * @param len   Effective length of data, -1 The time indicates the end of the data 
     */
    void onRecord(byte[] bytes, int len);
  }

With these parameters, you can record now. Look at the following example first


public void startRecord(AudioParams params, RecordCallback callback) {
    int simpleRate = params.simpleRate;
    int channelConfig = params.getChannelConfig();
    int audioFormat = params.getEncodingFormat();
    //  According to AudioRecord Offered api Get the minimum cache size 
    int bufferSize = AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);
    // Create Record Object 
    record = new AudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
    recordThread = new Thread(() -> {
      byte[] buffer = new byte[bufferSize];
      record.startRecording();
      recording = true;
      while (recording) {
        int read = record.read(buffer, 0, bufferSize);
        //  Callback data to the outside 
        if (read > 0 && callback != null) {
          callback.onRecord(buffer, read);
        }
      }
      if (callback != null) {
        // len  For -1 When means the end 
        callback.onRecord(buffer, -1);
        recording = false;
      }
      // Release resources 
      release();
    });
    recordThread.start();
  }

This method is simply to collect audio data, which is the most original pcm data.

After getting pcm data, if it is saved directly to a file, it cannot be played directly, because it is only a pile of data without any format description. If you want an ordinary player to play it, you need to add a file header to the file to tell the player the format of this data. Here is the data directly saved to wav format. The following is how to add wav format file headers


private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, int bits) {
    byte[] header = new byte[44];
    // RIFF/WAVE header
    header[0] = 'R';
    header[1] = 'I';
    header[2] = 'F';
    header[3] = 'F';

    int fileLength = totalDataLen + 36;
    header[4] = (byte) (fileLength & 0xff);
    header[5] = (byte) (fileLength >> 8 & 0xff);
    header[6] = (byte) (fileLength >> 16 & 0xff);
    header[7] = (byte) (fileLength >> 24 & 0xff);
    //WAVE
    header[8] = 'W';
    header[9] = 'A';
    header[10] = 'V';
    header[11] = 'E';
    // 'fmt ' chunk
    header[12] = 'f';
    header[13] = 'm';
    header[14] = 't';
    header[15] = ' ';
    // 4 bytes: size of 'fmt ' chunk
    header[16] = 16;
    header[17] = 0;
    header[18] = 0;
    header[19] = 0;

    // pcm format = 1
    header[20] = 1;
    header[21] = 0;
    header[22] = (byte) channelCount;
    header[23] = 0;

    header[24] = (byte) (sampleRate & 0xff);
    header[25] = (byte) (sampleRate >> 8 & 0xff);
    header[26] = (byte) (sampleRate >> 16 & 0xff);
    header[27] = (byte) (sampleRate >> 24 & 0xff);

    int byteRate = sampleRate * bits * channelCount / 8;
    header[28] = (byte) (byteRate & 0xff);
    header[29] = (byte) (byteRate >> 8 & 0xff);
    header[30] = (byte) (byteRate >> 16 & 0xff);
    header[31] = (byte) (byteRate >> 24 & 0xff);
    // block align
    header[32] = (byte) (channelCount * bits / 8);
    header[33] = 0;
    // bits per sample
    header[34] = (byte) bits;
    header[35] = 0;
    //data
    header[36] = 'd';
    header[37] = 'a';
    header[38] = 't';
    header[39] = 'a';
    header[40] = (byte) (totalDataLen & 0xff);
    header[41] = (byte) (totalDataLen >> 8 & 0xff);
    header[42] = (byte) (totalDataLen >> 16 & 0xff);
    header[43] = (byte) (totalDataLen >> 24 & 0xff);
    return header;
  }

Set the file header under 1 according to several parameters, and then write the pcm data collected by recording directly, which can be played normally. wav file header format definition, you can click here to view or Baidu.

If you want to save directly to a file through AudioRecord recording, you can refer to the following method


public void startRecord(String filePath, AudioParams params, RecordCallback callback) {
    int channelCount = params.getChannelCount();
    int bits = params.getBits();

    final boolean storeFile = filePath != null && !filePath.isEmpty();

    startRecord(params, (bytes, len) -> {
      if (storeFile) {
        if (file == null) {
          File f = new File(filePath);
          if (f.exists()) {
            f.delete();
          }
          try {
            file = new RandomAccessFile(f, "rw");
            file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
        if (len > 0) {
          try {
            file.write(bytes, 0, len);
          } catch (IOException e) {
            e.printStackTrace();
          }
        } else {
          try {
            //  Because the header information has been written before, it is the length of the data to subtract the header information here 
            int length = (int) file.length() - 44;
            file.seek(0);
            file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
            file.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
      if (callback != null) {
        callback.onRecord(bytes, len);
      }
    });
  }

First, create a file through RandomAccessFile, and write the file header first. For the time being, we don't know how long it will be recorded and how many pcm data, and the length will be represented by 0 first. After recording, rewrite the file header information through seek (int) method, or save pcm data to a temporary file first, and then write it to a new file, so there is no example here.

Finally, put in the code of the complete class


package cn.sskbskdrin.record.audio;

import android.media.AudioRecord;
import android.media.MediaRecorder;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * @author sskbskdrin
 * @date 2019/April/3
 */
public class AudioRecordManager {

  private AudioParams DEFAULT_FORMAT = new AudioParams(8000, 1, 16);

  private AudioRecord record;

  private Thread recordThread;

  private boolean recording = false;

  private RandomAccessFile file;

  public void startRecord(String filePath, RecordCallback callback) {
    startRecord(filePath, DEFAULT_FORMAT, callback);
  }

  public void startRecord(String filePath, AudioParams params, RecordCallback callback) {
    int channelCount = params.getChannelCount();
    int bits = params.getBits();

    final boolean storeFile = filePath != null && !filePath.isEmpty();

    startRecord(params, (bytes, len) -> {
      if (storeFile) {
        if (file == null) {
          File f = new File(filePath);
          if (f.exists()) {
            f.delete();
          }
          try {
            file = new RandomAccessFile(f, "rw");
            file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
        if (len > 0) {
          try {
            file.write(bytes, 0, len);
          } catch (IOException e) {
            e.printStackTrace();
          }
        } else {
          try {
            //  Because the header information has been written before, it is the length of the data to subtract the header information here 
            int length = (int) file.length() - 44;
            file.seek(0);
            file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
            file.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
      if (callback != null) {
        callback.onRecord(bytes, len);
      }
    });
  }

  public void startRecord(AudioParams params, RecordCallback callback) {
    int simpleRate = params.simpleRate;
    int channelConfig = params.getChannelConfig();
    int audioFormat = params.getEncodingFormat();
    //  According to AudioRecord Offered api Get the minimum cache size 
    int bufferSize = AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);
    // Create Record Object 
    record = new AudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
    recordThread = new Thread(() -> {
      byte[] buffer = new byte[bufferSize];
      record.startRecording();
      recording = true;
      while (recording) {
        int read = record.read(buffer, 0, bufferSize);
        //  Callback data to the outside 
        if (read > 0 && callback != null) {
          callback.onRecord(buffer, read);
        }
      }
      if (callback != null) {
        // len  For -1 When means the end 
        callback.onRecord(buffer, -1);
        recording = false;
      }
      // Release resources 
      release();
    });
    recordThread.start();
  }

  public void stop() {
    recording = false;
  }

  public void release() {
    recording = false;
    if (record != null) {
      record.stop();
      record.release();
    }
    record = null;
    file = null;
    recordThread = null;
  }

  private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, int bits) {
    byte[] header = new byte[44];
    // RIFF/WAVE header
    header[0] = 'R';
    header[1] = 'I';
    header[2] = 'F';
    header[3] = 'F';

    int fileLength = totalDataLen + 36;
    header[4] = (byte) (fileLength & 0xff);
    header[5] = (byte) (fileLength >> 8 & 0xff);
    header[6] = (byte) (fileLength >> 16 & 0xff);
    header[7] = (byte) (fileLength >> 24 & 0xff);
    //WAVE
    header[8] = 'W';
    header[9] = 'A';
    header[10] = 'V';
    header[11] = 'E';
    // 'fmt ' chunk
    header[12] = 'f';
    header[13] = 'm';
    header[14] = 't';
    header[15] = ' ';
    // 4 bytes: size of 'fmt ' chunk
    header[16] = 16;
    header[17] = 0;
    header[18] = 0;
    header[19] = 0;

    // pcm format = 1
    header[20] = 1;
    header[21] = 0;
    header[22] = (byte) channelCount;
    header[23] = 0;

    header[24] = (byte) (sampleRate & 0xff);
    header[25] = (byte) (sampleRate >> 8 & 0xff);
    header[26] = (byte) (sampleRate >> 16 & 0xff);
    header[27] = (byte) (sampleRate >> 24 & 0xff);

    int byteRate = sampleRate * bits * channelCount / 8;
    header[28] = (byte) (byteRate & 0xff);
    header[29] = (byte) (byteRate >> 8 & 0xff);
    header[30] = (byte) (byteRate >> 16 & 0xff);
    header[31] = (byte) (byteRate >> 24 & 0xff);
    // block align
    header[32] = (byte) (channelCount * bits / 8);
    header[33] = 0;
    // bits per sample
    header[34] = (byte) bits;
    header[35] = 0;
    //data
    header[36] = 'd';
    header[37] = 'a';
    header[38] = 't';
    header[39] = 'a';
    header[40] = (byte) (totalDataLen & 0xff);
    header[41] = (byte) (totalDataLen >> 8 & 0xff);
    header[42] = (byte) (totalDataLen >> 16 & 0xff);
    header[43] = (byte) (totalDataLen >> 24 & 0xff);
    return header;
  }

  public interface RecordCallback {
    /**
     *  Data callback 
     *
     * @param bytes  Data 
     * @param len   Effective length of data, -1 The time indicates the end of the data 
     */
    void onRecord(byte[] bytes, int len);
  }
}

If there is anything wrong, please comment and correct it

These are the details of Android recording with AudioRecord. For more information about Android AudioRecord, please pay attention to other related articles on this site!


Related articles: