开发体验

第9章 木琴

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

作品描述

如图9-1所示,这个应用(最初由App Inventor团队的Liz Looney{![Liz Looney是本书的合著者之一。她是谷歌的软件工程师,也是谷歌“机器人工作小组”(Robotics Task Force)的成员,作为App Inventor团队的初创成员,她领导了乐高头脑风暴机器人(LEGO MINDSTORMS)组件的开发工作。Liz Looney是一位卓越的软件工程师,有25年以上的从业经历,曾先后在Borland、Oracle及Google公司工作。——译者注]}创建)可以实现以下功能:

  • 触摸屏幕上的八个彩色按钮,可以播放八个不同的音符;
  • 按播放按钮,可以回放之前弹奏的音符;
  • 按重置按钮清除之前弹过的音符,以便输入新曲。
图9-1 “木琴”应用的用户界面

学习要点

本章涵盖了以下内容:

  • 使用单一的声音组件来播放不同的音频文件;
  • 使用计时器组件来计算并实现两个音符之间的延迟;
  • 在自定义的过程里使用条件判断;
  • 创建能够自我调用的过程;
  • 列表的高级应用,包括添加、选取及删除列表项。

准备开始

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

设计组件

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

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

组建类型 所属类别 名称 作用
按钮 用户界面 按钮1 弹奏低音C
按钮 用户界面 按钮2 弹奏D
按钮 用户界面 按钮3 弹奏E
按钮 用户界面 按钮4 弹奏F
按钮 用户界面 按钮5 弹奏G
按钮 用户界面 按钮6 弹奏A
按钮 用户界面 按钮7 弹奏B
按钮 用户界面 按钮8 弹奏高音C
音频播放器 多媒体 音频播放器1 播放音符或曲子
按钮 用户界面 播放按钮 回放曲子
按钮 用户界面 重置按钮 清除保存的曲子,开始创作新曲
水平布局 组局布局 水平布局1 放置播放及重置按钮
计时器 传感器 计时器1 记录两个音符之间的延时

创建键盘

用户界面中包含了八个音符键盘,音高从低音C到高音C。本节将创建这样的音乐键盘。

创建两个音符按钮

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

  1. 从用户界面分组中拖出一个按钮,放在预览窗口中,保留按钮的默认名称“按钮1”。为了让它的外观更接近于木琴的按键(品红色的长条),特做如下设置。

    • 背景颜色:品红色;
    • 显示文本:“C”;
    • 宽度:充满,使其在水平方向上占满屏幕;
    • 高度:40像素。
  2. 重复上述步骤创建第二个按钮,名称为按钮2,放在按钮1下面。宽度及高度属性与按钮1相同,但背景颜色设为红色,显示文本设为“D”。{![建议将C、D的字体颜色设为白色以便于识别。——译者注]}

(稍后将重复步骤2来创建其余六个音符按钮。)

此时的设计视图如图9-2所示。

图9-2 用按钮创建两个音符键盘

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

添加音效播放器组件

我们不能让木琴没有声音,所以要创建一个音效播放器组件,名字为音效播放器1。最小间隔属性设置为0(默认值为500毫秒)。这可以让我们的演奏要多快有多快,而不必在两个音符之间等半秒钟(500毫秒)。不必设置源文件属性,稍后我们会在编程视图中设置。

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

在按钮与声音之间建立关联

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

  1. 从按钮1抽屉里拖出“当按钮1被点击时”块。
  2. 从音效播放器1抽屉里拖出“设音效播放器1的源文件为”块,放在“当按钮1被点击时”块中。
  3. 在空白处输入“文本”两个字,可以创建一个空文本块。(比起从内置块分组中打开文本抽屉拖出文本块,这样更快捷。)设置文本值为“1.wav”,并将其填充在“设音效播放器1的源文件为”块的插槽中。
  4. 添加“让音效播放器1播放”块。
图9-3 点击按钮时播放声音

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

