Work with streaming audio
Introduction
For receiving raw data from a microphone, the class is responsible
android.media.AudioRecord
. It writes data to the internal buffer, from which we must periodically collect it.Constructor
To create an object you need to specify:
audioSource | Where is the recording from? In our case, thisMediaRecorder.AudioSource.MIC |
---|---|
sampleRateInHz | Hertz sample rate. The documentation claims that 44100Hz is supported by all devices |
channelconfig | Channel Configuration Maybe CHANNEL_IN_MONO or CHANNEL_IN_STEREO . Mono works everywhere. Important: these constants do not match the number of channels that they denote. You cannot pass 1 or 2 to this parameter. |
audioFormat | An input data format, better known as a codec. Maybe ENCODING_PCM_16BIT orENCODING_PCM_8BIT |
bufferSizeInBytes | The size of that same internal buffer. From it you can read the audio stream. The reading portion size should not exceed this value. This parameter has the minimum value that can be obtained through getMinBufferSize() . |
During its creation, an object tries to get the system resources it needs. How well he succeeded in this, you can find out by calling the function
getState()
. If she returns STATE_INITIALIZED
, then everything is fine, if STATE_UNINITIALIZED
- then an error has occurred. There can be two causes of the error: a buffer that is too small and an invalid format. The first is to be avoided by calling
getMinBufferSize()
. The second, in fact, by him.getMinBufferSize ()
This static method returns the minimum size of the internal buffer at which the object
AudioRecord
can work. Parameters have the same meaning as for the constructor. It should be noted that using this particular value for recording is not a good idea. If the system is still busy with something, then the program may still not have time to read all the data in a row, and there will be holes in the record. I met advice to take a size ten times larger.Retrieving a list of formats
The method
getMinBufferSize()
has a nice feature - swear at parameters that are invalid for this device , returning ERROR_BAD_VALUE
or ERROR
. This means that by going through all the possible combinations, you can find out what formats the device supports. For example, like this:
int[] rates = {8000, 11025, 22050,44100, 48000, 96000 };
int[] chans = {AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO};
int[] encs = {AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT};
for(int enc : encs)
{
for(int ch : chans)
{
for(int rate : rates)
{
int t = AudioRecord.getMinBufferSize(rate, ch, enc);
if((t != AudioRecord.ERROR) && (t != AudioRecord.ERROR_BAD_VALUE))
{
// добавляем формат
}
}
}
}
Data reading
To get data from the internal buffer, use the method
read()
. It exists in three versions:read(byte[] audioData, int offsetInBytes, int sizeInBytes)
read(short[] audioData, int offsetInShorts, int sizeInShorts)
read(ByteBuffer audioBuffer, int sizeInBytes)
audioData | array in which data will be written |
---|---|
audioBuffer | buffer to which data will be written |
offsetInBytes / offsetInShorts | index at which recording will start |
sizeInShorts | size of the requested data block. In bytes for ByteBuffer and byte[] , in short integers forshort[] |
If everything is fine, then the method will return the number of bytes read, if it is an option with
ByteBuffer
or byte[]
, or read short integers for short[]
. If at the time of the call the object was not correctly initialized, it will return ERROR_INVALID_OPERATION , and if something is wrong with the parameters - ERROR_BAD_VALUE Important: the method blocks the calling thread until it reads the requested amount of data. If there are not enough of them in the internal buffer, then he
read()
will wait until they come from the microphone. Therefore, the method should be called from a separate thread , otherwise the application will hang.Approach, departure, fixation
So that the program can receive data from the microphone, you need to specify the appropriate resolution in the AndroidManifest, xml file:
To start recording, you need to call the method
startRecording()
, and to finish - stop()
. You can start and stop recording as many times as you like. After the work with the object is completed, you should call the method
release()
. It will release all system resources captured by the object. After that, the object cannot be used, and the variable referencing it should be set to null
. Important: these three methods, unlike those mentioned earlier, will be thrown away
IllegalStateException
if called for an uninitialized (well, a word ... :) object or in the wrong order. Therefore, they must be handled “carefully”, i.e. through the block try
.Usage example
The class below does everything that is mentioned above. In addition, he sends
Handler
messages registered with him about received data. This data will be processed in another thread, therefore, in order not to overwrite the data that has not yet been processed with new ones, a circular buffer is used. The code used a class
AudioFormatInfo
. It is a POJO with three fields describing the recording format sampleRateInHz
, channelConfig
and audioFormat
.package com.MyCompany;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.Process;
//AudioFormatInfo - POJO с полями sampleRateInHz, channelConfig и audioFormat
public class AudioReciever implements Runnable
{
private boolean mIsRunning;
private List handlers;
private AudioFormatInfo format;
private AudioRecord mRecord;
private final int BUFF_COUNT = 32;
public AudioReciever(AudioFormatInfo format)
{
this.format = format;
handlers = new ArrayList();
mIsRunning = true;
mRecord = null;
}
public void addHandler(Handler handler)
{
handlers.add(handler);
}
public void stop()
{
mIsRunning = false;
}
@Override
public void run()
{
// приоритет для потока обработки аудио
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
mIsRunning = true;
int buffSize = AudioRecord.getMinBufferSize(format.getSampleRateInHz(),
format.getChannelConfig(), format.getAudioFormat());
if(buffSize == AudioRecord.ERROR)
{
System.err.println("getMinBufferSize returned ERROR");
return;
}
if(buffSize == AudioRecord.ERROR_BAD_VALUE)
{
System.err.println("getMinBufferSize returned ERROR_BAD_VALUE");
return;
}
// здесь работаем с short, поэтому требуем 16-bit
if(format.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT)
{
System.err.println("unknown format");
return;
}
// циклический буфер буферов. Чтобы не затереть данные,
// пока главный поток их обрабатывает
short[][] buffers = new short[BUFF_COUNT][buffSize >> 1];
mRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
format.getSampleRateInHz(),
format.getChannelConfig(), format.getAudioFormat(),
buffSize * 10);
if(mRecord.getState() != AudioRecord.STATE_INITIALIZED)
{
System.err.println("getState() != STATE_INITIALIZED");
return;
}
try
{
mRecord.startRecording();
}
catch(IllegalStateException e)
{
e.printStackTrace();
return;
}
int count = 0;
while(mIsRunning)
{
int samplesRead = mRecord.read(buffers[count], 0, buffers[count].length);
if(samplesRead == AudioRecord.ERROR_INVALID_OPERATION)
{
System.err.println("read() returned ERROR_INVALID_OPERATION");
return;
}
if(samplesRead == AudioRecord.ERROR_BAD_VALUE)
{
System.err.println("read() returned ERROR_BAD_VALUE");
return;
}
// посылаем оповещение обработчикам
sendMsg(buffers[count]);
count = (count + 1) % BUFF_COUNT;
}
try
{
try
{
mRecord.stop();
}
catch(IllegalStateException e)
{
e.printStackTrace();
return;
}
}
finally
{
// освобождаем ресурсы
mRecord.release();
mRecord = null;
}
}
private void sendMsg(short[] data)
{
for(Handler handler : handlers)
{
handler.sendMessage(handler.obtainMessage(MSG_DATA, data));
}
}
}