Разработка под Android / Работа с потоковым аудио

в 7:12, , рубрики: android, Audio, raw, метки: , ,

Введение

За получение необработанных данных (raw data) с микрофона отвечает класс android.media.AudioRecord. Он записывает данные во внутренний буфер, из которого мы должны периодически их забирать.

Конструктор

Для создания объекта нужно указать:

audioSource Откуда ведётся запись. В нашем случае это MediaRecorder.AudioSource.MIC
sampleRateInHz Частота дискретизации в герцах. Документация утверждает, что 44100Гц поддерживается всеми устройствами
channelConfig Конфигурация каналов. Может быть CHANNEL_IN_MONO или CHANNEL_IN_STEREO. Моно работает везде.

Важно: эти константы не совпадают с количеством каналов, которое они обозначают. В этот параметр нельзя передавать 1 или 2.

audioFormat Формат входных данных, более известный как кодек. Может быть ENCODING_PCM_16BIT или ENCODING_PCM_8BIT
bufferSizeInBytes Размер того самого внутреннего буфера. Из него можно считывать аудиопоток. Размер порции считывания не должен превышать эту величину. У этого параметра есть минимально допустимое значение, которое можно получить через getMinBufferSize().

При своём создании объект пытается получить нужные ему ресурсы системы. Насколько удачно у него это получилось, можно узнать, вызвав функцию getState(). Если она вернёт STATE_INITIALIZED, то всё нормально, если STATE_UNINITIALIZED — значит, произошла ошибка.

Причин ошибки может быть две: слишком маленький буфер и недопустимый формат. Первого нужно избегать вызовом getMinBufferSize(). Второго, на самом деле, им же.

getMinBufferSize()

Этот статический метод выдаёт минимальный размер внутреннего буфера, при котором объект AudioRecord сможет работать. Параметры имеют тот же смысл, что и для конструктора. Следует заметить, что использование именно этой величины для записи — не лучшая идея. Если система будет ещё чем-то занята, то программа всё равно может не успевать считывать все данные подряд, и в записи будут дырки. Мне встречался совет брать размер в десять раз больше.

Получение списка форматов

Метод getMinBufferSize() имеет приятную особенность — ругаться на недопустимые для данного устройства параметры, возвращая ERROR_BAD_VALUE или ERROR. Это означает, что перебирая все возможные сочетания, можно узнать, какие форматы поддерживает устройство.

Например, так:

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))              {                  // добавляем формат              }          }      }  }
Считывание данных

Для получения данных из внутреннего буфера служит метод read(). Он существует в трёх вариантах:

Их параметры:

audioData массив, в который будут записаны данные
audioBuffer буфер, в который будут записаны данные
offsetInBytes /
offsetInShorts
индекс, с которого начнётся запись
sizeInShorts размер запрашиваемого блока данных. В байтах для ByteBuffer и byte[], в коротких целых для short[]

Если всё нормально, то метод вернёт количество прочитанных байт, если это вариант с ByteBuffer или byte[], или прочитанных коротких целых для short[]. Если на момент вызова объект не был правильно инициализирован, то выдаст ERROR_INVALID_OPERATION, а если что-то не так с параметрами — ERROR_BAD_VALUE

Важно: метод блокирует вызывающий поток до тех пор, пока не считает запрошенное количество данных. Если во внутреннем буфере их недостаточно, то read() будет ожидать, пока они придут от микрофона. Поэтому метод следует вызывать из отдельного потока, иначе приложение будет висеть.

Подход, отход, фиксация

Чтобы программа могла получать данные от микрофона, нужно указать в файле AndroidManifest,xml соответствующее разрешение:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Чтобы начать запись, нужно вызвать метод startRecording(), а чтобы закончить — stop(). Запускать и останавливать запись можно сколько угодно раз.

После того, как работа с объектом закончена, следует вызвать метод release(). Он освободит все системные ресурсы, захваченные объектом. После этого объект нельзя использовать, а ссылающуюся на него переменную следует установить в null.

Важно: эти три метода, в отличие от упоминавшихся ранее, выбросят IllegalStateException, если их вызвать для неинициализированного (ну и слово...:) объекта или не в том порядке. Поэтому обращаться с ними нужно «аккуратно», т.е. через блок try.

Пример использования

Приведённый ниже класс делает всё то, о чём сказано выше. Кроме того, он посылает зарегистрированным у него Handler-ам сообщения о принятых данных. Эти данные будут обрабатываться в другом потоке, поэтому, чтобы не затереть ещё не обработанные данные новыми, использован циклический буфер.

В коде использован класс AudioFormatInfo. Он представляет собой POJO с тремя полями, описывающими формат записи: sampleRateInHz, channelConfig и 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<Handler> handlers;  	private AudioFormatInfo format;  	private AudioRecord mRecord;  	  	private final int BUFF_COUNT = 32;  	  	public AudioReciever(AudioFormatInfo format)  	{  		this.format = format;  		handlers = new ArrayList<Handler>();  		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));  		}  	}  }

Автор: ilyasivkov


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js