воскресенье, 16 сентября 2012 г.

Talking Pets: Разработка питомца под Android – часть 1


Talking Pets: Разработка питомца под Android – часть 1
Разработку развлекательных приложений в стиле говорящих питомцев можно разделить на 3 основные части: запись речи, воспроизведение речи и анимация. Данная статья откроет небольшой цикл статей о разработке собственного говорящего питомца. Начнем нашу разработку с алгоритмов записи речи. 


Немного теории


Для обнаружения человеческой речи или звуков необходимо рассчитать уровень звукового давления. Для этого используются следующие формулы:
Talking Pets: Разработка питомца под Android – Расчет уровня звукового давления
В случае набора значений n
Talking Pets: Разработка питомца под Android – Набор значений n

значение среднеквадратичного звукового давления определяется по формуле: 
Talking Pets: Разработка питомца под Android – Среднеквадратичное значение

является константой, равной 20 µPa.


Полученный уровень звукового давления позволяет обнаружить человеческую речь или звук. Если данный уровень больше, чем граничное значение, то начинается запись.
Получается следующий алгоритм работы:

  • запускаем поток с записью;
  • получаем записанный блок данных и рассчитываем уровень звукового давления;
  • если полученный уровень больше уровня по умолчанию, то заносим данные в хранилище;
  • по окончанию записи (окончанием записи является переход, когда уровень звукового давления становиться меньше уровня по умолчанию) передаем данные из хранилища на вход потоку воспроизведения;
  • воспроизводим запись;
  • по окончанию воспроизведения возвращаемся на 1-ый шаг.

Что использовалось


Для записи звука используется класс AudioRecord. Этот класс управляет аудиоресурсами, чтобы записать аудиоданные со входов аудио оборудования на платформу. Это достигается за счет чтения данных из объекта AudioRecord. Для этого используется один из трех методов: read(byte[], int, int), read(short[], int, int) или read(ByteBuffer, int). Выбор того, какой метод использовать, будет основываться на формате хранения данных.
После создания, объект AudioRecord инициализирует связанный аудио буфер, который будет заполнен новыми аудио данными. Размер этого буфера, указанного в ходе создания, определяет, как долго AudioRecord может записывать звук до переполнения.


Практика

Сначала необходимо добавить разрешения в AndroidManifest:

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


Основные константы и атрибуты для класса:
public static final int DEFAULT_LP = 73; // Уровень звукового давления по умолчанию
public static int FREQUENCY = 24050; // Частота записи
public static final int CHANNEL = AudioFormat.CHANNEL_CONFIGURATION_STEREO; // Моно или стерео канал
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;// Кодирование
public static final int SIZE = 1000000; // Размер данных
 
// Частота дискретизации
private static final int[] mSampleRates = new int[] { 44100, 22050, 16000, 11025, 8000 };
 
// Записываемые данные
private byte[] array = new byte[SIZE];
private int size = 0;

private int bufferSize; // Размер буфера записи
private byte[] buffer;  // Буфер, содержащий записанный голос
 
private AudioRecord audioRecord = null;  // Используется для записи звуковых данных
private Timer recordTimer = new Timer();  // Таймер для остановки записи

private Handler handler; // Для отправки сообщений UI потоку приложения

// Служат для проверки разных состояний потока
private boolean isStop = false;  
private boolean isStartRecord = false; 
private boolean isNotFullData = false;  
private boolean isRecord = false; 
private boolean isTimerStart = false;
private boolean isFirst = true;
private boolean isFinish = false;
private boolean isRec = false;

private byte[] buffer_temp; // промежуточный буфер

У нас есть буфер данных byte[] buffer, в который AudioRecord помещает полученные данные из аудио оборудования.
Сначала необходимо инициализировать AudioRecord:
 /**
  * Используется для поиска возможного AudioRecord с заданными параметрами для устройства
  */
 public AudioRecord findAudioRecord() {
  DebugLog.w(TAG, "findAudioRecord()");
  // Поиск минимального размера буфера
  this.bufferSize = AudioRecord.getMinBufferSize(FREQUENCY, CHANNEL, ENCODING);
  if (this.bufferSize != AudioRecord.ERROR_BAD_VALUE) {
   AudioRecord recorder = new AudioRecord(AudioSource.DEFAULT,
     FREQUENCY, CHANNEL, ENCODING, bufferSize);
   if (recorder.getState() == AudioRecord.STATE_INITIALIZED)
    return recorder;
  }
  // Если создать AudioRecord с заданными параметрами не удалось, то пробуем создать хоть какой-нибудь AudioRecord
  for (int rate : mSampleRates) {
   try {
    this.bufferSize = AudioRecord.getMinBufferSize(rate, CHANNEL,
      ENCODING);

    if (this.bufferSize != AudioRecord.ERROR_BAD_VALUE) {
     FREQUENCY = rate;
     AudioRecord recorder = new AudioRecord(AudioSource.DEFAULT,
       FREQUENCY, CHANNEL, ENCODING, this.bufferSize);

     if (recorder.getState() == AudioRecord.STATE_INITIALIZED)
      return recorder;
    }
   } catch (Exception e) {
    e.printStackTrace();
   }
  }
  return null;
 }

 public Record(Handler handler) {
  DebugLog.w(TAG, "Record()");
  this.handler = handler;
  this.isNotFullData = false;
  this.isStop = false;
  this.isFirst = true;
  this.audioRecord = findAudioRecord();
  if (this.bufferSize > 0)
   this.buffer = new byte[this.bufferSize]; 
 }


