segunda-feira, 3 de janeiro de 2011

Áudio no Android

Olá povo,

Para tocar qualquer som no Android, podemos utilizar a classe MediaPlayer. Com ela podemos executar arquivos de áudio que estejam dentro do APK (na pasta assets ou res/raw) ou ainda no sistema de arquivos do aparelho (como SD card por exemplo).
// Carregando audio do diretório res/raw
MediaPlayer player = 
  MediaPlayer.create(this, R.raw.explosion);
player.start();

// Carregando audio do cartão de memória
MediaPlayer mp = new MediaPlayer();
mp.setDataSource("/sdcard/explosion.mp3");
mp.prepare();
mp.start();


Porém, em um dos aplicativos que desenvolvemos aqui no trabalho, nós precisávamos tocar dois sons simultâneamente: a música do jogo e o efeito sonoro. A classe MediaPlayer não é aconselhável para efeitos sonoros uma vez que eles precisam de um tempo de resposta baixo. Para esse trabalho, podemos utilizar as classes SoundPool e AudioManager (do pacote android.media) que facilitam o trabalho com sons, permitindo tocá-los concorrentemente.

Vejam o exemplo comentado abaixo:
public class SoundManager {
  // Total de sons no pool
  private static final int MAXSTREAMS = 4;
  // Instância única 
  private static SoundManager instance;
  
  // Pool de sons
  private SoundPool mSoundPool;
  // AudioManager para controlar o volume do som
  private AudioManager mAudioManager;
  // Lista com os ids dos sons adicionados
  private ArrayList<Integer> mSoundPoolMap;
  // Pilha que armazena as transações 
  // de execução dos sons
  private Stack<Integer> mSongsTransactions;
  
  private Context mContext;

  // Construtor privado pra implementar o 
  // Singleton Design Pattern
  private SoundManager(Context ct) {
    mContext = ct;
    mSoundPoolMap = new ArrayList<Integer>();
    mSongsTransactions = new Stack<Integer>();
    
    // Criando o pool de sons
    mSoundPool = new SoundPool(
        MAXSTREAMS, AudioManager.STREAM_MUSIC, 0);
    
    // AudioManager é um serviço de sistema
    mAudioManager = (AudioManager) 
        mContext.getSystemService(
            Context.AUDIO_SERVICE);
  }

  // Método estático para obter a instância única
  public static SoundManager getInstance(Context ct) {
    if (instance == null){
      instance = new SoundManager(ct);
    }
    return instance;
  }

  // Adiciona um som ao pool
  public void addSound(int soundId) {
    mSoundPoolMap.add(
      /* Carrega e obtém o id do som no pool
       * O segundo parâmetro o id do recurso
       * E o terceiro não serve pra nada :) ,
       * Mas na documentação diz pra colocar 1 */
      mSoundPool.load(mContext, soundId, 1));
  }

  // Manda tocar um determinado som
  public void playSound(int index) {
    /* O AudioManager é usado aqui pra obter
     * o valor atual do volume do aparelho para
     * não tocar o som nem baixo nem alto demais.
     * A divisão que é feita aqui é pq o método 
     * requer um valor entre 0.0 e 1.0. */
    float streamVolume = 
      mAudioManager.getStreamVolume(
          AudioManager.STREAM_MUSIC);
    streamVolume /= 
      mAudioManager.getStreamMaxVolume(
          AudioManager.STREAM_MUSIC);
    
    /* playId, armazena o id da requisição do som 
     * a ser tocado. Ele é usado para parar um 
     * determinado som a qualquer momento. */
    int playId = mSoundPool.play(
      mSoundPoolMap.get(index), // ID do som
      streamVolume, // volume da esquerda
      streamVolume, // volume da direita
      1, // prioridade 
      0, // -1 toca repetidamente, 
         // n = número de repetições)
      1  // pitch. 0.5f metade da velocidade
         // 1 = normal e 2 = dobro da velocidade
      );
    
    // adiciona o id da transação na pilha
    mSongsTransactions.push(playId);
  }

  public void stopSounds() {
    // Percorre todos os ids da pilha e manda
    // parar todos os sons
    while (mSongsTransactions.size() > 0)
      mSoundPool.stop(mSongsTransactions.pop());
  }

