第 9 章 木琴

优质
小牛编辑
134浏览
2023-12-01

很难相信,利用技术来记录和播放音乐只能追溯到1878年,也就是爱迪生获得留声机专利时。时至今日,我们已经有了长足的进步,音乐合成器、光盘、采样和混音、播放音乐的手机,甚至是拥塞的远程互联网。在本章中,通过创建一个可以录制和播放音乐的木琴应用,你也将成为这种进步的推动者。

{%}

作品描述

如图9-1所示,这个应用(最初由App Inventor团队的Liz Looney创建)可以做到:

{%}

图 9-1 木琴应用的用户界面

  • 通过触摸屏幕上的彩色按钮播放八个不同的音符;

  • 按“播放”按钮,回放之前弹奏的音符;

  • 按“重置”按钮清除之前弹过的音符,以便输入新曲。

学习要点

本章程涵盖了以下概念:

  • 使用单一的声音组件来播放不同的音频文件;

  • 使用Clock组件来计算并实现两个音符之间的延迟;

  • 在创建一个过程时做判断;

  • 创建能够自我调用的过程;

  • 列表的高级应用,包括添加、删除及读取项。

准备开始

登陆App Inventor网站,创建新项目“Xylophone”,屏幕标题设置为“木琴”,并连接到测试手机或模拟器。

设计组件

应用中有13个不同的组件(其中8个Button组成了乐器键盘),见表9-1。要在编程之前一次性创建这么多组件,显得有些乏味,因此我们按功能将应用划分为若干部分,分步来创建组件,这需要在组件设计器与块编辑器之间反复切换,就像第五章“瓢虫快跑”应用中一样。

表9-1木琴应用的所有组件

组件类型

面板中分组

命名

作用

Button

User Interface

Button1

播放低音C

Button

User Interface

Button2

播放D

Button

User Interface

Button3

播放E

Button

User Interface

Button4

播放F

Button

User Interface

Button5

播放G

Button

User Interface

Button6

播放A

Button

User Interface

Button7

播放B

Button

User Interface

Button8

播放高音C

Sound

Media

Sound1

播放音符

Button

User Interface

PlayButton

回放曲子

Button

User Interface

ResetButton

清除保存的曲子,开始新曲

HorizontalArrangement

Layout

HorizontalArrangement1

放置“播放”及“重置”按钮

Clock

User Interface

Clock1

记录音符之间的延迟

创建键盘

用户界面中包含了从低音C到高音C的大调五声(七音符)音阶的八个音符键盘,本节将创建这样的音乐键盘。

创建第一个音符按钮

首先创建前两个木琴键,用按钮来实现:

1. 从面板(palette)的user interface组中拖出一个按钮,保留Button1的名称,我们希望它像木琴的键一样,是一个洋红色(Magenta)的长条,因此做如下设置:

  • BackgroundColor属性:为洋红色(Magenta);

  • Text属性:为“C”;

  • Width属性:为“Fill parent”,使其占满屏幕;

  • Height属性:为40像素。

2. 重复上述步骤创建第二个按钮,名为Button2,放在Button1下面。Width及Height属性值同Button1,但BackgroundColor属性设为红色,Text属性设置为“D”。

(稍后将重复步骤2来创建其余六个音符按钮。)在组件设计器中看起来如图9-2所示。

{%}

图 9-2 用按钮来充当音符按键

在手机上的显示看起来与此相似,只是两个彩色按钮之间没有空白。

添加Sound组件

我们不能让木琴没有声音,创建一个Sound组件,名字为Sound1。MinimumInterval(最小间隔)属性设置为0(默认值为500毫秒)。这可以让我们的演奏要多快有多快,而不必等半秒钟(500毫秒)。不必设置Source属性,稍后我们会在块编辑器中设置。

下载1.wav和2.wav,并加载到项目中。与前几章不同,这里的声音文件必须保持原有文件名,不能修改,理由稍后就会明晰。后面还有六个声音文件需要加载。