Теперь расчитываем Prms:
private double getPrms(byte[] buffer)
 {
  double prms = 0d;
  for (int i = 0; i < buffer.length / 2; i++) {
   short x = getShort(buffer[i * 2], buffer[i * 2 + 1]);
   prms += x * x;
  }
  
  prms = Math.sqrt(prms / buffer.length);
  return prms;
 }

Метод getShor tпреобразует byte в short для более точного расчета уровня звукового давления:
private short getShort(byte argB1, byte argB2) {
  return (short) (argB1 | (argB2 << 8));
 }

После того как рассчитали Prms, определяем Lp (уровень звукового давления):
private double getPrms(byte[] buffer)
 {
  double prms = 0d;
  for (int i = 0; i < buffer.length / 2; i++) {
   short x = getShort(buffer[i * 2], buffer[i * 2 + 1]);
   prms += x * x;
  }
  
  prms = Math.sqrt(prms / buffer.length);
  return prms;
 }
Основной метод – это run, который вызывается у Runnable объектов. В этом методе проверяется, надо ли записывать данные или нет, являются ли записанные данные речью, и закончилась ли запись.
public void run() {
  
  while (!this.isFinish)
  {
   if (!this.isStartRecord)
    continue;
   try {
    DebugLog.w(TAG, "run()");
    int lp = 0; // текущий уровень звукового давления
    // Инициализация переменных для расчета уровня звукового давления
    double prms = 0.0;
    double pref = 0.00002;
    int shum = -80; // фоновый шум канала передачи данных
    this.array = new byte[SIZE];
    this.size = 0;
    
    boolean isListen = true;
    int count_lp = 0;
    
    if (this.audioRecord != null) {
     
     this.audioRecord.startRecording();   
     while (this.isNotFullData) {
 
      Arrays.fill(buffer, (byte) 0); // заполняем массив нулями
      this.audioRecord.read(this.buffer, 0, this.buffer.length); // начинаем записывать речь
      
       // FIXME: при первой записи заноситься шум. Пропускаем первые данные.
      //Из-за этого возможна ситуация, что персонаж проглотит первый слог 
      if (this.isFirst) {
       this.isFirst = false;
       continue;
      }
      prms = getPrms(this.buffer);
      lp = getLp(prms, pref, shum);

      int max_lp = DEFAULT_LP;
      
      if (!isRecord) {
       // Если текущий уровень звукового давления больше, чем уровень по умолчанию, начинаем записывать данные.
       if (lp > max_lp) {   
        
        DebugLog.i(TAG, "lp = " + lp);
        
        count_lp++;
        if (count_lp != 1)
        {
         this.isTimerStart = true;
         this.isRecord = true;
         this.isRec = true;
         if (null != this.recordTimer)
          this.recordTimer.cancel();
         this.recordTimer = new Timer();
         if (isListen) {
          this.handler.sendEmptyMessage(Constants.MSG_LISTEN);
          isListen = false;
         }
        }
        else
        {
         this.buffer_temp = this.buffer;       
        }
       }
      } else {
       if (lp <= max_lp) {
        /* Если уровень звукового давления меньше уровня по умолчанию, то запускаем таймер для остановки записи.
          Это используется для того, чтобы записать небольшие паузы между словами (в 0,65 с), а не начинать воспроизводить
          сразу после окончания слова.
        */
        this.isRecord = false;
        if (null != this.recordTimer)
         this.recordTimer.cancel();
        TimerTask task = new TimerTask() {
         public void run() {
          isTimerStart = false;
          isNotFullData = false;
          isStop = false;
         }
        };
        this.recordTimer = new Timer();
        this.recordTimer.schedule(task, 650);
        this.isTimerStart = true;
       }
      }
      // Заносим звуковые данные в массив, если они есть
      if (this.isTimerStart) {
       if (this.buffer_temp!=null)
       {
        for (int i = 0; i < this.buffer_temp.length; i++)
         this.array[this.size + i] = this.buffer_temp[i];
        this.size += this.buffer_temp.length;
        this.buffer_temp = null;
       }
       for (int i = 0; i < this.buffer.length; i++)
        this.array[this.size + i] = this.buffer[i];
       this.size += this.buffer.length;  
       if (this.size + this.buffer.length > SIZE)
        this.isNotFullData = false;
      }
     }
    }
   } catch (Exception e) {
    e.printStackTrace();
   }

   try {
    if (this.recordTimer!=null)
     this.recordTimer.cancel();
    this.isRec = false;
    if (this.audioRecord != null) {
     if (!this.isStop) {
      this.isStartRecord = false;
      
      // Вопроизводим записанную речь
      Message msg = this.handler.obtainMessage();
      msg.what = Constants.MSG_PLAYBACK;
      
      Bundle bundle = new Bundle();
      bundle.putInt(Constants.DATA_SIZE, this.size); 
      bundle.putByteArray(Constants.DATA_ARRAY, this.array);
      msg.setData(bundle);
      
      this.handler.sendMessage(msg);
     }
    } 
   } catch (Exception e) {
    e.printStackTrace();
   }  
  }
 }

Ссылки

  • Исходные коды данного проекта можно скачать отсюда (только пока это не действующий проект): zip
  • Пример говорящего питомца под Android: Тарабаня
  • Talking Pets: Разработка питомца под Android – часть 2, часть 3

Комментариев нет:

Отправить комментарий