Android Getting Started tutorial | Audio

Collect and play audio PCM data, read and write audio wav files

Use AudioRecord and AudioTrack to collect and play audio PCM data, and read and write audio wav files

preparation
Android provides AudioRecord and MediaRecord. MediaRecord to select the recording format. AudioRecord obtains data in PCM coding format. AudioRecord can set relevant parameters for converting analog signals into digital signals, including sampling rate and quantization depth, as well as the number of channels.

PCM

PCM is a common coding format in the transformation from analog signal to digital signal, which is called pulse coding modulation. PCM divides the analog signal into multiple segments according to a certain spacing, and then quantizes the strength of each spacing through binary. PCM represents the amplitude of a piece of audio in an audio file over time. Android supports PCM audio data in WAV files.

WAV

WAV, MP3 and other common audio formats. Different coding formats correspond to the original audio that does not pass. In order to facilitate transmission, the original audio is usually compressed. In order to identify the audio format, each format has a specific header. Wav takes RIFF as the standard. RIFF is a resource exchange file standard. RIFF stores the file in each tag block. The basic unit is trunk. Each trunk consists of tag bit, data size and data storage.

PCM packaged as WAV

pcm is the original audio data. WAV is a common audio format in windows. Only a file header is added to pcm data.

Start addressOccupied spaceMeaning of this address number
00H4byteRIFF, resource exchange file flag.
04H4byteThe total number of bytes from the next address to the end of the file. The high-order byte is in the back, here is 001437ECH, which is 1325036byte in decimal system, and the previous 8byte is exactly 1325044byte.
08H4byteWAVE stands for the wav e file format.
0CH4byteFMT, waveform format flag
10H4byte00000010H, 16PCM, my understanding is to use 16bit data to represent a quantization result.
14H2byteWhen it is 1, it indicates linear PCM coding, and when it is greater than 1, it indicates compressed coding. This is 0001H.
16H2byte1 is mono, 2 is dual, here is 0001H.
18H4byteThe sampling frequency here is 00002B11H, i.e. 11025Hz.
1CH4byteByte rate = sampling frequency * number of audio channels * number of samples per sampling / 800005622H, that is, 22050Byte/s=11025 * 1 * 16/2
20H2byteBlock alignment = number of channels * number of samples per sampling / 80002H, that is, 2 = = 1 * 16 / 8
22H2byteThe number of sample data bits is 0010H, i.e. 16, and one quantized sample accounts for 2byte.
24H4bytedata, just a sign.
28H4byteThe size of the actual audio data of the Wav file is 001437C8H, that is, 1325000. Plus 2CH, it is exactly 1325044. The size of the whole file.
2CHUncertainQuantitative data

AudioRecord

AudioRecord enables recording sound from an audio input device. Get audio in PCM format. The methods of reading audio include read(byte[], int, int), read(short[], int, int) or read (byte buffer, int). This method can be selected according to storage mode and demand.

Permission required < uses permission Android: name = "Android. Permission. Record_audio" / >

AudioRecord constructor

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
  • audioSource sound source device, common microphone, mediarecorder audioSource. MIC
  • samplerateInHz sampling frequency, 44100Hz is the frequency currently supported by all devices
  • channelConfig audio channel, mono or stereo
  • audioFormat this parameter is the quantization depth, that is, the number of bits per sample
  • bufferSizeInBytes can determine the size of the buffer required to read data from the hardware each time through the getMinBufferSize() method.

Get wav file

To obtain WAV files, you need to add a header on the basis of PCM. PCM files can be converted into wav. Here is an idea that PCM and wav are generated almost at the same time.

PCM and wav are created at the same time, and a default header is given to the wav file. After the recording thread starts, write PCM and wav at the same time. When the recording is completed, regenerate the header and modify the header of the wav file using RandomAccessFile.

AudioTrack

Use AudioTrack to play audio. When initializing AudioTrack, it should be set according to the parameters during recording.

Code example

The tool class WindEar realizes the collection and playback of audio PCM data, and the functions of reading and writing audio wav files.

  • AudioRecordThread uses AudioRecord to record PCM files, optionally generating wav files at the same time
  • AudioTrackPlayThread a thread that plays PCM or wav audio files using AudioTrack
  • WindState indicates the current state, such as whether it is playing, recording, etc

FileOutputStream and FileInputStream are used for reading and writing PCM files