图9-4 两个按钮发出不同的声音

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

  1. 在内置块分组中打开过程抽屉,拖出“定义过程”块。(除非特别声明,定义过程块指的是“定义过程...执行”块,而非“定义过程...返回”块。)
  2. 点击定义过程块左上角的蓝色标记,将一个“参数x”拖入到“输入项”中,将“x”改为“数”,可以参见第5章的图5-6。
  3. 单击“我的过程”,将过程名改为“演奏音符”。
  4. 从按钮1的点击事件处理程序中拖出第一行代码块(设音效播放器1的源文件为),这时第二行代码块(让音效播放器1播放)也会随之而来,将两行代码块放在定义过程块中(“执行”两个字的右侧)。
  5. 将1.wav块拖入垃圾桶。
  6. 从文本抽屉中拖出拼字串块放到“设音效播放器1的源文件为”块的插槽内。
  7. 将鼠标悬停在演奏音符过程的“数”参数上,拖出“数”块,并将其放入拼字串块的第一个插槽中。
  8. 从文本抽屉中拖出空文本块,放在拼字串块的第二个插槽中。
  9. 将文本值设置为“.wav”。(切记不要输入引号。)
  10. 从过程抽屉中拖出“调用演奏音符”块,放到空的按钮1的点击事件处理程序中。
  11. 在参数“数”插槽中填入文本块“1”。

现在,当按钮1被点击时,演奏音符过程将以数字1为参数被调用。该过程将音效播放器1的源文件属性设为“1.wav”,并播放该声音。

以同样方法创建按钮2的点击事件处理程序,调用参数为2的演奏音符过程。(可以复制已有的调用演奏音符块,将其移动到按钮2的点击事件处理程序中,并将参数更改为2。)程序如图9-5所示。

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

命令安卓加载声音

此时在手机上测试应用,结果可能会让你失望:第一次按键时,可能会听不到预想的声音,或弹出错误提示,或有一段意外的延迟。{![最新发布的版本中,听得到声音,没有错误提示,延迟几乎感觉不到。——译者注]}这是因为安卓系统是在程序运行时才开始加载声音文件,加载过程需要一点时间,因此会造成延迟。那么为什么前几章没有出现过这个问题?因为我们已经在设计视图中设置了音效播放器组件的源文件属性,当程序启动时,声音文件会自动加载。而这里,直到程序启动完成之后,也没有设置组件的源文件属性,因此没有对声音进行预加载。我们需要在程序启动时直接加载声音文件,如图9-6所示。

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

测试:点击按钮,检查声音的播放是否有延迟。(如果没有听到声音,请确保手机的媒体音量没有被设成静音。)

实现其余的音符

两个按钮已经实现了演奏音符的功能,现在需要回到设计视图中,添加其余六个按键。首先要下载其余的六个声音文件:

然后,创建六个新按钮,重复此前的步骤,分别设置每个按钮的显示文本属性及背景颜色属性,具体设置如下:

  • 按钮3(“E”,粉红色 )
  • 按钮4(“F”,橙色 )
  • 按钮5(“G”,黄色 )
  • 按钮6(“A”,绿色 )
  • 按钮7(“B”,青色 )
  • 按钮8(“C”,蓝色 )

按钮8的文本颜色属性需要改为白色,这样更加醒目,如图9-7所示。

图9-7 在设计视图中添加其余按钮,并上传声音文件

回到编程视图中,为每个新按钮创建点击事件处理程序,并以相应的参数调用演奏音符过程。同样,在Screen1的初始化程序中加载新的声音文件,如图9-8所示。

图9-8 编写按钮的点击事件处理程序,使键盘与音调相对应

测试:所有按钮都已就绪,试试点击它们,看是否每个按钮奏出不同的音符?

记录并回放音符

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

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

  • 音符:用于保存与演奏的音符相对应的声音文件名,按照演奏顺序排列。
  • 时间:记录音符演奏时的时间点。

提示:在继续之前,不妨复习一下在第8章“总统问答”中学过的的关于列表的知识,也可以参看第19章。

我们可以从计时器组件中得到时间信息,因此也可以用来记录音符的演奏时长,以便能够以恰当的速度回放。

添加组件

在设计视图中添加一个计时器组件,并添加播放和重置按钮,按钮放在水平布局组件中。

  1. 从传感器分组中拖出计时器组件放在预览窗口中,它将出现在“非可视组件”区域。取消勾选启用计时属性,因为我们希望在回放期间,计时器听从我们的调遣,适时地启动并结束计时。
  2. 从布局组件分组中拖出一个水平布局组件放在按钮下面,设置其宽度属性设为“充满”。
  3. 从用户界面分组中拖出一个按钮,改名为播放按钮,显示文本属性设为“播放”。
  4. 拖出另一个按钮并放在播放按钮的右侧,改名为重置按钮,显示文本属性设为“重置”。

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

图9-9 添加组件以便记录并回放音符

记录音符及时间

