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:

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 de Vasconcelos Leal 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

Cláudio Roberto disse...

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

Nelson Glauber de Vasconcelos Leal 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 agr 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