Android游戏编程之音频编程

以下均转自Android游戏编程入门经典,转载请标明出处

如果你有一台Android设备,就会注意到当你按下增大或降低音量按钮时,你所控制的不同音量设置取决于你正在运行的应用程序。在通话中,你控制的是输入语音流的音量;在视频播放器中,你控制的是视频音频的音量;在主屏幕上,你控制的是铃声的音量。

Android为不同的目的提供不同音频流。当我们在游戏中播放音频时,可使用类来输出音效和音乐到特定的音乐流。不过,在我们想播放音效或音乐之前,需要确定音量按钮控制了正确的音频流。为此,我们使用Context接口的另一个方法:

context.setVolumeControlStream(AudioManager.STREAM_MUSIC);

一如既往,Context的实现仍然由我们的活动来负责。调用该方法之后,音量按钮就控制了该音乐流,后面我们就可使用它来输出我们的音效和音乐。在活动的生命周期内我们只需要调用该方法一次,最好是在Activity.onCreate()方法中调用它。

首先我们要分清音乐流和音效的不同。后者一般是存储在内存中且其长度不会超过几秒钟。Android系统给我们提供了一个SoundPool类,使用它可以很容易实现音效播放。

我们可以很简单地初始化一个新的SoundPool实例,如下所示:

SoundPool soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);

第一个参数指定在同一时刻我们最多能播放多少个音效。这并不是说我们不能加载更多的音效文件,它只不过是限制可同时播放的音效个数。第二个参数指定了SoundPool使用什么音频流来输出该音频。我们在这里选择音乐流,同时也已经为它设置好音量控件。最后一个参数现在没有使用,它应该为默认值0.

为了从一个音频文件加载音效到堆内存中,我们可使用SoundPool.load()方法。所有的文件都存储在assets/目录下,因此我们需要重载SoundPool.load()方法。所有的文件都存储在assets/目录下,因此我们需要重载SoundPool.load()方法来获得一个AssetFileDescriptor。我们怎么获得AssetFileDescriptor呢?使用AssetManager。这里我们使用SoundPool从assets/目录加载一个名为explosion.ogg的OGG文件:

AssetFileDescriptor descriptor = assetManager.openFd("explosion.ogg");

int explosionId = soundPool.load(descriptor, 1);

通过AssetManager.openFd()方法可直接获得AssetFileDescriptor,而通过SoundPool可很容易地加载音效,第二个参数用于指定该音效的优先级。这个参数目前未使用,为了以后的兼容应设置为1.

SoundPool.load()方法将返回一个整型值,它将作为一个句柄用于加载的音效。当我们想播放音效时,只需要指定该句柄,SoundPool就知道该播放哪个音频。

soundPool.play(explosionId, 1.0f, 1.0f, 0, 0, 1);

第一个参数是从SoundPool.load()方法接受句柄。接下来两个参数分别用于指定左右通道的音量,其值应该从0(静音)到1(最大)

接下来两个参数我们很少使用,其中第一个参数是优先级,目前没有使用,并且应该设置为0.而另一个参数用于指定音效循环播放的频率,一般不建议循环播放音效,因此设置为0。最后一个参数是播放速率,将其设置为大于1时,音效播放的速度将会比其在录制时快;而将它设置为小于1时,播放该音效就会比较慢。

当我们不再需要一个音效并希望释放内存时,可使用SoundPool.unload()方法:

soundPool.unload(explosionId);

我们只需要将从SoundPool.load()方法接收的音效句柄传入即可,该方法会将音效从内存卸载。

当我们完成所有的音效输出且不再需要SoundPool时,需要调用SoundPool.release()方法来释放SoundPool所占用的所有资源。当然,在释放之后,我们不能再使用SoundPool,而且SoundPool所加载的所有音效也会被释放。

现在编写一个简单的测试活动,每当单击屏幕时它就播放一个爆炸音效。代码如下:

package org.example.ch04_android_basics;

import java.io.IOException;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;

public class SoundPoolTest extends Activity implements OnTouchListener{
	SoundPool soundPool;
	int explosionId = -1;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		TextView textView = new TextView(this);
		textView.setOnTouchListener(this);
		setContentView(textView);
		
		setVolumeControlStream(AudioManager.STREAM_MUSIC);
		soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
		
		try{
			AssetManager assetManager = getAssets();
			AssetFileDescriptor descriptor = assetManager
					.openFd("explosion.ogg");
			explosionId = soundPool.load(descriptor, 1);
		}catch(IOException e){
			textView.setText("Couldn't load sound effect from asset, " +
					e.getMessage());
		}
	}

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		// TODO Auto-generated method stub
		if(event.getAction() == MotionEvent.ACTION_UP){
			if(explosionId != -1){
				soundPool.play(explosionId, 1, 1, 0, 0, 1);
			}
		}
		return true;
	}

	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		super.onPause();
		soundPool.release();
	}

}

SoundPool在处理MP3文件或长的音频文件时会有问题,长文件的定义超过5或6秒钟。一般建议使用OGG音频文件来代替MP3文件,并尽可能使用低的采样率和持续时间,同时保持音效质量。


短小的音效很时候放在Android应用程序从操作系统分配到的堆内存中,而包含较长音乐文件的大音频文件就很不适合了。为此,我们就需要将音乐以流的方式输出到音频硬件上,这就意味着每次我们只能读入一小块数据,该数据足于解码成原生的PCM数据并输出到音频芯片上。