回到编程视图中,为组件添加正确的行为。我们需要维护两个列表:音符与时间,每次用户按下一个按钮,就向两个列表中各添加一项。

  1. 从变量抽屉中拖出一个声明global变量(我的变量)块来定义一个新的变量;
  2. 点击“我的变量”将变量命名为“音符”;
  3. 打开列表抽屉,拖出一个空列表块,将其放置在声明变量(音符)的插槽中。

这样就定义了一个名为音符的空列表。重复上述步骤定义另一个变量,命名为“时间”。新的代码块如图9-10所示。

图9-10 声明两个全局变量来保存弹奏的音符及时间

块的作用

每演奏一个音符,需要保存两项数据:声音文件名(保存到音符列表),以及演奏开始瞬间的时刻(保存到时间列表)。用“让计时器1求当前时间”块来记录时刻,它返回当前时刻的时间值(例如,2011年3月12日上午8时33分14秒),精确到毫秒。这两项数据分别来自于音效播放器的源文件属性以及计时器组件的当前时间属性,将分别被添加到音符及时间列表中,如图9-11所示。

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

例如,如果你演奏“长城外,古道边”[GEGCACG](其中的C为高音C),你的列表中最终会有七条记录,可能如下所示。

  • 音符:5.wav,3.wav,5.wav,8.wav,6.wav ,8.wav,5.wav
  • 时间[日期省略]:12:00:01,12:00:02,12:00:02.5,12:00:03,12:00:05,12:00:06,12:00:07

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

图9-12 对用户点击重置按钮进行反馈

回放音符

作为一个思想实验,先来考虑如何实现音符的回放,而暂时忽略回放速度。通过创建图9-13中的代码块,来实现这个阶段性的目标。

  • 用全局变量“计数”来跟踪音符列表中当前正在播放的音符的索引值(位置)。
  • 新建过程“回放”,用来播放当前音符,并移动到下一个音符。
  • 编写播放按钮点击事件处理程序:设计数为1,只要列表中有保存的音符,就调用回放过程。
图9-13 回放已经记录的音符

块的作用

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

为了更好地了解递归的工作原理,我们来一步一步地探究,当用户演奏同时记录了三个音符(5.wav、3.wav和5.wav),然后按下播放按钮时,都发生了什么。播放按钮的点击事件处理程序首先判断列表中是否保存了音符:由于音符列表长度3>0,列表不空,因此设定计数等于1,并调用回放过程。

  1. 在第一次调用回放过程时,计数 = 1:
    • 设音效播放器1的源文件为音符列表中的第1项,即5.wav;
    • 调用音效播放器1的播放功能,即播放5.wav;
    • 由于计数(1)小于音符列表的长度(3),因此计数递增为2,并再次调用回放过程。
  2. 第二次调用回放过程时,计数 = 2:
    • 设音效播放器1的源文件为音符列表中的第2项,即3.wav;
    • 调用音效播放器1的播放功能,即播放3.wav;
    • 由于计数(2)小于音符列表的长度(3),因此计数递增为3,并再次调用回放过程。
  3. 第三次调用回放过程时,计数 = 3:
    • 设音效播放器1的源文件为音符列表中的第3项,即5.wav;
    • 调用音效播放器1的播放功能,即播放5.wav;
    • 由于计数(3)不再小于音符列表的长度(3),因此什么事都没有发生,回放过程执行完毕。

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

这里的递归是正确的,但这个例子中还有另一个问题:在两次调用音效播放器1的播放功能之间,时间间隔几乎为零(程序运行的速度非常快)。因此,除了最后一个音符可以正常播放,其余音符都被下一个音符截断了,这些音符都等不到播放完成,音效播放器1的源文件属性就已经被改写为下一个音符,并由播放器播放出来。为了获得正确的播放效果,需要在两次调用回放过程之间添加延迟功能。

回放适当延迟的音符

延迟的设定与两个音符之间的时间差有关,我们用计时器的计时功能来体现这个时间差。例如,如果时间差为3000毫秒(3秒),则将计时器1的计时间隔设置为3000毫秒,并启动计时器;在计时结束时再去调用回放过程。对回放过程中的“如果...则”块做出修改,如图9-14所示。创建计时器1的计时事件处理程序,来设定计时结束时将发生的事情。

图9-14 在两个音符之间添加延迟

块的作用

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

  • 音符:5.wav,3.wav,5.wav
  • 时间:12:00:00,12:00:01,12:00:01.5

