воскресенье, 31 марта 2013 г.

Звуковые эффекты

В этой статье расскажу как добавить в приложение android звуковые эффекты. Особо подчеркну, что это именно отдельные звуки, а не полноценное прослушивание аудио файлов.
Напишем высокоинтеллектуальное приложение “Коровка говорит Му” :)

1. Для начала немного теории:
Для хранения и воспроизведения звуков используется класс SoundPool.

SoundPool это набор (коллекция) звуков, которые могут быть загружены из ресурсов приложения или файловой системы устройства. Загрузить можно файлы практически всех популярных форматов, такие как mp3, ogg, wav, aac, 3gp, flac. После загрузки все файлы перекодируются (в 16-bit PCM моно или стерео), для упрощения воспроизведения.

Для экземпляра класса SoundPool можно задать количество аудио потоков, проигрываемых одновременно. Если максимальное число потоков будет превышено, автоматически остановится поток с самым низкий приоритетом. При наличии нескольких потоков с тем же низким приоритетом, будет выбран старейший из них. В случае, если приоритет нового потока ниже, чем все активные потоки, новый звук не будет проигран.Ограничение максимального числа потоков помогает снизить загрузку процессора.

Звуки могут быть проиграны несколько раз или даже зациклены. В последнем случае для остановки проигрывания нужно явно вызывать метод остановки.

Можно управлять скоростью воспроизведения аудио файла. Скорость 1,0 означает воспроизведение звука в исходном виде, 2,0 - воспроизведение в два раза быстрее, а 0,5 в два раза медленнее. Диапазон скорости воспроизведения составляет от 0,5 до 2,0.

1) Конструктор класса SoundPool выглядит так:
SoundPool (int maxStreams, int streamType, int srcQuality)
где
maxStreams - максимальное количество потоков, который могут воспроизводится одновременно
streamType - тип аудио потока, это константа из класса AudioManager. Здесь чаще всего используется AudioManager.STREAM_MUSIC.
srcQuality - качество кодирования. Сейчас этот параметр не используется, поэтому всегда будем передавать 0.

2) Загрузить аудио файлы в SoundPool можно с помощью 4 методов:
int load (AssetFileDescriptor afd, int priority)
Загрузка из assets. Директория /assets находится на том же уровне, что и /res. Для файлов, располагающихся в /assets, не генерируются идентификаторы ресурсов и доступ к ним можно получить по пути к файлу. Эта директория, в отличие от /res, позволяет задавать произвольную глубину подкаталогов и произвольные имена файлов.

int load (Context context, int resId, int priority)
Загрузка файла по идентификатору ресурса. Аудио файлы могут лежать в директории res/raw, и соответственно идентификатор сгенерится например такой R.raw.sound

int load (String path, int priority)
Загрузка файла из указанного пути

int load (FileDescriptor fd, long offset, long length, int priority)
Загрузка файла из файловой системы устройства. Этот вариант полезен, если вы храните несколько звуков в одином двоичном файле. offset определяет смещение от начала файла, а length определяет длину звука.

В каждом методе мы передаем priority - приоритет файла. Методы возвращают идентификатор загруженного звука, который позже используется для воспроизведения.

3) Загрузка файлов происходит асинхронно, то есть после вызова метода load() файл не сразу будет доступен для воспроизведения, так как необходимо время для его загрузки и перекодирования. Для того чтобы отследить окончание загрузки нужно обработать событие OnLoadCompleteListener.
Для этого устанавливаем обработчик события с помощью метода
void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener)
где SoundPool.OnLoadCompleteListener это интерфейс с единственным методом
void onLoadComplete(SoundPool soundPool, int sampleId, int status)
который вызывается когда файл загружен. Здесь sampleId - идентификатор загруженного файла (то есть то, что вернул метод load()), а status - статус загрузки (значение 0 обозначает успешную загрузку).

4) Остальные методы класса очень простые:
int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) - Проигрование звука
soundID - идентификатор звука (который вернул load())
leftVolume - уровень громкости для левого канала (от 0.0 до 1.0)
rightVolume - уровень громкости для правого канала (от 0.0 до 1.0)
priority - приоритет потока (0 - самый низкий)
loop - количество повторов (0 - без повторов, (-1) - зациклен)
rate - скорость воспроизведения (от 0.5 до 2.0)

void stop(int streamID) Остановка воспроизведения
void pause(int streamID) Приостановить воспроизведение
void resume(int streamID) Возобновить приостановленный поток (если поток был не на паузе, то метод не даст никакого результата)

void setLoop(int streamID, int loop) - Установка количество повторов для потока
void setPriority(int streamID, int priority) - Установка приоритета для потока
void setRate(int streamID, float rate) - Установка скорости для потока
void setVolume(int streamID, float leftVolume, float rightVolume) - Установка громкости для потока

void autoPause() Приостановить все активные потоки
void autoResume() - Возобновить все потоки