声音与按钮的连接

当某个按钮被点击时,用程序来实现播放声音的行为,即:如果Button1被点击,则播放1.wav,如果Button2被点击,播放2.wav,等等。切换到块编辑器,如图9-3所示,进行以下设置:

1. 从Screen1项下的Button1抽屉里拖出Button1.Click块;

2. 从Sound1抽屉里拖set Sound1.Source块,放置在Button1.Click块中;

3. 输入“text”来创建一个文本块(而不是从Built-in项下的Text抽屉里拖出,这样更便捷。)设置文本值为“1.wav”,并与Sound1.Source块连接;

4. 添加Sound1.Play块。

{%}

图 9-3 点击按钮时播放声音

对Button2进行同样设置,如图9-4(只改了文件名),代码几乎完全重复。

{%}

图 9-4 添加更多的声音

重复的代码提示我们最好是创建一个过程,像在第3章“打地鼠”和第5章“瓢虫快跑”中那样。具体来说,我们将创建一个带数字参数的过程,将Sound1的Source属性设置为相应的声音文件,并播放该声音文件。这是对程序进行重构改进而又不改变程序行为的又一个例子,这一概念在“打地鼠”一章中首次引入。用join块将数字(如1)与文本“.wav”连接起来,创造出正规的文件名(如“1.wav”)。下面是创建这个过程的步骤:

1. 在块编辑器中打开Procedures抽屉,拖出“to procedure”块;

2. 单击procedure将过程名改为playNote;

3. 点击procedure块左上角的蓝色方块呼出内部组件,将一个input x块插入“inputs”块;

4. 将input x块中的x改为number;

5. 将set Sound1.Source to块从Button1.Click事件处理程序中拖出,放在PlayNote过程内“do”的右边,Sound1.Play块也将随之移动;

6. 将1.wav块拖入垃圾桶;

7. 从Text抽屉中拖出join块放到set Sound1.Source to的插槽内;

8. 将鼠标悬停在playNote的number参数上,呼出并拖动get number块,并将其放入join块的第一个插槽中;

9. 从Text抽屉中拖出空文本块,放在join块的第二个插槽中;

##将文本值设置为“.wav”。(切记不要输入引号);

##从Procedures抽屉中拖出call PlayNote块,放到空的Button1.Click内;

##在number插槽中插入文本“1”。

现在,当Button1被点击时,过程PlayNote将以数字1为参数被调用。该过程将Sound1.Source属性设为“1.wav”,并播放该声音。

创建一个Button2.Click块,调用参数为2的PlayNote过程。(可以复制现有的PlayNote块,将其移动到Button2.Click块内,并将参数更改为2;也可以复制整个Button1.Click块,然后将Button1改为Button2,再将参数1改为2。)程序如图9-5所示。

{%}

图 9-5 创建一个过程来演奏音符

告诉Android加载声音

此时在手机上测试程序会让你失望:第一次按键时,不但没听到预想的声音,手机还弹出错误提示:“Error 703:Unable to play 1.wav”(不能播放1.wav);第二次再按同一个键时,才听到声音。这是因为Android系统是在程序运行时才加载声音文件(只需加载一次),加载过程需要一点时间。第一次按键,当call Sound1 play块开始执行时,set Sound1.Source to块的加载任务尚未完成,因此系统给出错误提示;等到第二次按键时,声音文件已经加载完成,因此可以正常播放。为什么前几章没有出现过这个问题?因为我们在组件设计器中预先设置了Sound组件的Source属性为某个声音文件,当程序启动时,声音文件会自动加载。而这里,直到程序启动之后,我们也没有对Sound1.Source进行设置,因此没有对声音做初始化。我们必须在程序启动时直接加载声音文件,如图9-6所示。

{%}

图 9-6 在应用启动时加载声音文件

 测试:在手机中重新启动应用,按键之后立刻播放声音。(如果你没有听到声音,请确保手机上的媒体音量没有被设置为静音。)