这听起来挺吓人的。不过幸运的是,我们有MediaPlayer类,它能处理的所有事情。初始化MediaPlayer类:

MediaPlayer mediaPlayer = new MediaPlayer();

接下来我们需要告诉MediaPlayer播放什么文件,这同样通过AssetFileDescriptor来实现:

AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");

mediaPlayer.setDataSource(descriptor.getFileDescriptor(), descriptor.getStartOffset(), descriptor.getLength());

这里稍微比SoundPool中复杂一点。MediaPlayer.setDataSource()方法并没有直接获取AssetFileDescriptor,而是使用一个FileDescriptor,通过AssetFileDescriptor().getFileDescriptor()方法获取该描述符,除此之外我们还需要指定音频文件的偏移量和长度。为什么是偏移量?实际上资源是以单个文件形式存储的,为了让MediaPlayer获得文件的起始地址,我们需要将asset文件夹中的文件的偏移量提供给它。

在播放该音乐文件之前,需要再调用一个方法为MediaPlayer的播放做准备:

MediaPlayer.prepare();

这将实际地打开文件,检查它是否可以读取并用MediaPlayer实例来进行播放。从这里开始,我们就可以随意地播放、暂停和停止音频文件,也可以设置循环播放和改变音量。

启动播放可通过调用下面的方法进行:

mediaPlayer.start();

注意,该方法必须在成功调用MediaPlayer.prepare()方法之后才能调用(你将注意到它是否会抛出一个运行时异常)。

开始播放后,我们可以通过调用pause()方法来暂停播放:

MediaPlayer.pause();

只有我们成功准备好MediaPlayer并已启动播放,调用此方法才会生效。为了恢复一个暂停的MediaPlayer,可再次调用MediaPlayer.start()方法而无需任何准备。

通过调用下面的方法可停止播放:

MediaPlayer.stop();

注意,当我们想启动一个停止的MediaPlayer时,需要先再次调用MediaPlayer.prepare()方法。

我们可通过下面方法设置循环播放:

MediaPlayer.setLooping(true);

可通过下面的方法来调整音乐播放的音量:

MediaPlayer.setVolume(1, 1);

这会重新设置左右声道的音量,文档中没有指定这两个参数的设定范围。从多次尝试的结果看,有效值应该是0-1。

最后,我们需要一个方法来检查该播放是否完成,有两种方式可实现这一点。对于第一种方式,可向MediaPlayer注册一个OnCompletionListener,当播放完成时它就会被调用:

mediaPlayer.setOnCompletionListener(listener);

如果我们想轮询MediaPlayer的状态,则可使用下面方法:

boolean isPlaying = mediaPlayer.isPlaying();

注意,如果MediaPlayer被设置成循环播放,前面两个方法都无法指示该MediaPlayer已停止。

最后,如果MediaPlayer实例完成了所有的操作,就需要通过下面的方法来确保它所占用的资源得以释放:

mediaPlayer.release();

在丢弃一个实例前,执行这个操作应是很好的实践。

如果我们没有将MediaPlayer设置成循环且播放已结束,就可以通过MediaPlayer.prepare()和MediaPlayer.start()方法重启MediaPlayer。

大多数这些方法都是异步的,因此当调用MediaPlayer.stop()时,MediaPlayer.isPlaying()方法可能还需要一点时间才能返回。

现在编写一个测试活动,用循环模式播放assets/目录下的一个音频文件。该音效将根据活动的生命周期实现暂停和恢复,当活动暂停时,音乐也要暂停;而活动恢复时,音乐也要从上次暂停的地方开始播放。

代码如下:

package org.example.ch04_android_basics;

import java.io.IOException;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.widget.TextView;

public class MediaPlayerTest extends Activity {
	MediaPlayer mediaPlayer;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		TextView textView = new TextView(this);
		setContentView(textView);
		
		setVolumeControlStream(AudioManager.STREAM_MUSIC);
		mediaPlayer = new MediaPlayer();
		try{
			AssetManager assetManager = getAssets();
			AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");
			mediaPlayer.setDataSource(descriptor.getFileDescriptor(),
					descriptor.getStartOffset(), descriptor.getLength());
			mediaPlayer.prepare();
			mediaPlayer.setLooping(true);
		}catch(IOException e){
			textView.setText("Couldn't load music file, " + e.getMessage());
			mediaPlayer = null;
		}
	}

	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		super.onPause();
		if(mediaPlayer != null){
			mediaPlayer.pause();
			if(isFinishing()){
				mediaPlayer.stop();
				mediaPlayer.release();
			}
		}
	}

	@Override
	protected void onResume() {
		// TODO Auto-generated method stub
		super.onResume();
		if(mediaPlayer != null)
			mediaPlayer.start();
	}
}

在onResume()方法中,我们只需启动MediaPlayer(如果已经成功创建它)。onResume()方法是一个处理该操作的完美地方,因为它在onCreate()方法和onPause()方法之后被调用。在第一种情况下,它将第一次启动播放;在第二种情况下,它将简单地恢复已暂停的MediaPlayer。

在onPause()方法中,我们暂停MediaPlayer。如果该活动被销毁,我们还需要停止该MediaPlayer并释放所有资源

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值