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 address | Occupied space | Meaning of this address number |
---|---|---|
00H | 4byte | RIFF, resource exchange file flag. |
04H | 4byte | The 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. |
08H | 4byte | WAVE stands for the wav e file format. |
0CH | 4byte | FMT, waveform format flag |
10H | 4byte | 00000010H, 16PCM, my understanding is to use 16bit data to represent a quantization result. |
14H | 2byte | When it is 1, it indicates linear PCM coding, and when it is greater than 1, it indicates compressed coding. This is 0001H. |
16H | 2byte | 1 is mono, 2 is dual, here is 0001H. |
18H | 4byte | The sampling frequency here is 00002B11H, i.e. 11025Hz. |
1CH | 4byte | Byte rate = sampling frequency * number of audio channels * number of samples per sampling / 800005622H, that is, 22050Byte/s=11025 * 1 * 16/2 |
20H | 2byte | Block alignment = number of channels * number of samples per sampling / 80002H, that is, 2 = = 1 * 16 / 8 |
22H | 2byte | The number of sample data bits is 0010H, i.e. 16, and one quantized sample accounts for 2byte. |
24H | 4byte | data, just a sign. |
28H | 4byte | The 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. |
2CH | Uncertain | Quantitative 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; } }