实现其余的音符

两个按钮已经实现了演奏音符的功能,现在需要回到组件设计器,加载其余六个声音文件3.wav、4.wav、5.wav、6.wav、7.wav和8.wav,并添加其余六个音符。首先创建六个新Button组件,重复此前的步骤,但Text及backgroundColor属性的设置有所不同,具体设置如下:

  • Button3(“E” Pink / 粉红色)

  • Button4(“F”,Orange / 橙色)

  • Button5(“G”,Yellow / 黄色)

  • Button6(“A”,Green / 绿)

  • Button7(“B”,Cyan / 青色)

  • Button8(“C”,Blue / 蓝)

Button8的TextColor属性需要改为白色,这样更加醒目,如图9-7所示。

{%}

图 9-7 在组件设计器中放置其余的声音按钮

回到块编辑器中,为每个新按钮创建Click块并以相应的参数调用PlayNote过程。同样,在Screen.Initialize中加载新的声音文件,如图9-8所示。

{%}

图 9-8 对按钮单击事件编程,使得键盘与音调相对应

 测试:现在所有按钮都已经就绪,点击不同按钮会演奏不同的音符。

记录并回放音符

用按键来弹奏音符的确有趣,但如果能录制并播放歌曲岂不更好。为了实现回放功能,需要记录弹奏的音符并加以保存。除了要记录弹奏的音高(声音文件),还要记录两个音符之间的时间长度,否则将无法表现两个连续快弹音符与两个间隔10秒的音符之间的差别。

我们需要维护两个列表,每弹奏一个音符,两个列表中都会各自添加一条记录:

  • notes:包含与演奏的音符相对应的声音文件名,按照演奏顺序排列;

  • times:记录音符演奏时的时间点。

 提示:在继续之前,不妨复习一下在“总统测验”中所学到的关于列表的知识。

我们可以从Clock组件中得到计时信息,因此也可以用来正确地设定音符的回放速度。

添加组件

在设计器中添加一个Clock组件及“播放”和“重置”按钮,按钮放在HorizontalArrangement中:

1. 拖入一个Clock组件,它将出现在“不可见组件”区域,取消勾选TimerEnabled属性,因为我们希望在回放期间,计时器听从我们的调遣,适时地启动并完成计时;

2. 从layout组中拖出一个HorizontalArrangement组件放在按钮下面,Width属性设为“Fill parent”;

3. 从User Interface组中拖动一个按钮,改名为PlayButton,Text属性设为“播放”;

4. 拖出另一个按钮并放在PlayButton右侧,改名为ResetButton,Text属性设为“重置”。

图9-9中显示了应用在设计视图中的外观。

{%}

图 9-9 记录并回放声音的组件被添加到设计器中

记录音符及时间

回到块编辑器中,为组件添加正确的行为。我们需要维护两个列表:notes与times,每次用户按下一个按钮,就向列表中添加一项:

1. 从Variables抽屉中拖出一个initialize global name to块来定义一个新的变量;

2. 单击“name”将变量命名为“notes”;

3. 打开Lists抽屉,拖动一个make a list块,将其放置在变量notes的插槽中;

这样就定义了一个名为“notes”的空列表。重复上述步骤定义另一个变量,命名为“times”。块的样子如图9-10所示。

{%}

图 9-10 设置变量来记录音符

块的功能

每演奏一个音符,需要保存两项数据:声音文件名(保存到notes列表),以及演奏瞬间的时刻(保存到times列表)。用Clock1.Now块来记录时刻,它返回当前时刻的时间值(例如,2011年3月12日上午8时33分14秒),精确到毫秒。这些数据可以通过Sound1.Source和Clock1.Now块获得,将分别被添加到notes及times列表中,如图9-11所示。

{%}

图 9-11 将演奏的声音添加到列表中