The generateWavFileHeader method can generate the header of the wav file

/**
 * Audio recorder
 * Use AudioRecord and AudioTrack API to collect and play audio PCM data, and read and write audio wav files
 * Check permissions. Check the microphone and put it in the Activity
 */
public class WindEar {
 private static final String TAG = "rustApp";
 private static final String TMP_FOLDER_NAME = "AnWindEar";
 private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
 private static final int PLAY_AUDIO_BUFFER_TIMES = 1;
 private static final int AUDIO_FREQUENCY = 44100;
​
 private static final int RECORD_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
 private static final int PLAY_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO;
 private static final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
​
 private AudioRecordThread aRecordThread;           // Recording thread
 private volatile WindState state = WindState.IDLE; // current state
 private File tmpPCMFile = null;
 private File tmpWavFile = null;
 private OnState onStateListener;
 private Handler mainHandler = new Handler(Looper.getMainLooper());
​
 /**
 * PCM Cache directory
 */
 private static String cachePCMFolder;
​
 /**
 * wav Cache directory
 */
 private static String wavFolderPath;
​
 private static WindEar instance = new WindEar();
​
 private WindEar() {
​
 }
​
 public static WindEar getInstance() {
 if (null == instance) {
 instance = new WindEar();
 }
 return instance;
 }
​
 public void setOnStateListener(OnState onStateListener) {
 this.onStateListener = onStateListener;
 }
​
 /**
 * initial directory
 */
 public static void init(Context context) {
 // Stored in App or on SD card
//        cachePCMFolder = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
 cachePCMFolder = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
 + TMP_FOLDER_NAME;
​
 File folder = new File(cachePCMFolder);
 if (!folder.exists()) {
 boolean f = folder.mkdirs();
 Log.d(TAG, String.format(Locale.CHINA, "PCM catalogue:%s -> %b", cachePCMFolder, f));
 } else {
 for (File f : folder.listFiles()) {
 boolean d = f.delete();
 Log.d(TAG, String.format(Locale.CHINA, "delete PCM file:%s %b", f.getName(), d));
 }
 Log.d(TAG, String.format(Locale.CHINA, "PCM catalogue:%s", cachePCMFolder));
 }
​
 wavFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
 + TMP_FOLDER_NAME;
//        wavFolderPath = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
 File wavDir = new File(wavFolderPath);
 if (!wavDir.exists()) {
 boolean w = wavDir.mkdirs();
 Log.d(TAG, String.format(Locale.CHINA, "wav catalogue:%s -> %b", wavFolderPath, w));
 } else {
 Log.d(TAG, String.format(Locale.CHINA, "wav catalogue:%s", wavFolderPath));
 }
 }
​
 /**
 * Start recording audio
 */
 public synchronized void startRecord(boolean createWav) {
 if (!state.equals(WindState.IDLE)) {
 Log.w(TAG, "Unable to start recording, current status is " + state);
 return;
 }
 try {
 tmpPCMFile = File.createTempFile("recording", ".pcm", new File(cachePCMFolder));
 if (createWav) {
 SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd_HHmmss", Locale.CHINA);
 tmpWavFile = new File(wavFolderPath + File.separator + "r" + sdf.format(new Date()) + ".wav");
 }
 Log.d(TAG, "tmp file " + tmpPCMFile.getName());
 } catch (IOException e) {
 e.printStackTrace();
 }
 if (null != aRecordThread) {
 aRecordThread.interrupt();
 aRecordThread = null;
 }
 aRecordThread = new AudioRecordThread(createWav);
 aRecordThread.start();
 }
​
 public synchronized void stopRecord() {
 if (!state.equals(WindState.RECORDING)) {
 return;
 }
 state = WindState.STOP_RECORD;
 notifyState(state);
 }
​
 /**
 * Play the recorded PCM file
 */
 public synchronized void startPlayPCM() {
 if (!isIdle()) {
 return;
 }
 new AudioTrackPlayThread(tmpPCMFile).start();
 }
​
 /**
 * Play the recorded wav file
 */
 public synchronized void startPlayWav() {
 if (!isIdle()) {
 return;
 }
 new AudioTrackPlayThread(tmpWavFile).start();
 }
​
 public synchronized void stopPlay() {
 if (!state.equals(WindState.PLAYING)) {
 return;
 }
 state = WindState.STOP_PLAY;
 }
​
 public synchronized boolean isIdle() {
 return WindState.IDLE.equals(state);
 }
​
 /**
 * Audio recording thread
 * Use FileOutputStream to write files
 */
 private class AudioRecordThread extends Thread {
 AudioRecord aRecord;
 int bufferSize = 10240;
 boolean createWav = false;
​
 AudioRecordThread(boolean createWav) {
 this.createWav = createWav;
 bufferSize = AudioRecord.getMinBufferSize(AUDIO_FREQUENCY,
 RECORD_CHANNEL_CONFIG, AUDIO_ENCODING) * RECORD_AUDIO_BUFFER_TIMES;
 Log.d(TAG, "record buffer size = " + bufferSize);
 aRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, AUDIO_FREQUENCY,
 RECORD_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize);
 }
​
 @Override
 public void run() {
 state = WindState.RECORDING;
 notifyState(state);
 Log.d(TAG, "Recording starts");
 try {
 // Select FileOutputStream instead of DataOutputStream here
 FileOutputStream pcmFos = new FileOutputStream(tmpPCMFile);
​
 FileOutputStream wavFos = new FileOutputStream(tmpWavFile);
 if (createWav) {
 writeWavFileHeader(wavFos, bufferSize, AUDIO_FREQUENCY, aRecord.getChannelCount());
 }
 aRecord.startRecording();
 byte[] byteBuffer = new byte[bufferSize];
 while (state.equals(WindState.RECORDING) && !isInterrupted()) {
 int end = aRecord.read(byteBuffer, 0, byteBuffer.length);
 pcmFos.write(byteBuffer, 0, end);
 pcmFos.flush();
 if (createWav) {
 wavFos.write(byteBuffer, 0, end);
 wavFos.flush();
 }
 }
 aRecord.stop(); // End of recording
 pcmFos.close();
 wavFos.close();
 if (createWav) {
 // Modify header
 RandomAccessFile wavRaf = new RandomAccessFile(tmpWavFile, "rw");
 byte[] header = generateWavFileHeader(tmpPCMFile.length(), AUDIO_FREQUENCY, aRecord.getChannelCount());
 Log.d(TAG, "header: " + getHexString(header));
 wavRaf.seek(0);
 wavRaf.write(header);
 wavRaf.close();
 Log.d(TAG, "tmpWavFile.length: " + tmpWavFile.length());
 }
 Log.i(TAG, "audio tmp PCM file len: " + tmpPCMFile.length());
 } catch (Exception e) {
 Log.e(TAG, "AudioRecordThread:", e);
 notifyState(WindState.ERROR);
 }
 notifyState(state);
 state = WindState.IDLE;
 notifyState(state);
 Log.d(TAG, "End of recording");
 }
​
 }
