手机播放.doc
7.2 音乐播放器
在Android平台中,以可选包的方式提供了一个名为android.media的包来提供用于播放多媒体文件的API。 7.2.1 音乐播放器开发引言
对于任何一个拥有多媒体手机的读者而言,如果能够自主开发出一款定制的手机音乐播放器是一件多么令人有成就感的事情。如果说仅仅是在WTK模拟器上,那么使用MMAPI就可以做出一个简单的音乐播放器来,然而如果要真正应用到实机上却不是那么一帆风顺的事情,在本节中笔者会逐步向读者解释这么说的原因。图7-1是一款音乐播放器在WTK模拟器和NOKIA 5310实机上的运行界面。
该款播放器在实机上支持mp3、wma、mid、aac(Advanced Audio Coding,高级音频编码)、mxmf、amr、nrt(NOKIA Ring Tone,NOKIA自谱铃声)等音乐媒体类型。
提示 音乐播放器能够播放的音乐媒体类型都是实机可以支持的类型。如果实机不支持播放某种
的音乐文件,那么音乐播放器也将无法播放该类型的文件。
图7-1 音乐播放器在WTK模拟器和NOKIA 5310实机上的运行界面
7.2.2 音乐播放器的功能说明
作为一款实用的手机音乐播放器,不仅要考虑界面的个性化,而且还要考虑较高的播放效率和手机厂商对音乐媒体类型的支持。
(1)实现播放的基本功能。它包括播放控制、音量调节、扩展卡资源读取支持等。 (2)流畅的播放效果。本案例中采用播放池(Player Pool)策略,这一策略将使播放过程更为流畅。造成播放停滞的原因主要有两个方面:一方面基于MIDlet的安全考虑,每当读取本地文件时手机系统都要求进行手动确认,这样就会间断播放过程。另外一方面,每次对音乐文件的播放预处理也会造成一定的播放间隔。
(3)完全满足手机厂商所支持的音乐媒体类型,支持尽可能多的音乐媒体类型。本案例中采用的是系统自动匹配的策略,即由手机系统本身来识别播放音乐类型。
以上的要求看似比较简单,但是由于模拟器环境和实机环境的差异,在实际开发中总会遇到一些小问题,接下来就和读者朋友们分享开发手机音乐播放器的苦闷和惊喜。
7.2.3 音乐播放池
引进播放池的策略主要是为了克服播放音乐文件时由于创建和销毁播放器对象(Player)而影响效率的情形。通过播放池方式,可将所有的播放器对象放入池中进行管理,播放指定的音乐文件时只需要在播放池中获取对应的播放器对象进行播放即可,而无须重新初始化播放器状态。这种方式的另外一个优点是还可以记录该音乐文件的播放状态,例如,播放位置、音量值等。
当然,采用播放池的方式会增加内存的消耗,在实施过程中需要结合硬件配置对池的大小进行调整。
7.2.4
分析
将手机音乐播放器工程定义为4个类:
(1)PlayerMIDlet,主MIDlet也是整个程序的入口,程序启动时显示音乐播放控制面板。 (2)AudioPanel,音乐播放控制面板,它是程序主界面,负责播放控制并对播放对象池进行管理。 (3)PoolHelper,对象池帮助类,负责生成播放对象池。
(4)AudioSettingPanel,音量控制面板,实现对播放过程的音量控制。
另外,音乐播放器工程还引入了文件选择模块(参见第3章),用于生成播放文件列
。图7-2是该工程的框架设计图。
图7-2 手机音乐播放器工程的框架设计图
7.2.5 音乐播放器的实现过程
1(添加播放列表
如图7-3所示,当单击“添加播放列表”菜单时,播放器会调用文件选择组件来选择音乐文件,并将选择的文件添加到主界面的列表中。
图7-3 播放列表面板
代码7-1是调用文件选择组件的关键代码。
代码7-1 调用文件选择组件
//界面命令响应
public void commandAction(Command c, Displayable d) {
„„
else if(c == cmdAddPlayList) {//添加播放列表
chooser = new FileChooser(this);
}
„„
}
代码7-2是添加播放列表的核心代码。文件选择组件通过回调播放器主界面的
finishAddList方法来实现播放列表的添加(第2行)。在添加之前还进行了播
放项目的重复性判断(第19行),以免重复添加。 当播放项目添加成功后,以播放文件的资源字符串启动对象池帮助线程
PoolHelper(第40行),并提供开始播放命令(第46行)。 代码7-2 添加播放列表
1 //结束添加列表
2 public void finishAddList() {
3 //显示当前界面(必须在showSelected之前) 4 display.setCurrent(playList);
5
6 //显示选择结果
7 showSelected();
8 }
9
10 //显示选择结果
11 private void showSelected() {
12 //当前选择播放资源
13 String uri = chooser.getCurrentFile(); 14
15 if(uri == null) {//取消选择
16 return;
17 }
18
19 if(URITable.indexOf(uri) == -1) {//该资源不存在 20 //添加资源列表
21 URITable.insertElementAt(uri, 0); 22
23 //以插入方式
24 playList.insert(0, getRelativeName(uri), 25 iconHelper.getIconByExt(extractExt(uri) ) );
26 }
27 else {//该资源已经存在,无须更新
28 //for debug
29 System.out.println("Item " + uri + " already exists!!");
30
31 Alert alert = new Alert("Error", 32 "This item already exists!!", errorImage, AlertType.ERROR);
33 alert.setTimeout(Alert.FOREVER); 34 Display.getDisplay(let).setCurrent(alert); 35
36 return;
37 }
38
39 //对象池帮助类
40 helper = new PoolHelper(this, uri); 41 helper.start();
42
43 //如果不存在当前播放才添加播放菜单 44 if(isPlaying == false) {
45 //添加播放
46 playList.addCommand(cmdPlay); 47 }
48 }
2(播放控制
如图7-4所示,音乐播放控制面板(主界面)提供了播放、暂停、音量设置等播
放控制,并对播放状态进行侦听(实现了PlayerListener的接口)。
图7-4 音乐播放控制面板
(1)播放控制菜单。为了控制播放过程,播放界面提供了控制菜单。在命令执行回调函数(commandAction)
中对用户的控制行为进行处理。代码7-3是播放控制菜单命令响应的关键代码。 代码7-3 播放控制菜单的命令响应
1 //界面命令响应
2 public void commandAction(Command c, Displayable d) { 3 „„
4 else if(c == cmdPlay) {//播放
5 //记录当前选择媒体索引
6 selectedIndex = playList.getSelectedIndex();
7
8 if(startPlay() == true) {//播放成功 9 //修改播放控制
10 isPlaying = true;
11
12 playList.removeCommand(cmdPlay); 13 playList.addCommand(cmdSetting); 14 playList.addCommand(cmdStop); 15 }
16 }
17 else if(c == cmdStop) {//停止
18 if(stopPlay() ) {//暂停成功 19 //修改播放控制
20 isPlaying = false;
21
22 playList.removeCommand(cmdStop); 23 playList.addCommand(cmdPlay); 24 }
25 }
26 else if(c == cmdSetting) {//音量设置 27 AudioSettingPanel panel = new AudioSettingPanel(let, this,
28 (VolumeControl)volumePool.elementAt(selectedIndex) );
29 }
30 }
(2)开始播放。当播放列表添加完毕后,就可以播放指定的项目了。在代码7-4中,用于播放的播放器对
象是从播放器对象池中获取的(第4行),通过调用播放器对象的start方法开始进行播放(第8行)。
代码7-4 开始播放
1 //开始播放
2 private boolean startPlay() {
3 //开始播放
4 Player player = (Player)playerPool.elementAt(selectedIndex); 5
6 if(player != null) {//当前播放器有效
7 try {
8 player.start();
9 }
10 catch(MediaException e) {
11 //显示警告
12 Alert alert = new Alert("Exception", 13 e.getMessage(), errorImage, AlertType.ERROR); 14 alert.setTimeout(Alert.FOREVER);
15 Display.getDisplay(let).setCurrent(alert); 16
17 return (false);
18 }
19 }
20 else {
21 Alert alert = new Alert("Error",
22 "Player created failed!! The reason may be\n" + 23 "system can't supported current media type!!", 24 errorImage, AlertType.ERROR);
25 alert.setTimeout(Alert.FOREVER);
26 Display.getDisplay(let).setCurrent(alert); 27
28 return (false);
29 }
30
31 //记录当前资源字符串
32 currentURI = playList.getString(selectedIndex); 33
34 return (true);
35 }
(3)停止播放。通过播放控制菜单,用户可以随时终止正在播放的项目。在代码7-5中,用于停止播放的
播放器对象也是从播放器对象池中获取的(第3行),通过调用播放器对象的stop方法停止播放(第7行)。
代码7-5 停止播放
1 //停止播放
2 private boolean stopPlay() {
3 Player player = (Player)playerPool.elementAt(selectedIndex); 4
5 if(player != null) {
6 try {
7 player.stop();
8 }
9 catch (MediaException e) {
10 return (false);
11 }
12
13 return (true);
14 }
15
16 return (false);
17 }
(4)侦听播放状态。播放状态的侦听对于控制播放状态是相当便利的,通过对播放状态的侦听可以接收到
完整的播放状态信息。在代码7-6中,通过实现PlayerListener接口的playerUpdate方法来实现对播放状态的侦听。所有的播放器对象都设置同样的播放侦听,这样可以方便对整体播放的控制,因为在同一时刻只能允许一个播放行为。
代码7-6 侦听播放状态
1 //播放侦听回调函数
2 public void playerUpdate(Player player, String event, Object eventData) { 3 if( event.equals(PlayerListener.CLOSED) || //当关闭 4 event.equals(PlayerListener.END_OF_MEDIA) ) {//当结束 5 //自动修改播放控制
6 isPlaying = false;
7
8 //播放结束释放停止菜单,添加播放菜单
9 playList.removeCommand(cmdStop);
10 playList.addCommand(cmdPlay);
11
12 playList.setTitle(DEFAULT_TITLE);
13 }
14 else if(event.equals(PlayerListener.STOPPED) ) {//当停止 15 playList.setTitle("Stop " + currentURI); 16 }
17 else if(event.equals(PlayerListener.STARTED) ) {//当开始 18 playList.setTitle("Playing " + currentURI); 19 }
20 }
3(音量控制
代码7-3中调用了音量控制面板,所传入的音量控制对象(VolumeControl)是
从音量控制器对象池中获得的。如图7-5所示,在音量控制面板中通过Gauge
组件来设置音量控制对象的音量代表值。
图7-5 音量控制面板
在代码7-7中,通过实现ItemStateListener接口的itemStateChanged方法来捕获用户设定值的改变,同时通过音量控制接口的setLevel来设置控制音量的值。
代码7-7 音量控制面板
//音量值项目改表
public void itemStateChanged(Item item) {
vc.setLevel(gauge.getValue() );
gauge.setLabel("Volume: " + gauge.getValue() );
}
4(音乐播放器对象池管理
在代码7-3和代码7-4中提到了播放器对象池和音量控制器对象池,代码7-2中提到了使用播放文件的资源字符串来启用对象池帮助线程。在代码7-8中的实现就是通过播放文件的资源字符串来生成播放器和音量控制器对象(第24行),并纳入对象池的管理(第46行)。
(1)音乐播放池对象管理。
代码7-8 音乐播放池对象管理
1 public class PoolHelper extends Thread {
2 //常量定义
3 private final String RES_PREFIX = "file://"; //资源前缀
4 private final String VOLUME_CTRL_NAME = "VolumeControl"; //音量控制器名称
5 //音乐播放面板
6 private AudioPanel panel;
7 //播放资源URL
8 private String url;
9
10 public PoolHelper(AudioPanel __panel, final String __uri) {
11 panel = __panel;
12 url = RES_PREFIX + __uri; 13
14 //调试信息
15 System.out.println(url); 16 }
17
18 public void run() {
19 Player player = null;
20 VolumeControl volume = null; 21
22 try {
23 //创建播放器
24 player = Manager.createPlayer(url);
25 //调整到实行模式
26 player.realize();
27
28 //设置播放侦听
29 player.addPlayerListener(panel); 30
31 //获取音量控制器并设定初值 32 volume = (VolumeControl)player.getControl(VOLUME_CTRL_NAME);
33
34 if(volume != null) {//某些时候可能会为空,该错误无法捕获
35 volume.setLevel(62); //0.618 36 }
37 }
38 catch (IOException e) {
39 e.printStackTrace();
40 }
41 catch(MediaException e) {
42 e.printStackTrace();
43 }
44
45 //添加到媒体文件流容器(以插入方式) 46 panel.playerPool.insertElementAt(player, 0); 47 panel.volumePool.insertElementAt(volume, 0);
48 }
49 };
(2)释放对象池。代码7-9完整地揭示了如何释放播放器所使用的资源。当程序退出时必须释放对象池。
播放器对象必须通过调用其close方法(第10行)才能实现资源的完全释放。
代码7-9 释放对象池
1 //释放媒体流池
2 private void freePlayerPool() {
3 int playerCount = playerPool.size(); 4
5 //关闭所有的音乐播放器
6 for(int i = 0; i < playerCount; ++i) { 7 //关闭播放器
8 Player player = (Player)playerPool.elementAt(i);
9 if(player != null) {
10 player.close();
11 }
12 }
13
14 //释放播放器池和音量调节池
15 playerPool.removeAllElements(); 16 volumePool.removeAllElements(); 17 }
7.2.6 对音乐播放器开发的探讨
1(对音乐媒体类型的支持
图7-6是常见的音乐类型不支持的警告。
图7-6 音乐类型不支持的警告
对于WTK模拟器环境(笔者使用的是WTK2.5.2),似乎只支持wav、mid、au等类型的音乐媒体,当播放mp3、wma类型的音乐时就会抛出“不支持的内容类型”的异常。对于实机环境,厂商
支持的类型程序都可以支持。在NOKIA 5310环境中,可以支持mp3、wma、mid、aac等主流音乐媒体类型以及其他NOKIA自己定义的播放类型(例如nrt格式)。
提示 所谓的不支持某种类型的音乐~实际上是指在该环境下没有安装该类型的音乐文件解码器,Decoder,。当模拟器环境和实机环境安装的解码器不同时~就会出现在模拟器环境下不支持的文件可以在实机上正常播放的情形。
2(音乐播放对象的创建方式
代码7-8中并没有使用输入流和音乐内容类型(Content type)的方式来创建音乐播放对象,而是通过文件的URI来自动创建。
使用文件输入流创建播放对象存在制约。在模拟器环境下调试发现,使用文件输入流创建播放的方式不适用于较大的音乐文件,否则将抛出“内存不足”(Out of Memory)的异常。在实机环境下,向手机上安装超过一定限制的Jar文件时,将会得到一个“安装包过大而无法安装”的提示。该限制额度可能因各款手机的性能不同而存在一定差异,但是这个值应该小于一个常规mp3文件的大小(3,4MB)。图7-7就是在将一个文件大小约为2MB的Jar文件安装到手机上时遇到的错误提示。
图7-7 安装Jar文件的错误提示
使用输入流创建音乐播放对象适用于较小的音乐文件,这也是J2ME教程中常用的通过Class类的getResourceAsStream方法在资源中获取文件流来创建播放对象的方式。
提示 使用文件输入流来创建播放器对象~存在一次性将媒体文件进行载入的动作~当媒体文件较大时~就容易出现内存无法装载文件内容~而抛出“内存不足”的异常。而使用URI方式来创建播放器对象并不存在一次性载入文件的动作~而是按照媒体文件的时间轴来“源源不断”地分片读取文件内容~给内存造成的压力也就小得多。
3(应用软件的提示音设置
在实机上运行该程序,在播放音乐列表时会显示图7-8所示的异常信息。但是通过系统自带的播放器进行播放是没有问题的。初步怀疑是音乐文件的读取权限问题,但是即使设置了程序的读取权限该问题依旧存在。经过比较分析,发现问题在应用软件的提示音设置。
该异常之所以提示“不允许”,是因为笔者在手机的功能设置中设置应用软件的提示音为关闭。当设置应用软件的提示音为开的状态后该异常不再出现。图7-9是在NOKIA 5310实机上设置软件提示音开关的实例图。
图7-8 音乐播放异常
图7-9 应用软件的提示音设置
以上的这些“机关”,正是笔者在本节开头所说的仅仅在WTK模拟器上写一个简单的播放器是很简单的,而要在实机上写一款能用的音乐播放器却比较困难的原因