例如,如果你演奏“哆来咪哆咪哆咪”[CDECECE],你的列表中最终会有七条记录,可能是:

  • notes:1.wav,2.wav,3.wav,1.wav,3.wav ,1.wav,3.wav

  • times[日期省略]:12:00:01,12:00:03,12:00:04,12:00:05,12:00:06,12:00:07,12:00:08

当用户按下“重置”按钮时,我们希望清空这两个列表。由于用户看不到清空带来的任何变化,因此添加一个Sound1.Vibrate块,通过振动来告知用户按键生效了,这种设置对用户来说是非常友好的。图9-12显示了这一功能用到的块。

{%}

图 9-12 为用户的“重置”操作提供反馈

音符的回放

作为一个思想实验,先来考虑如何实现音符的回放,而暂时忽略回放速度。我们可以(但不会)通过创建图9-13中的那块来实现这个暂时的目标:

  • 变量count用来跟踪notes列表中当前正在播放的音符的索引(位置);

  • 新过程 PlayBackNote,用来播放当前音符,并移动到下一个音符;

  • 编写PlayButton.Click事件处理程序,设置count为1,只要列表中有保存的音符,就调用PlayBackNote。

{%}

图 9-13 回放被记录下来的音符

块的功能

这可能是你第一次看到能自我调用的过程。这件事乍一看好像不可能,但实际上这是计算机科学中一个非常重要的概念:强大的递归。

为了更好地了解递归的工作原理,我们来一步一步地探究,当用户演奏了三个音符( 1.wav、 3.wav和6.wav),然后按下“播放”按钮时,都发生了什么。PlayButton.Click首先判断列表中是否保存了音符:由于notes列表长度3>0,列表不空,因此设定count等于1,并调用PlayBackNote:

1. 在第一次调用PlayBackNote时,count= 1:

  • Sound1.Source被设置为在notes中的第1项,即1.wav;

  • 调用Sound1.Play,播放1.wav;

  • 由于count值(1)小于notes的长度(3),因此count递增为2,并再次调用PlayBackNote;

2. 第二次调用PlayBackNote时,count=2:

  • Sound1.Source被设置为notes中的第2项,即3.wav;

  • 调用Sound1.Play,播放3.wav;

  • 由于count(2)小于notes的长度(3),因此count递增为3,并再次调用PlayBackNote;

3. 第三次调用PlayBackNote时,count=3:

  • Sound1.Source被设置为notes中的第3项,即6.wav;

  • 调用Sound1.Play,播放6.wav;

  • 由于count(3)不小于notes的长度(3),因此跳出if块,回放结束。

 提示:虽然递归功能强大,但运用起来存在危险。来做一个思想实验:问问自己,如果程序员忘了在PlayBackNote块中插入count递增的块,会发生什么事情。

这里的递归是正确的,但这个例子中还有另一个问题:在两次调用Sound1.Play之间几乎没有时间间隔(程序运行的速度非常快),因此每个音符都被下一个音符截断,除了最后一个。所有音符(除了最后一个)都等不到播放完,Sound1的source属性就已经被改写为下一个音符,并由Sound1.Play播放出来。为了获得正确的行为,需要在两次调用PlayBackNote之间添加延迟功能。

播放适当延迟的音符

延迟的设定与两个音符之间的时间差有关,我们用clock来为这个时间差计时。例如,如果时间差为3,000毫秒(3秒),则将Clock1.TimerInterval设置为3000,并启动计时器;在计时结束时再调用PlayBackNote。对PlayBackNote的if块做出修改,如图9-14所示。创建Clock1.Timer事件并编写事件处理程序,来说明计时结束时将发生的事情。

{%}

图 9-14 在音符之间加入延迟

块的功能

现在假设两个列表中记录了以下内容:

  • notes:1.wav,3.wav,6.wav

  • times:12:00:00,12:00:01,12:00:04

如图9-14所示,在PlayButton.Click中设置count为1,并调用PlayBackNote。