如图9-14所示,在播放按钮的点击事件处理程序中设置计数为1,并调用回放过程。

  1. 第一次调用回放过程时,计数 = 1:

    • 设音效播放器1的源文件为音符列表中的第1项,即“5.wav”;
    • 调用音效播放器1的播放功能,播放5.wav;
    • 因为计数(1)小于音符列表的长度(3),于是设计时器1的计时间隔为时间列表中的第1项(12:00:00)与第2项(12:00:01)之间的时间差,即1秒。计数递增到2,启用计时器1(计时器1开始计时)。

    在计时器1开始计时的这个1秒钟内,什么事情都不会发生。1秒钟之后,计时结束,计时器暂时禁用,并调用回放过程。

  2. 第二次调用回放过程时,计数 = 2 :

    • 设音效播放器1的源文件为音符列表中的第2项,即“3.wav”;
    • 调用音效播放器1的播放功能,播放3.wav;
    • 因为计数(2)小于音符列表的长度(3),因此设计时器1的计时间隔为时间列表中的第2项(12:00:01)与第3项(12:00:01.5)之间的时间差,即0.5秒。计数递增到3,启用计时器1(计时器1开始计时)。

    在计时器1开始计时的这个0.5秒钟内,什么事情都不会发生。0.5秒钟之后,计时结束,计时器暂时禁用,并调用回放过程。

  3. 第三次调用回放过程时,计数 = 3 :

    • 设音效播放器1的源文件为音符列表中的第3项,即“5.wav”;
    • 调用音效播放器1的播放功能,播放5.wav;
    • 因为计数(3)不再小于音符列表的长度(3),因此什么事情也不会发生,回放过程执行完毕。

完整的“木琴”应用

图9-15中是“木琴”应用中所有代码块的最终版本。

图9-15 “木琴”应用中的全部代码

改进

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

  • 目前,在回放过程中,没有对用户点击重置按钮做任何限制,这将导致程序的崩溃。(你能说出原因吗?)修改播放按钮的点击事件处理程序,让重置按钮在回放期间禁用,回放完成后再重新启用。将回放过程中的“如果...则”块改为“如果...则...否则”块,并在“否则”分支中重新启用重置按钮。
  • 类似问题也发生在播放按钮上,用户可以在回放过程中再次点击该按钮。(想象一下会发生什么。) 在播放按钮的点击事件处理程序中禁用播放按钮,将其显示文本属性修改为“播放中...... ”,并且像重置按钮一样,在回放过程的“如果...则...否则”块中重新启用该按钮,并重置其显示文本属性。
  • 添加一个按钮来显示一首歌曲的名字,如“致爱丽丝”。当用户单击时,向音符列表及时间列表中填写相应的值,将计数设为1,并调用回放过程。计时器有一个非常有用的功能——创建毫秒时间点功能{![用毫秒数表示当前时间,起始时间为1970年1月1日0时0分0秒0毫秒。——译者注]},可以用来设定音符之间的延迟。
  • 如果用户按下一个音符,然后去做别的事情,几小时后回来,又按下另一个音符,那么尽管音符可能属于同一首歌,但这绝不是用户的意图。有两种方法可以改进程序:(1)在一个合理的时间间隔后,停止记录音符,如1分钟;(2)设置一个最大时间间隔,用数学抽屉中的“最大值”块,来截断那些超过最大值的时间间隔,从而限制音符播放的时长。
  • 通过改变按钮的外观,如显示文本、背景颜色或前景颜色(字体颜色)属性,来形象地提示当前正在播放的音符。

小结

以下是本章涵盖的概念。

  • 通过修改音效播放器组件的源文件属性,可以用一个而非八个音频播放器组件来播放不同的音频文件。记住要在应用初始化时加载声音文件,以免运行时加载文件引起播放的延迟(见图9-6)。
  • 列表可以为程序提供存储功能,可以在列表中保存用户的操作记录,并在以后对存储内容进行提取和再处理。本章我们使用这个功能来录制及播放歌曲。
  • 计时器组件可以用来确定当前时间,两个时间的差值为我们提供了两个事件之间的时间间隔。
  • 计时器组件的计时间隔属性可以在程序中设置,就像我们设置两个音符之间的时间间隔一样。
  • 编写一个能自我调用的过程不仅是可能的,有时也是必要的。这种强大的技术称为递归。在编写递归过程时,一定要确保为程序的退出设定一个基本条件,它的重要性远大于为自我调用设定条件,否则程序将陷入无限循环。

下面这首优美的歌曲供学习者练习之用。

附图:这首优美的歌曲供学习者练习之用。