■ 前言
在上一讲中我们讲述的是如何制作JAVA手机多媒体功能中的动画,本讲中我们将介绍如何制作JAVA手机的另一个多媒体功能——声音,即N800的音乐播放功能。若是在动画上再配以音乐播放功能,就能制作出具有丰富表现力的应用程序了。
■ 音乐播放
现在我们利用MIDP应用程序播放音乐。但是, MIDP标准API不支持声音播放功能。机种不同,则声音播放方法也不同,所以每个厂商都会使用他们各自扩展的API 。N800使用NEC扩展的API,所以能用MIDP应用程序播放声音。
■ N800的音乐播放功能
N800只能播放SMF(format 0)格式的音乐数据,最大文件尺寸为10Kbyte。SMF格式即Standard MIDI File 的缩略语,就是为了能实现互换性而设定的文件形式,互换性是指在不同的应用程序中也具有能处理数据的特性。SMF分为format0和format1两种格式。N800所采用的是format0格式。这两种格式具有以下不同特点。(表 1)
format0 | Midi的16频声音收录在1个磁道上的形式 |
format1 | 不限制磁道数量的形式 |
表 1
■ 播放音乐
接下来我们实际操作读取文件播放音乐的功能。
● 音乐数据的读取
利用扩展API上的Media类的static方法读取音乐数据。自变量中记述了音乐数据文件的通过。AudioClip audio = Media.getAudioClip(“/test.mid”);
另外,能够从web上获得音乐数,并且能够从RMS中得到音乐数据。但,由于形式相同,这里就不特别讲述了。详细情况请参考扩展API文档。
● 播放
播放读取的音乐数据。使用AudioClip例子(在这称为audio)play方法播放。
audio.play();
而且也能同时播放两个以上的音乐数据。此时,使用AudioClip例子(在这称为audio1、audio2)play方法播放。
audio1.play();
audio2.play();
上述情况下能够同时播放audio1,audio2。
● 停止
能够暂停音乐的播放。使用AudioClip例子的stop方法能够停止。
audio.stop();
● 其他功能
AudioClip定义了读取其他音乐数据信息的方法和决定反复播放次数的方法。(表 2)
getChannel() | 取得音乐数据的频数 |
getLapsedTime() | 以MS为单位取得所演奏的音乐数据的播放时间 |
getTempo() | 取得音乐数据的速度 |
getTime() | 取得音乐数据的播放时间 |
setLoopCount(int count) | 设定演奏的音乐数据的反复播放次数 |
表 2
■ 音频事件
音频演奏过程中,演奏开始时、停止时、结束时都会发生音频事件,能定义此时的处理。要定义音频事件发生时的处理,有必要安装AudioListener接口和记述audioAction方法内的处理。
然后,使用AudioClip对象的addAudioListener方法进入AudioListener。
public class AudioTest implements AudioListener {
/** * 构造函数 **/
public AudioTest() { AudioClip audio = Media.getAudioClip("/test.mid");// 读取音乐数据 audio.addAudioListener(this);// 注册audio事务监听器 }
/** * 音频事件的处理 **/
public void audioAction(AudioClip sound, int event, int param) { //记述处理 • • } } |
ex. 1
记述处理的audioAction方法的自变量如下所示。
AudioClip sound | 传递事件发生来源的对象 |
int event | 传递事件的种类 |
int param | 传递事件的参数。由于事件不同,则参数的意思也不同。不包含参数的事件的情况下,只传递0 |
表 3
此外,事件的种类(audioAction方法的自变量、事件)在AudioListener接口文件夹中定义如下。(表 4)
static int AUDIO_COMPLETE | 表示音乐播放结束 |
static int AUDIO_STARTED | 表示音乐播放开始 |
static int AUDIO_COMPLETE | 表示音乐播放停止 |
表 4
以下展示的是只播放音频数据的简单范例。
import javax.microedition.lcdui.Display; import javax.microedition.midlet.MIDlet; import javax.microedition.midlet.MIDletStateChangeException; /** * 音乐播放的简单范例 */ public class Audio extends MIDlet { Display display; AudioCanvas canvas; /** * 构造函数 */ public Audio() { canvas = new AudioCanvas(); display = Display.getDisplay(this); } protected void startApp() throws MIDletStateChangeException { display.setCurrent(canvas); } protected void pauseApp() {} protected void destroyApp(boolean arg0) throws MIDletStateChangeException {} } import javax.microedition.lcdui.*; import com.nec.media.*; /** * 音频 canvas **/ public class AudioCanvas extends Canvas implements Runnable, CommandListener, AudioListener { Command START = new Command("play", Command.OK, 0); Command STOP = new Command("stop", Command.OK, 0); AudioClip a;// 音乐数据 Thread th; /** * 构造函数 **/ public AudioCanvas() { a = Media.getAudioClip("/_test.mid");// 读取音乐数据 a.addAudioListener(this);// 注册 AudioListener this.addCommand(START); this.addCommand(STOP); this.setCommandListener(this); th = new Thread(this); th.start(); } /** * 音频事件的处理 */ public void audioAction(AudioClip sound, int event, int param) { if (sound == a) { if (event == AudioListener.AUDIO_COMPLETE) { sound.play(); } } } /** * 描绘处理 */ protected void paint(Graphics g) { g.setColor(255, 255, 255); g.fillRect(0, 0, getHeight(), getWidth()); g.setColor(0, 0, 0); g.drawString("Music Play?", 50, 52, Graphics.TOP | Graphics.LEFT); g.drawString( "channel=" + a.getChannel(), 30, 64, Graphics.TOP | Graphics.LEFT); g.drawString( "lapsed time=" + a.getLapsedTime(), 30, 76, Graphics.TOP | Graphics.LEFT); g.drawString( "tempo=" + a.getTempo(), 30, 88, Graphics.TOP | Graphics.LEFT); g.drawString( "time=" + a.getTime(), 30, 100, Graphics.TOP | Graphics.LEFT); } /** * 命令事件的处理 */ public void commandAction(Command c, Displayable d) { System.out.println("test"); if (c.equals(START)) { a.play(); } else if (c.equals(STOP)) { a.stop(); } } /** * 线程的处理 * 刷新查看 */ public void run() { while (true) { repaint(); try{ Thread.sleep(500); }catch(Exception e){ } } } } |
ex. 2
接受表示音乐播放结束的事件后,根据明确的开始播放音乐菜单可以无限循环地播放音乐。下面的演示详细记述了上述例子中的audioAction方法,能够实现无限循环播放。(ex. 3)
/** * 音频事件的处理 */ public void audioAction(AudioClip sound, int event, int param) { if (sound == a) { if (event == AudioListener.AUDIO_COMPLETE) { sound.play(); } } } |
ex. 3
制作应用程序
接下来制作实际的发声应用程序。
本讲中制作的是简单的“泡泡龙”游戏。
■ 游戏方法
移动小棒接住反弹的球使其不掉下去,使上方的彩球逐渐消失的游戏。彩球完全消失并清除后,球再落下则此游戏通过。
■ 准备工作
准备游戏必备的图片和音效。
准备以下图片。
图1 球的图片

图2 小棒的图片

图3 彩球的图片
· 背景音乐 ( bgm.mid )
· 球反弹时的音效 ( ball.mid )
· 彩球破碎时的音效 ( block.mid )
■ 设计
以下是状态连接图(図 4)
本讲中为了简单化,在启动应用程序的同时立刻就启动游戏。形成游戏开始、球落下后游戏结束、全部清除彩球后游戏过关。

Figure 4
■ 制作应用程序
现在我们按照以下顺序制作应用程序。
1.类结构
2.变量、常量的定义
3.查看图片和音效
4.使图片运动
5.球的反弹
6.音乐的播放
1.类结构
下表内容是类结构。(表 5)
BlockApplication | 泡泡龙游戏的MIDlet |
BlockCanvas | 泡泡龙游戏的Canvas |
表 5 2. 变量、常量的定义
将下面的应用变量、定量作为BlockCanvas例子的属性并定义。(ex. 4)
// 状态设定 private int state; // 状态 private final int ACTIVE = 1; private final int GAME_OVER = 2; private final int CLEAR = 3; // 彩球的设定 private final int BLOCK_H = 7; // 彩球横向的个数 private final int BLOCK_V = 5; // 彩球纵向的个数 private final int BLOCK_WIDTH = getWidth() / BLOCK_H; private final int BLOCK_HEIGHT = BLOCK_WIDTH / 2; private boolean block[][] = new boolean[BLOCK_H][BLOCK_V]; private int blockCount; // 彩球个数 // 小棒的设定 private final int BAR_HEIGHT = 11; private final int BAR_WIDTH = 23; private int barX = 0; private int barY = getHeight() - BAR_HEIGHT; private int barMovCode Example = 0; // 球的设定 private final int BALL_HEIGHT = 10; private final int BALL_WIDTH = 10; private int ballX; private int ballY; private int ballMovCode Example = 5; private int ballMoveY = 5; private Thread th; // 画面类 private Image barImg = null; private Image ballImg = null; private Image blockImg = null; // 音效类 private AudioClip bgm; // Background music private AudioClip ballSound; // Sound of bouncing ball private AudioClip blockSound; // Sound of destroying blocks |
ex. 4
3. 查看图片和音效
查看准备好的图片和音效。BlockCanvas的构造函数内分别读取小棒、球、彩球的图片。(ex. 5)
// 读取图片 try { barImg = Image.createImage("/bar.png"); ballImg = Image.createImage("/ball.png"); blockImg = Image.createImage("/block.png"); } catch (Code Exampleception e) { e.printStackTrace(); } |
ex. 5
能查看读取后的图片。
彩球在图中的分配为横7纵5,读取彩球图片并描画在画面中。用旗表示彩球的状态。保持原来的排列。将一个一个的彩球使用原来的排列并计算出坐标,安排在画面中。(ex. 6)
// 查看彩球 g.setColor(0, 0, 255); for (int i = 0; i < BLOCK_H; i++) { / for (int j = 0; j < BLOCK_V; j++) { if (block[i][j]) { g.drawImage( blockImg, i * BLOCK_WIDTH, (j + 1) * BLOCK_HEIGHT, Graphics.LEFT | Graphics.TOP); } } } |
ex. 6
接着查看球和小棒。在paint方法中有以下叙述。(ex. 7)
// 查看球 g.drawImage(ballImg, ballX, ballY, Graphics.LEFT | Graphics.TOP); // 查看小棒 g.drawImage(barImg, barX, barY, Graphics.LEFT | Graphics.TOP); |
ex. 7
图片安装完成后出现如下画面。

4. 使图片运动
接下来使用线程和按键事件移动球和小棒。为了使用线程就得在AudioCanvas类中安装Runnable接口、记述run方法。使用球和小棒定义的移动速度分别变化球和小棒的坐标。此外,小棒的移动速度根据按键处理而变化。以下记述了run方法 。 (ex. 8)
/** * 线程的运行处理 */ public void run() { while (state == ACTIVE) { moveBall();// 使球运动 moveBar();// 移动小棒 repaint();// 再次描画 try { Thread.sleep(50); } catch (InterruptedCode Exampleception e) { e.printStackTrace(); break; } } } /** // 使球运动 */ public void moveBall() { ballX += ballMovCode Example; ballY += ballMoveY; } /** * 移动小棒 */ public void moveBar() { barX += barMovCode Example; // 不能向画面外移动 if (barX < 0) { barX = 0; } else if (barX + BAR_WIDTH > getWidth()) { barX = getWidth() - BAR_WIDTH; } } |
ex. 8
以下表示的是按键处理。(ex. 9)
/***************************************** * 按键处理 *****************************************/ /** * 按按键时 */ protected void keyPressed(int key) { if (state == ACTIVE) {// 正在运动 if (getGameAction(key) == Canvas.RIGHT) { barMovCode Example = 6; } else if (getGameAction(key) == Canvas.LEFT) { barMovCode Example = -6; } repaint(); }else{// 停止运动后 // 再次启动 this.initialize(); } } /** * 释放按键时 */ protected void keyReleased(int key) { barMovCode Example = 0; } |
ex. 9
5. 球的反弹
下面是球的反弹。
球的反弹形式有以下3种。
· 碰边壁后反弹
· 碰小棒后反弹
· 碰彩球后反弹
记述了每个moveBall方法。(ex. 10)
碰彩球的反弹时
block[i][j] = false;
blockCount--;
彩球立刻就破碎。彩球破碎后余下的彩球数量blockCount将有所减少。
另外,球掉落时,改变游戏状态后游戏结束。
/** // 使球运动 */ public void moveBall() { ballX += ballMovCode Example; ballY += ballMoveY; // 反弹 // 碰边壁后反弹 if (ballX < 0) { ballMovCode Example *= -1; ballX = 0; } else if (getWidth() < ballX + BALL_HEIGHT) { ballX = getWidth() - BALL_HEIGHT; ballMovCode Example *= -1; } if (ballY < 0) { ballMoveY *= -1; ballY = 0; } else if (ballY > getHeight()) { // 球落下后 // 游戏结束 state = GAME_OVER; } // 碰上小棒后反弹 if (ballY + BALL_HEIGHT > barY && ballX + BALL_WIDTH > barX && ballX < barX + BAR_WIDTH) { ballMoveY *= -1; ballY = barY - BALL_HEIGHT; if (barMovCode Example < 0) { ballMovCode Example -= 2; } else if (barMovCode Example > 0) { ballMovCode Example += 2; } } // 碰上彩球后反弹 for (int i = 0; i < BLOCK_H; i++) { for (int j = 0; j < BLOCK_V; j++) { if (block[i][j]) { if (ballX + BALL_WIDTH > i * BLOCK_WIDTH && ballX < (i + 1) * BLOCK_WIDTH) { if (ballY + BALL_HEIGHT > (j + 1) * BLOCK_HEIGHT && ballY < (j + 2) * BLOCK_HEIGHT) { // 清除彩球 block[i][j] = false; blockCount--; ballMoveY *= -1; } } } } } } } |
ex. 10
碰小棒后反弹情况如下所示:向右按键时,球就会让右方快速移动,反之,向左按键时,球则向左方快速移动。(ex. 11)
if (barMoveX < 0) { ballMoveX -= 2; } else if (barMoveX > 0) { ballMoveX += 2; } |
ex. 11
· 清除检查
至此安装完毕游戏就有雏形了。但是,在现在的程序中即使彩球全部消失,游戏也不能清除。那么,球与彩球相撞时,数出彩球的剩余数。当该数值为0时,则游戏清除。以下记述的是该处理。(ex. 12)
// 清除彩球 block[i][j] = false; blockCount--; ballMoveY *= -1; // 播放音效 blockSound.play(); // 检查游戏清除 if (blockCount == 0) { state = CLEAR; } |
ex. 12
6. 音乐播放
在本讲中的泡泡龙游戏的应用程序中最好使用BGM和音效。游戏开始的同时演奏BGM,音效则是球在碰壁、碰小棒反弹时,以及彩球破碎时才播放的。
· 读取
用BlockApplication构造函数读取音乐数据。而且,这里的BGM能够循环播放,所以可以使用音频事件处理。(ex. 13)
//声音数据的读取 bgm = Media.getAudioClip("/bgm.mid");// 背景音乐 ballSound = Media.getAudioClip("/ball.mid");// 球反弹后的音效 blockSound = Media.getAudioClip("/block.mid");// 球破碎的音效 bgm.addAudioListener(this);// 增加 AudioListener |
ex. 13
· 播放
读取音乐数据后,接下来进行播放。BGM在游戏开始的同时能够播放,所以在AudioCanvas类的start方法中记述播放处理并能够播放出来。
球的音效:用moveBall方法进行下面反弹判断时,能够播放音效。(ex. 14)
// 反弹 // 碰边壁后反弹 if (ballX < 0) { ballMoveX *= -1; ballX = 0; // 播放音效 ballSound.play(); } else if (getWidth() < ballX + BALL_HEIGHT) { ballX = getWidth() - BALL_HEIGHT; ballMoveX *= -1; // 播放音效 ballSound.play(); } if (ballY < 0) { ballMoveY *= -1; ballY = 0; // 播放音效 ballSound.play(); } else if (ballY > getHeight()) { // 球落下后 // 游戏结束 state = GAME_OVER; } // 碰上小棒反弹 if (ballY + BALL_HEIGHT > barY && ballX + BALL_WIDTH > barX && ballX < barX + BAR_WIDTH) { ballMoveY *= -1; ballY = barY - BALL_HEIGHT; if (barMoveX < 0) { ballMoveX -= 2; } else if (barMoveX > 0) { ballMoveX += 2; } // 播放音效 ballSound.play(); } |
ex. 14
彩球的音效:用moveBall方法判定彩球的碰撞时,如下记述并能够播放。 (ex. 15)
// 碰上彩球后反弹 for (int i = 0; i < BLOCK_H; i++) { for (int j = 0; j < BLOCK_V; j++) { if (block[i][j]) { if (ballX + BALL_WIDTH > i * BLOCK_WIDTH && ballX < (i + 1) * BLOCK_WIDTH) { if (ballY + BALL_HEIGHT > (j + 1) * BLOCK_HEIGHT && ballY < (j + 2) * BLOCK_HEIGHT) { block[i][j] = false; ballMoveY *= -1; // 播放音效 blockSound.play(); } } } } } |
ex. 15
■ 完成
下面是实际制作的程序一式。 ( BlockApplication.zip )
运行结果如下所示。
总结
在本讲的讲解中能够自由播放音乐数据了。因此,能够制作成创造性的应用程序。但是,扩展应用程序时,不能保存高分、数据等 。在下讲我们将学习如何使用固定存储器保存数据的方法。
N820 问世 在本栏目中简单将N800和N800的后继机种N820进行一下比较。
■ N820的特点 - 256Kbyte存储空间
- 255x240(纵x横)和65535色的显示屏
- 查看png格式的画面文件
- 播放smf格式的音乐文件
- http通信,socket通信
- 逆光、双感光板控制
- Sprite功能
- ImageMap功能
- 各种各样的制图扩展功能
- 3D引擎
由于N820具有256Kbyte的较大存储空间,所以能制作容量稍大、自由度较高的手机应用程序。另外,也能制作对应http、socket通信的自由度较高网络应用程序。因此,也能够搭载3D引擎、3D描画。而且还能安装N800对应的Sprite功能、ImageMap功能的描画功能。
■ 与N800的比较 下表是N800和N820的比较。(表 6)
项目 | N800 | N820 | 显示屏尺寸 | 180x162(纵x横) | 255x240(纵x横) | JAD文件尺寸 | 最大 2 KB | 最大 2 KB | JAR文件尺寸 | 最大 50 KB | 最大 1 MB | RMS尺寸 | 最大 10 KB | 最大 10 KB | 记录存储数量 | 最大 3 records | 最大 3 records | 通信协议 | 只有http | HTTP and socket | 画像文件 | PNG | PNG | 音乐文件 | SMF(format 0) 最大10Kbyte | SMF(format0) 最大10Kbyte | 表 6 ■ NEC N820 Application模拟器 下面是模拟N820工作的模拟器,称为「NEC 820 Application Emulator」。与以前我们所介绍的「NEC N800 Application Emulator」在外观上没有什么区别。(図 5)
 图 5 打开模拟器,就是现在的手机画面表示。与N800相比,手机设计多少有些变化,手机的内显示屏变大了。下图是用N820 Application Emulator制作的“泡泡龙”游戏画面。“泡泡龙”游戏由于是假定在N800的屏幕上应用的。因此画面尺寸要比N820中的内屏尺寸稍小。因此,彩球之间存有空隙。(図 6)
 図 6 ■ 总结 N820的优点是具有256Kbyte的大容量存储空间,而且使用3D图表引擎、3D图表应用程序、能够制作成对应socket通信的TCP/IP网络应用程序。对于应用开发者而言,N820是一部制作JAVA应用程序非常有价值的终端。对于寻求高级机种的用户而言,应该是一部高精细画面、高功能的极大满足用户需要的终端。今后N820的用户应该会大幅度增加的。
|