​
 private static String getHexString(byte[] bytes) {
 StringBuilder sb = new StringBuilder();
 for (byte b : bytes) {
 sb.append(Integer.toHexString(b)).append(",");
 }
 return sb.toString();
 }
​
 /**
 * AudioTrack Play audio thread
 * Reading files using FileInputStream
 */
 private class AudioTrackPlayThread extends Thread {
 AudioTrack track;
 int bufferSize = 10240;
 File audioFile = null;
​
 AudioTrackPlayThread(File aFile) {
 setPriority(Thread.MAX_PRIORITY);
 audioFile = aFile;
 int bufferSize = AudioTrack.getMinBufferSize(AUDIO_FREQUENCY,
 PLAY_CHANNEL_CONFIG, AUDIO_ENCODING) * PLAY_AUDIO_BUFFER_TIMES;
 track = new AudioTrack(AudioManager.STREAM_MUSIC,
 AUDIO_FREQUENCY,
 PLAY_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize,
 AudioTrack.MODE_STREAM);
 }
​
 @Override
 public void run() {
 super.run();
 state = WindState.PLAYING;
 notifyState(state);
 try {
 FileInputStream fis = new FileInputStream(audioFile);
 track.play();
 byte[] aByteBuffer = new byte[bufferSize];
 while (state.equals(WindState.PLAYING) &&
 fis.read(aByteBuffer) >= 0) {
 track.write(aByteBuffer, 0, aByteBuffer.length);
 }
 track.stop();
 track.release();
 } catch (Exception e) {
 Log.e(TAG, "AudioTrackPlayThread:", e);
 notifyState(WindState.ERROR);
 }
 state = WindState.STOP_PLAY;
 notifyState(state);
 state = WindState.IDLE;
 notifyState(state);
 }
​
 }
