声音功能
下载文件:test.wav(storm注:经测试已坏,请自己找可用的wav文件代替)
博士,上一讲我们学习了N840Java开发环境的一些知识,这一讲要讲新的东西了吧? | |
恩,这一讲就来讲N840被加强的声音功能吧。 | |
声音播放功能以前的手机就实现了,还有什么新功能么? | |
MIDP2.0新增加了WAV播放和TONE。此外,声音的播放方法也不同了。首先,咱们先来了解一下WAV文件的播放。 |
1. WAV文件的播放
所谓的WAV,指Windows标准的声音文件格式。Windows启动时的声音就是以WAV格式保存的。 | |
那电脑中的WAV文件也可以在手机上面播放么? | |
恩。但是,N840可播放的WAV文件要限定在sampling频率8KHz、8比特、monoPCM(单耳非压缩)。Windows可以播放的声音,放到手机上不一定能播放出来。 | |
恩。真麻烦啊。那手机上能播放的声音数据应该怎么来准备呢? | |
市面上卖的音乐编辑软件就可以,网上也有许多on-line软件可以转换文件的形式的。另外,使用Windows自带的录音机([程序]→[附件]→[娱乐]→[录音机])也可以保存为WAV的形式。不过在保存时一定要按照刚才所说的进行频率等的设定。 | |
原来如此啊。这样,就可以将自己喜欢的音乐做成WAV形式了。 | |
还有一点,注意文件的尺寸不能超过128KB,太大的话就无效了。 下面我们就来制作播放WAV的Java程序吧。 先启动KtoolBar选择制作“WAVTest”程序。类名也同样设为WAVTest。 |
接下来介绍一下播放WAV文件的简单用例。下面的代码有“Play”和“Exit” 两个command,使用“Play”command来播放音乐。 |
WAVTest.java
1 | import java.io.*; | ||
2 | import javax.microedition.lcdui.*; | ||
3 | import javax.microedition.media.*; | ||
4 | import javax.microedition.midlet.*; | ||
5 | |||
6 | public class WAVTest extends MIDlet implements CommandListener { | ||
7 | private final String WAV_FILE = "test.wav"; // 播放的WAV文件名 | ||
8 | private Display display; | ||
9 | private Form mainForm; | ||
10 | private Command playCommand; | ||
11 | |||
12 | public WAVTest() { | ||
13 | mainForm = new Form("WAVTest"); | ||
14 | playCommand = new Command("Play", Command.SCREEN, 0); | ||
15 | mainForm.addCommand(playCommand); | ||
16 | mainForm.addCommand(new Command("Exit", Command.EXIT, 0)); | ||
17 | mainForm.setCommandListener(this); | ||
18 | display = Display.getDisplay(this); | ||
19 | } | ||
20 | |||
21 | public void startApp() { | ||
22 | display.setCurrent(mainForm); | ||
23 | } | ||
24 | |||
25 | public void pauseApp() {} | ||
26 | |||
27 | public void destroyApp(boolean unconditional) {} | ||
28 | |||
29 | /** | ||
30 | * command按键被按下时的处理 | ||
31 | */ | ||
32 | public void commandAction(Command c, Displayable d) { | ||
33 | if (c.getCommandType() == Command.EXIT) { | ||
34 | notifyDestroyed(); | ||
35 | } else if (c == playCommand) { | ||
36 | playSound(); | ||
37 | } | ||
38 | } | ||
39 | |||
40 | /** | ||
41 | * 声音的播放 | ||
42 | */ | ||
43 | private void playSound() { | ||
44 | try { | ||
45 | // 从WAV文件取得InputStream | ||
46 | InputStream in = getClass().getResourceAsStream(WAV_FILE); | ||
47 | |||
48 | // 生成Player对象 | ||
49 | Player p = Manager.createPlayer(in, "audio/x-wav"); | ||
50 | |||
51 | // 声音的播放 | ||
52 | p.start(); | ||
53 | } | ||
54 | catch (Exception e) { // 发生异常时的处理 | ||
55 | // 生成Alert对象 | ||
56 | Alert alert = new Alert("Exception", e.toString(), null, AlertType.ERROR); | ||
57 | |||
58 | // 显示异常的内容 | ||
59 | display.setCurrent(alert, mainForm); | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
首先,来实际运行一下上面的程序,然后再对代码的内容进行说明。 | |
好的。代码是保存在程序中的src文件夹中的,那么,WAV数据应该保存在哪里呢? | |
声音数据保存在resource文件夹中。在程序文件夹中,有以res命名的文件夹,保存在里面就可以了。这次播放的WAV用例的名称为“test.wav”,因此就以“test.wav”的名称保存。 | |
是,我明白了。准备好文件之后,点击“Build”-“Run”是吧? | |
恩,没错。记得很清楚啊,如果忘记了就复习一下前讲的内容吧。 |
接下来,我们就来分析代码。 这个代码比较简单,尼克你能看明白吧。 | |
是的。在WAVTest()这个constructor中,添加了“Play”和“Exit”命令。启动命令按键时的处理是,启动“Play”命令时,执行第43行的playSound()方法。 | |
没错。playSound()方法里,包含了播放WAV文件的程序的main部分。其中,下面三行读取和播放WAV文件。 |
InputStream in = getClass().getResourceAsStream(WAV_FILE); Player p = Manager.createPlayer(in, "audio/x-wav"); p.start(); |
首先,第一行取得了用于从WAV文件读取数据的InputStream对象。在getResourceAsStream方法的参量里,指定了文件名。 | |
文件名WAV_FILE,在程序的第七行如下所示定义。 private final String WAV_FILE = "test.wav"; | |
对。变更文件名时,可以在test.wav的位置输入喜欢的名称。 接下来,第二行使用Manager类的createPlayer方法,生成Player对象。在参量里,指定InputStream对象和显示内容类型的字符串。 | |
什么是内容类型呢? | |
简单的说,就是文件内包含的数据的种类。这次讲的是WAV形式的声音文件,因此内容类型就是“audio/x-wav”。 | |
可以播放WAV格式以外的文件么? | |
恩,N840可以播放下面格式的声音。 |
文件类型 | 文件的扩展名 | 内容类型 |
SMF, SP-MIDI | .mid | audio/midi audio/x-midi audio/sp-midi |
8bit, 8KHz, mono PCM | .wav | audio/x-wav |
Tone sequence | .jts | audio/x-tone-seq |
我们现在讲的是WAV形式的例子,如果是MIDI形式或者TONE序列文件,只需要改一下显示内容类型的字符串,就可以同样生成Player对象了。 | |
这样啊,不同文件形式也可以使用同样的记录方法,真方便啊。 | |
生成Player对象后,使用Player类的start方法就可以播放声音了。 | |
try{ }里面包含了从取得InputStream到播放的代码,这是为什么啊? | |
恩,因为声音播放可能会出现问题。例如,声音文件没有被放在resource里面,或者文件形式是手机不能播放的,或者尺寸太大等,可能会发生各种各样的问题。当这些问题发生时,就可以运行try{ }后面的catch(Exception e) { }中的命令来解决。 | |
Catch后面的{ }包含的命令是下面这两个吧。 |
Alert alert = new Alert("Exception", e.toString(), null, AlertType.ERROR); display.setCurrent(alert, mainForm); |
恩,利用上面的命令,就可以在手机的画面上面显示问题发生的原因了。开发时设置这样的命令真是很方便啊。 | |
是啊是啊。这样WAVTest.java的全部内容我都明白了。 |
2. TONE的播放
MIDP2.0新增了播放TONE的功能。下面我们就来试一下这个功能吧。 | |
与WAV的播放不一样么? | |
与以前学的,事先准备声音数据才能播放的文件不同,TONE的播放是指定的声音只能在指定的时间播放。因此,可以在程序中生成melody或者一些细微的控制。 | |
原来是这样啊。 | |
下面我们就来看使用TONE的例子。ToneTest类的代码和刚才讲的差不多,只是播放声音的playSound方法的内容有些差异。 |
ToneTest.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.media.*; | ||
3 | import javax.microedition.midlet.*; | ||
4 | |||
5 | public class ToneTest extends MIDlet implements CommandListener { | ||
6 | private Display display; | ||
7 | private Form mainForm; | ||
8 | private Command playCommand; | ||
9 | |||
10 | public ToneTest() { | ||
11 | mainForm = new Form("ToneTest"); | ||
12 | playCommand = new Command("Play", Command.SCREEN, 0); | ||
13 | mainForm.addCommand(playCommand); | ||
14 | mainForm.addCommand(new Command("Exit", Command.EXIT, 0)); | ||
15 | mainForm.setCommandListener(this); | ||
16 | display = Display.getDisplay(this); | ||
17 | } | ||
18 | |||
19 | public void startApp() { | ||
20 | display.setCurrent(mainForm); | ||
21 | } | ||
22 | |||
23 | public void pauseApp() {} | ||
24 | |||
25 | public void destroyApp(boolean unconditional) {} | ||
26 | |||
27 | /** | ||
28 | * command按键被按下时的处理 | ||
29 | */ | ||
30 | public void commandAction(Command c, Displayable d) { | ||
31 | if (c.getCommandType() == Command.EXIT) { | ||
32 | notifyDestroyed(); | ||
33 | } else if (c == playCommand) { | ||
34 | playSound(); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * 声音的播放 | ||
40 | */ | ||
41 | private void playSound() { | ||
42 | try { | ||
43 | Manager.playTone(60, 500, 100); | ||
44 | } | ||
45 | catch (Exception e) { // 发生异常时的处理 | ||
46 | // Alert对象的生成 | ||
47 | Alert alert = new Alert("Exception", e.toString(), null, AlertType.ERROR); | ||
48 | |||
49 | // 显示异常的内容 | ||
50 | display.setCurrent(alert, mainForm); | ||
51 | } | ||
52 | } | ||
53 | } | ||
制作新程序、编译、运行这些都没问题了吧? | |
是的,都没有问题啦。 |
与之前播放WAV文件的程序代码没什么区别啊。第43行播放声音没错吧。Manager.playTone(60, 500, 100); | |
恩,没错没错。Manager类的playTone方法可以播放声音。 | |
PlayTone里面用参量指定了三个数字(60, 500, 100),是什么意思啊? | |
恩。这个问题有点复杂,咱们在后面再讲吧。其中第一个参量是用来规定声音的note number的,也就是指定声音的值在0~127范围内。 | |
0是低音127是高音么? | |
对,没错。第二个参量指定声音播放的时间为毫秒单位,第三个参量指定声音的大小值在0~100的范围内。 | |
这样啊。那么刚才代码中指定的note number表示,高低为60的声音在0.5秒中间以100的大小被播放出来是吧。 | |
是的。 | |
如果要像钢琴那样,奏出do·ri·mi·fa·so·la·xi·do这样的音阶的话应该怎么做呢? | |
这个啊,那我们就先撇开Java的话题,来讲一下音程吧。 首先,所谓的音程是与作为标准的音相比,声音高低相差的程度。 playTone 方法里面的note number,一般用60表示“do”的音。音乐用语称为“Middle C”。 因此,刚才设参量为60,实际上是在手机上播放“do”的声音。 | |
这么说61,是表示do后面的音ri么? | |
不是不是,值增加1的话只增加了半个音。也就是说61表示do增加半个音到“do#”,音乐用语用“C#”表示。 下面是键盘的音阶分别对应的数字。圆圈内的数字表示对应的note number。 |
恩。do·ri·mi·fa·so·la·xi·do对应的数字就是60, 62, 64, 65, 67, 69, 71, 72吧? | |
没错。像这样,两音间的间隔为2, 2, 1, 2, 2, 2, 1排列起来被称为measure scale。钢琴发音是以C音调的measure scale为基础的。 | |
因此,将playSound方法的try模块的内容做如下替换,就可以播放do·ri·mi·fa·so·la·xi·do了吧。 |
Manager.playTone(60, 500, 100); Manager.playTone(62, 500, 100); Manager.playTone(64, 500, 100); Manager.playTone(65, 500, 100); Manager.playTone(67, 500, 100); Manager.playTone(69, 500, 100); Manager.playTone(71, 500, 100); Manager.playTone(72, 500, 100); |
恩,上面的代码写的很好啊。试试吧。 | |
咦,复数的音怎么同时响了?然后出现了下面的错误。 「javax.microedition.media.MediaException: can't play tone」 | |
这是由于playTone没有等一个声音播放完,就开始进行下面的处理了。因此,如果要播放下一个音,就必须等上一个音播放完毕。因为手机一次不能播放那么多的声音,所以就出现错误了。 | |
原来是这个原因啊。那么怎样才能等前一个音播放完毕呢? | |
这个并不难啊。使用Thread类的sleep方法就可以了。 在playTone后加入下面的一行就可以了。 |
Thread.currentThread().sleep(600); |
sleep 方法的参量为600,是指等待600毫秒么? | |
没错啊。 下面,尼克你就可以使用排列和for语句,实现你想要的短代码了。 |
int note[] = {60, 62, 64, 65, 67, 69, 71, 72}; for(int i = 0; i < note.length; i++) { Manager.playTone(note[i], 500, 100); Thread.currentThread().sleep(600); } |
是啊。将上面的代码放到playSound方法里面,就可以奏出do·ri·mi·fa·so·la·xi·do的音了。这样,就可以在手机上面演奏自己喜欢听的音阶了。 |
3. 利用按键操作演奏声音
对于Java程序来讲,从1到8的按键分别对应“1→do、2→re、3→mi、4→fa、5→so、6→la、7→xi、8→do”,下面就试着做一下通过按键操作演奏的程序吧。 给这个程序取名为TonePlayer,代码如下所示。由TonePlayer和TonePlayerCanvas两个类构成。 |
TonePlayer.java
1 | import javax.microedition.midlet.*; | ||
2 | import javax.microedition.lcdui.*; | ||
3 | import javax.microedition.media.*; | ||
4 | import java.io.*; | ||
5 | |||
6 | public class TonePlayer extends MIDlet implements CommandListener { | ||
7 | private Display display; | ||
8 | private Canvas mainCanvas; | ||
9 | |||
10 | public TonePlayer() { | ||
11 | display = Display.getDisplay(this); | ||
12 | mainCanvas = new TonePlayerCanvas(); | ||
13 | mainCanvas.addCommand(new Command("Exit", Command.EXIT, 0)); | ||
14 | mainCanvas.setCommandListener(this); | ||
15 | } | ||
16 | |||
17 | public void startApp() { | ||
18 | display.setCurrent(mainCanvas); | ||
19 | } | ||
20 | |||
21 | public void pauseApp() {} | ||
22 | |||
23 | public void destroyApp(boolean unconditional) {} | ||
24 | |||
25 | public void commandAction(Command c, Displayable s) { | ||
26 | if (c.getCommandType() == Command.EXIT) { | ||
27 | notifyDestroyed(); | ||
28 | } | ||
29 | } | ||
30 | } | ||
TonePlayerCanvas.java
1 | import javax.microedition.lcdui.*; | ||
2 | import javax.microedition.media.*; | ||
3 | |||
4 | public class TonePlayerCanvas extends Canvas { | ||
5 | final int note_diff[] = {0, 2, 4, 5, 7, 9, 11, 12}; | ||
6 | protected void paint(Graphics g) {} | ||
7 | |||
8 | /* | ||
9 | * 键被按下时的处理 | ||
10 | */ | ||
11 | protected void keyPressed(int keyCode) { | ||
12 | int key = keyCode - Canvas.KEY_NUM1; // (被按键号码)-1的值 | ||
13 | if(key < 0 || key >= note_diff.length) { | ||
14 | return; // 被按键不在1~8范围内不执行任何处理 | ||
15 | } | ||
16 | // 决定note number | ||
17 | int note = 60 + note_diff[key]; | ||
18 | try { | ||
19 | Manager.playTone(note, 500, 60); // 演奏Tone500毫秒 | ||
20 | Thread.currentThread().sleep(600); // 待机600毫秒 | ||
21 | } catch(Exception e) { } | ||
22 | } | ||
23 | } | ||
TonePlayerCanvas是执行键被按下时的处理的啊。 | |
是的。按键被按下后,执行keyPressed方法。在参量的keyCode里面,输入各按键对应的数值。 | |
执行一下看看吧,确实1到8的按键可以演奏doremi的音阶呢。好像在演奏乐器呢,呵呵。 | |
恩,试着弹首曲子玩玩吧。 |
4. TONE序列(sequence)
TONE 的播放不但可以一个音一个音的演奏,也可以演奏连续的音。现在咱们就来说明一下这个TONE序列的播放。 | |
播放声音的方法有这么多种啊。 | |
所谓TONE序列,是指将曲子的信息以byte型的排列保存。 将下面的代码,放到ToneTest.java的playSound方法try程序块中,执行一下试试吧。 |
TonePlayerCanvas.java
1 | /** | ||
2 | * 声音的播放 | ||
3 | */ | ||
4 | private void playSound() { | ||
5 | // tone sequence的定义 | ||
6 | byte[] toneSequence = { | ||
7 | ToneControl.VERSION, 1, // 指定版本号为1 | ||
8 | ToneControl.TEMPO, 8, // 指定tempo为8 | ||
9 | // 下面列举出note number与演奏时间的组合 | ||
10 | 60, 4, | ||
11 | 62, 4, | ||
12 | 64, 4, | ||
13 | 65, 4, | ||
14 | 67, 8, | ||
15 | 67, 8, | ||
16 | 67, 4, | ||
17 | 65, 4, | ||
18 | 64, 4, | ||
19 | 62, 4, | ||
20 | 60, 8, | ||
21 | 60, 8 | ||
22 | }; | ||
23 | |||
24 | try{ | ||
25 | Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR); | ||
26 | p.realize(); | ||
27 | ToneControl c = (ToneControl)p.getControl("ToneControl"); | ||
28 | c.setSequence(toneSequence); | ||
29 | p.start(); | ||
30 | } | ||
31 | catch (Exception e) { // 发生异常时的处理 | ||
32 | // Alert对象的生成 | ||
33 | Alert alert = new Alert("Exception", e.toString(), null, AlertType.ERROR); | ||
34 | |||
35 | // 显示异常内容 | ||
36 | display.setCurrent(alert, mainForm); | ||
37 | } | ||
38 | } | ||
旋律出来了。 | |
是啊。像编码中的toneSequence那样,可以用byte型值的排列,来定义曲子。可以把开始的ToneControl.VERSION..1和 ToneControl.TEMPO看成是固定的值。设定好tempo的值之后,将note number与演奏的时间排列成组,就可以演奏出喜欢听的旋律了。 | |
实际用来演奏声音的,是第29行Player对象的start方法吧。 | |
没错。不过之前一定要作成ToneControl对象,然后使用setSequence方法来设定序列。 | |
使用这个方法,就可以在程序中添加旋律,也可以根据游戏的情况随意变换啦。 | |
呵呵,这一讲关于声音的说明就先到这了。 下一讲的内容是,可以提高游戏开发效率的GameAPI。 |