boolean unload(int soundID) Удаляет звук из SoundPool. Метод возвращает true если операция прошла успешно и false если такого SoundID не существует
void release() Удаляет все загруженные звуки из SoundPool и освобождает память. После вызова этого метода экземпляр класса SoundPool уже нельзя использовать. (Метод используется при выходе из программы, чтобы освободить ресурсы)

Все, можно приступать к программе.

2. Создаем новый проект (у меня CowSaysMoo)

Добавляем в main.xml 6 кнопок ImageButton с прозрачным фоном и нужной картинкой. Код приводить не буду, его можно посмотреть в исходниках (а их можно скачать в конце статьи)

3. Итак, нам надо:
1) Создать объект SoundPool
2) Загрузить в него аудио файлы. Я буду загружать их первым методом load() то есть из assets
3) Для интереса отловим событие OnLoadComplete, чтобы знать как это делается
4) Проигрываем нужный звук по нажатию на каждую кнопку

4. Создаем экземпляр класса SoundPool, задаем максимальное количество одновременно проигрываемых потоков - 3.

public class CowSaysMoo extends Activity {
    
  SoundPool mSoundPool;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
        
    mSoundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0);
  }
}
5. Положим аудио файлы в директорию /assets. При загрузки файлов в SoundPool метод load() возвращает идентификатор soundID, который нам нужно как-то сохранить для дальнейшего использованию. Я объявлю для каждого звука переменную, если же звуков много лучше завести для этого ассоциативный массив.

public class CowSaysMoo extends Activity {
    
  SoundPool mSoundPool;
  AssetManager assets;
  int catSound, chickenSound, cowSound, dogSound, duckSound, sheepSound;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
        
    mSoundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0);
    assets = getAssets();

    catSound = loadSound("cat.ogg");
    chickenSound = loadSound("chicken.ogg");
    cowSound = loadSound("cow.ogg");
    dogSound = loadSound("dog.ogg");
    duckSound = loadSound("duck.ogg");
    sheepSound = loadSound("sheep.ogg");
  }

  private int loadSound(String fileName) {
    AssetFileDescriptor afd = null;
    try {
      afd = assets.openFd(fileName);
    } catch (IOException e) {
      e.printStackTrace();
      Toast.makeText(this, "Couldn't load file '" + fileName + "'", Toast.LENGTH_SHORT).show();
      return -1;
    }
    return mSoundPool.load(afd, 1);
  }
AssetManager - класс предоставляющий доступ к файлам в директории /assets. Получить экземпляр класса для данного приложения можно методом getAssets();
AssetFileDescriptor - файловый дескриптор для файла из директории /assets. Его мы получаем с помощью метода openFd(), принимающего в качестве параметра имя файла. Если файл не найден или не может быть открыт, то выводим сообщение и в качестве soundID возвращаем -1.

6. Ради любопытства посмотрим как быстро у нас загружаются ресурсы, для этого напишем обработчик события OnLoadComplete

public class CowSaysMoo extends Activity {

  ...
  int countLoadedSound;

  protected void onCreate(Bundle savedInstanceState) {

    ...

    mContext = this;

    mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
      @Override
      public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
        Log.d("MY", "Complete load sampleId = " + sampleId + " status = " + status);
        if (status == 0)
          countLoadedSound++;
        if (countLoadedSound == 6)
          Toast.makeText(mContext, "All files sucessfully loaded", Toast.LENGTH_SHORT).show();
      }
    });
  }
}
Каждый раз когда заканчивается загрузка выводим сообщение в лог и если она закончилась успешно (status равен 0), то увеличиваем количество загруженных звуков countLoadedSound. Когда это количество равно 6, сообщаем об успешной загрузке.

Если мы запустим приложение, то увидим, что есть небольшая пауза между стартом программы и выводом сообщения об успешной загрузке файлов. В реальных приложениях это нужно учитывать, например, выводить диалог “Waiting” пока загрузка не закончится. И если наличие звука критично, как-то обрабатывать все ошибки связанные с загрузкой файлов. Мы этим заниматься не будем, пока ограничимся только записями в логах.

7. Файлы загружены, теперь пришло время проиграть звуки

protected void onCreate(Bundle savedInstanceState) {
  ...
  ImageButton cow = (ImageButton)this.findViewById(R.id.cow);
  cow.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      playSound(cowSound);
    }
  });
}

protected void playSound(int sound) {
  if (sound > 0)
    mSoundPool.play(sound, 1, 1, 1, 0, 1);
}
По нажатию кнопки вызываем метод playSound(), передавая ему нужный идентификатор звука. В методе playSound() проверяем, этот идентификатор. Как мы помним, если файл не найден, то метод loadSound() возвращал -1, а если метод load() класса SoundPool не смог загрузить файл, то soundID будет равен 0, поэтому проверяем, что SoundID > 0, что означает, что файл был успешно загружен. Если же все хорошо, то вызываем метод play().

Для остальных кнопок все аналогично.

8. Запустим и посмотрим, что нам скажет коровка :)

Исходники примера: CowSaysMoo.zip

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

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