  // Libera os recursos alocados
  public void cleanup() {
    mSoundPool.release();
    mSoundPool = null;
    mSoundPoolMap.clear();
    mSongsTransactions.clear();
    mAudioManager.unloadSoundEffects();
  }
}

Com a classe acima, podemos executar duas mídias concorrentemente e com um tempo de resposta muito bom. Vejam como utiliza-la abaixo:
public class TesteSomActivity 
  extends Activity 
  implements OnClickListener{

  SoundManager sm;
 
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    // Informa que a aplicação controlará o 
    // volume da media do telefone pelos botões
    // de volume do aparelho
    setVolumeControlStream(
      AudioManager.STREAM_MUSIC);
        
    sm = SoundManager.getInstance(this);
    sm.addSound(R.raw.explosao);
    sm.addSound(R.raw.musica);
        
    ((Button)findViewById(R.id.btnStart1)).
      setOnClickListener(this);
    ((Button)findViewById(R.id.btnStart2)).
      setOnClickListener(this);
    ((Button)findViewById(R.id.btnStop)).
      setOnClickListener(this);
  }

  public void onClick(View v) {
    if (v.getId() == R.id.btnStart1){
      sm.playSound(0);

    } else if (v.getId() == R.id.btnStart2){
      sm.playSound(1);

    } else if (v.getId() == R.id.btnStop){
      sm.stopSounds();
    }
  }
}

O primeiro botão executará um efeito sonoro de explosão, enquanto que o segundo tocará a música do jogo. O terceiro parará ambos.
Se o aúdio for muito grande, é melhor optar por combinar o MediaPlayer para a música do jogo e SoundPool para os efeitos sonoros.

Para fazer com que sua aplicação controle o volume da mídia através dos botões de volume do aparelho (ou das teclas + e - no emulador) basta colocar a linha abaixo no onCreate da sua Activity:

setVolumeControlStream(AudioManager.STREAM_MUSIC);


Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

P.S.: Esse exemplo foi baseado em dois posts encontrados por dois colaboradores daqui do CESAR.
Post 1 e
Post 2

9 comentários:

Rodrigo Jardim disse...

Entre tantas outras alternativas, só essa classe resolveu o problema do som da aplicação.

Fernando França disse...

olá, parabéns pelo post!
Eu usei sua classe e esta dando o seguinte erro no logCat:
04-01 15:29:14.223: WARN/SoundPool(654): sample 1 not READY

Isso está acontecendo usando outras classes que eu vi na net também...

Você sabe me dizer o que tenho que fazer?

Eu preciso executar um som de "bip" a cada segundo. Esse arquivo de som está no formato mp3 dentro da pasta raw e tem 834 Bytes.

Se você puder me ajudar eu te agradeço!

Nelson Glauber disse...

Oi Fernando,

Acho que o problema é que você está tentando usar o som sem que o SoundPool tenha terminado de carregá-lo.
Dá uma olhada nessa discussão:
http://stackoverflow.com/questions/5202510/soundpool-sample-not-ready

4br4ç05,
nglauber

Unknown disse...

Olá, como eu executo um som em mp3 no segundo exemplo, não consegui fazer isso.

Nelson Glauber disse...

Oi Claudio,

Verifica se o caminho do arquivo está correto. Uma outra coisa pode ser o codec usado pelo MP3 que não seja suportado.
Dá uma olhada nesse link aqui:
http://nglauber.blogspot.com.br/2011/04/android-dicas-2.html

Pode ser que ajude.

4br4ç05,
nglauber

Thiago disse...

mt show deu certo . Vlw !! Vai dar upgrade no joguinho to criando :D

Valdir lima disse...

Tudo certo. Testado e aprovado. Valeu.

andrea disse...

Muitos parabéns pelo post.

será que me podes tirar uma dúvida... Como posso fazer para que isto funcione, mas que os ficheiros estejam armazenados num servidor web. é possível?

Nelson Glauber disse...

Oi Andrea,

Como esses arquivos de som teoricamente devem ser pequenos, é melhor você fazer o download e cache dos mesmos antes de executar.

4br4ç05,
nglauber