1. 第一次调用PlayBackNote时,count= 1:

  • Sound1.Source被设置为notes中的第1项,即“1.wav”;

  • 调用Sound1.Play播放1.wav;

  • 因为count(1)小于notes的长度(3),于是Clock1.TimerInterval被设置为times列表中的第1项(12:00:00)与第2项(12:00:01)之间的时间差:1秒。Count递增到2,启用Clock1.Timer并开始计时;

Clock1.Timer开始计时,间隔1秒之后,计时结束,定时器暂时禁用,并调用PlayBackNote。

2. 第二次调用PlayBackNote时,count= 2 :

  • Sound1.Source被设置为notes中的第2项,即“3.wav”;

  • 调用Sound1.Play播放3.wav;

  • 因为count(2)小于notes的长度(3),于是Clock1.TimerInterval被设置为times列表中的第2项(12:00:01)与第3项(12:00:04)之间的时间差:3秒。Count递增到3,启用Clock1.Timer并开始计时;

Clock1.Timer计时开始,间隔3秒之后,定时器暂时禁用,并调用PlayBackNote。

3. 第三次调用PlayBackNote时,count= 3 :

  • Sound1.Source被设置为notes中的第3项,即“6.wav”;

  • 调用Sound1.Play来播放6.wav;

  • 由于count(3)不小于notes的长度(3),跳出if块,回放完成。

改进

下面是一些可供探讨的备选方案:

  • 目前,在回放过程中,没有对用户点击ResetButton做任何限制,这将导致程序的崩溃(错误提示:select list item: Attempt to get item number 4 of a list of lengh 0。)(你知道原因吗?)修改PlayButton.Click,让ResetButton在回放期间禁用,回放完成后再重新启用。将PlayBackNote中的if块改为ifelse块,并在“else”中重新启用ResetButton。

  • 类似问题也发生在PlayButton上,用户可以在回放过程中再次点击该按钮。(想象一下会发生什么。) 在PlayButton.Click中禁用PlayButton,并将其Text属性改为“播放中...... ”,并像ResetButton一样,在PlayBackNote的ifelse块中重新启用该按钮,并重置Text属性。

  • 添加一个按钮来显示一首歌曲的名字,如“致爱丽丝”。当用户单击时,向notes及times列表中填写相应的值,将count设定为1,并调用PlayBackNote。有一个非常有用的块Clock1.MakeInstantFromMillis(用毫秒设置时间间隔),可以用来设定音符之间的延迟。

  • 如果用户按下一个音符,然后去做别的事情了,几小时后回来,又按下另一个音符,尽管音符可能属于同一首歌,但这绝不是用户的意图。有两种方法可以改进程序:(1)在一个合理的时间间隔后,停止记录音符,如1分钟;(2)通过对Clock1.TimerInterval使用Math抽屉中的max块,来限制音符播放的时长。

  • 通过改变按钮的外观,如Text、BackgroundColor或ForegroundColor属性,来形象地提示当前正在播放的音符。

小结

以下是本章涵盖的概念:

  • 通过修改Sound组件的Source属性,可以用一个而非八个Sound组件来播放不同音频文件。记住要在应用初始化时加载声音文件,以免运行时加载所引起的问题。(见图9-6);

  • 列表(Lists)可以为程序提供存储功能,可以在列表中保存用户的操作记录,并在以后对存储内容进行提取和再处理。我们使用这个功能来录制及播放歌曲;

  • Clock组件可以用来确定当前时间,两个时间值只差为我们提供了两个事件之间的时间间隔;

  • Clock组件的TimerInterval属性可以在程序中设置,就像我们设置两个音符之间的时间间隔一样;

  • 编写一个能自我调用的过程不仅是可能的,有时也是必要的。这种强大的技术称为递归。在编写递归过程时,一定要确保为程序的退出设定一个基本条件,它的重要性远大于为自我调用设定条件,否则程序将陷入无限循环。

资源下载

1.wav

2.wav

3.wav

4.wav

5.wav

6.wav

7.wav

8.wav