Работа с потоковым аудио

    Введение


    За получение необработанных данных (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(). Он существует в трёх вариантах:
    • read(byte[] audioData, int offsetInBytes, int sizeInBytes)
    • read(short[] audioData, int offsetInShorts, int sizeInShorts)
    • read(ByteBuffer audioBuffer, int sizeInBytes)
    Их параметры:
    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));
    		}
    	}
    }
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      +3
      Думал вы расскажете про класс MediaPlayer и как с его помощью слушать потоковое аудио через интернет, а тут неожиданно топик про аудиозапись оказался.
        0
        Было бы неплохо увидеть пример записи потокового аудио в MP3 :)
          0
          Запилите, кто-нибудь, аналог AirVideo для андроида. Emit кривоват.
            0
            А можно ли с помощью android считывать аудио-поток входящего звонка?
              0
              MediaRecorder.AudioSource.VOICE_CALL — Voice call uplink + downlink audio source
              MediaRecorder.AudioSource.VOICE_DOWNLINK — Voice call downlink (Rx) audio source
              MediaRecorder.AudioSource.VOICE_UPLINK — Voice call uplink (Tx) audio source

              но не все аппараты поддерживают эти источники
                0
                А ответить сгенерированным аудио? Ну, на uplink отправить свой аудио-поток
                  0
                  насколько я знаю, это невозможно
                  0
                  Скажем так, что корпорация добра заблокировала корректное использование части этих команд. Вызвать VOICE_DOWNLINK конечно можно, но работает не корректно. Обонента та той стороне совсем нельзя записать к примеру.
                    0
                    Это вроде из-за каких-то американских законов, типа записывать можно только с разрешения записываемого.

              Only users with full accounts can post comments. Log in, please.