​
 private synchronized void notifyState(final WindState currentState) {
 if (null != onStateListener) {
 mainHandler.post(new Runnable() {
 @Override
 public void run() {
 onStateListener.onStateChanged(currentState);
 }
 });
 }
 }
​
 public interface OnState {
 void onStateChanged(WindState currentState);
 }
​
 /**
 * Indicates the current status
 */
 public enum WindState {
 ERROR,
 IDLE,
 RECORDING,
 STOP_RECORD,
 PLAYING,
 STOP_PLAY
 }
​
 /**
 * @param out            wav Audio file stream
 * @param totalAudioLen  Total length of audio data excluding header
 * @param longSampleRate Sample rate, that is, the frequency used when recording
 * @param channels       audioRecord Number of channels
 * @throws IOException Write file error
 */
 private void writeWavFileHeader(FileOutputStream out, long totalAudioLen, long longSampleRate,
 int channels) throws IOException {
 byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels);
 out.write(header, 0, header.length);
 }
​
 /**
 * The format of any kind of file can only be determined by adding the corresponding header file to the header,
 * wave It is a RIFF file structure. Each part is a chunk, including RIFF WAVE chunk,
 * FMT Chunk,Fact chunk,Data chunk,The Fact chunk is optional
 *
 * @param pcmAudioByteCount Total length of audio data excluding header
 * @param longSampleRate    Sample rate, that is, the frequency used when recording
 * @param channels          audioRecord Number of channels
 */
 private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
 long totalDataLen = pcmAudioByteCount + 36; // Total length of WAV files that do not contain the first 8 bytes
 long byteRate = longSampleRate * 2 * channels;
 byte[] header = new byte[44];
 header[0] = 'R'; // RIFF
 header[1] = 'I';
 header[2] = 'F';
 header[3] = 'F';
​
 header[4] = (byte) (totalDataLen & 0xff);//data size
 header[5] = (byte) ((totalDataLen >> 8) & 0xff);
 header[6] = (byte) ((totalDataLen >> 16) & 0xff);
 header[7] = (byte) ((totalDataLen >> 24) & 0xff);
​
 header[8] = 'W';//WAVE
 header[9] = 'A';
 header[10] = 'V';
 header[11] = 'E';
 //FMT Chunk
 header[12] = 'f'; // 'fmt '
 header[13] = 'm';
 header[14] = 't';
 header[15] = ' ';//Transition byte
 //data size
 header[16] = 16; // 4 bytes: size of 'fmt ' chunk
 header[17] = 0;
 header[18] = 0;
 header[19] = 0;
 //Coding mode 10H is PCM coding format
 header[20] = 1; // format = 1
 header[21] = 0;
 //Number of channels
 header[22] = (byte) channels;
 header[23] = 0;
 //Sampling rate, playback speed of each channel
 header[24] = (byte) (longSampleRate & 0xff);
 header[25] = (byte) ((longSampleRate >> 8) & 0xff);
 header[26] = (byte) ((longSampleRate >> 16) & 0xff);
 header[27] = (byte) ((longSampleRate >> 24) & 0xff);
 //Audio data transmission rate, sampling rate * number of channels * sampling depth / 8
 header[28] = (byte) (byteRate & 0xff);
 header[29] = (byte) ((byteRate >> 8) & 0xff);
 header[30] = (byte) ((byteRate >> 16) & 0xff);
 header[31] = (byte) ((byteRate >> 24) & 0xff);
 // Determine how many such bytes of data the system processes at a time, and determine the buffer, number of channels * sampling bits
 header[32] = (byte) (2 * channels);
 header[33] = 0;
 //Number of data bits per sample
 header[34] = 16;
 header[35] = 0;
 //Data chunk
 header[36] = 'd';//data
 header[37] = 'a';
 header[38] = 't';
 header[39] = 'a';
 header[40] = (byte) (pcmAudioByteCount & 0xff);
 header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
 header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
 header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
 return header;
 }
}

[Android audio and video development series tutorials]

Keywords: Android

Added by pneudralics on Tue, 14 Dec 2021 10:22:13 +0200