第 9 章 木琴

2018-02-24 15:51 更新

很難相信,利用技術(shù)來記錄和播放音樂只能追溯到1878年,也就是愛迪生獲得留聲機專利時。時至今日,我們已經(jīng)有了長足的進步,音樂合成器、光盤、采樣和混音、播放音樂的手機,甚至是擁塞的遠程互聯(lián)網(wǎng)。在本章中,通過創(chuàng)建一個可以錄制和播放音樂的木琴應用,你也將成為這種進步的推動者。

{%}

作品描述

如圖9-1所示,這個應用(最初由App Inventor團隊的Liz Looney創(chuàng)建)可以做到:

{%}

圖 9-1 木琴應用的用戶界面

  • 通過觸摸屏幕上的彩色按鈕播放八個不同的音符;

  • 按“播放”按鈕,回放之前彈奏的音符;

  • 按“重置”按鈕清除之前彈過的音符,以便輸入新曲。

學習要點

本章程涵蓋了以下概念:

  • 使用單一的聲音組件來播放不同的音頻文件;

  • 使用Clock組件來計算并實現(xiàn)兩個音符之間的延遲;

  • 在創(chuàng)建一個過程時做判斷;

  • 創(chuàng)建能夠自我調(diào)用的過程;

  • 列表的高級應用,包括添加、刪除及讀取項。

準備開始

登陸App Inventor網(wǎng)站,創(chuàng)建新項目“Xylophone”,屏幕標題設置為“木琴”,并連接到測試手機或模擬器。

設計組件

應用中有13個不同的組件(其中8個Button組成了樂器鍵盤),見表9-1。要在編程之前一次性創(chuàng)建這么多組件,顯得有些乏味,因此我們按功能將應用劃分為若干部分,分步來創(chuàng)建組件,這需要在組件設計器與塊編輯器之間反復切換,就像第五章“瓢蟲快跑”應用中一樣。

表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 記錄音符之間的延遲

創(chuàng)建鍵盤

用戶界面中包含了從低音C到高音C的大調(diào)五聲(七音符)音階的八個音符鍵盤,本節(jié)將創(chuàng)建這樣的音樂鍵盤。

創(chuàng)建第一個音符按鈕

首先創(chuàng)建前兩個木琴鍵,用按鈕來實現(xiàn):

1. 從面板(palette)的user interface組中拖出一個按鈕,保留Button1的名稱,我們希望它像木琴的鍵一樣,是一個洋紅色(Magenta)的長條,因此做如下設置:

  • BackgroundColor屬性:為洋紅色(Magenta);

  • Text屬性:為“C”;

  • Width屬性:為“Fill parent”,使其占滿屏幕;

  • Height屬性:為40像素。

2. 重復上述步驟創(chuàng)建第二個按鈕,名為Button2,放在Button1下面。Width及Height屬性值同Button1,但BackgroundColor屬性設為紅色,Text屬性設置為“D”。

(稍后將重復步驟2來創(chuàng)建其余六個音符按鈕。)在組件設計器中看起來如圖9-2所示。

{%}

圖 9-2 用按鈕來充當音符按鍵

在手機上的顯示看起來與此相似,只是兩個彩色按鈕之間沒有空白。

添加Sound組件

我們不能讓木琴沒有聲音,創(chuàng)建一個Sound組件,名字為Sound1。MinimumInterval(最小間隔)屬性設置為0(默認值為500毫秒)。這可以讓我們的演奏要多快有多快,而不必等半秒鐘(500毫秒)。不必設置Source屬性,稍后我們會在塊編輯器中設置。

下載1.wav和2.wav,并加載到項目中。與前幾章不同,這里的聲音文件必須保持原有文件名,不能修改,理由稍后就會明晰。后面還有六個聲音文件需要加載。

聲音與按鈕的連接

當某個按鈕被點擊時,用程序來實現(xiàn)播放聲音的行為,即:如果Button1被點擊,則播放1.wav,如果Button2被點擊,播放2.wav,等等。切換到塊編輯器,如圖9-3所示,進行以下設置:

1. 從Screen1項下的Button1抽屜里拖出Button1.Click塊;

2. 從Sound1抽屜里拖set Sound1.Source塊,放置在Button1.Click塊中;

3. 輸入“text”來創(chuàng)建一個文本塊(而不是從Built-in項下的Text抽屜里拖出,這樣更便捷。)設置文本值為“1.wav”,并與Sound1.Source塊連接;

4. 添加Sound1.Play塊。

{%}

圖 9-3 點擊按鈕時播放聲音

對Button2進行同樣設置,如圖9-4(只改了文件名),代碼幾乎完全重復。

{%}

圖 9-4 添加更多的聲音

重復的代碼提示我們最好是創(chuàng)建一個過程,像在第3章“打地鼠”和第5章“瓢蟲快跑”中那樣。具體來說,我們將創(chuàng)建一個帶數(shù)字參數(shù)的過程,將Sound1的Source屬性設置為相應的聲音文件,并播放該聲音文件。這是對程序進行重構(gòu)改進而又不改變程序行為的又一個例子,這一概念在“打地鼠”一章中首次引入。用join塊將數(shù)字(如1)與文本“.wav”連接起來,創(chuàng)造出正規(guī)的文件名(如“1.wav”)。下面是創(chuàng)建這個過程的步驟:

1. 在塊編輯器中打開Procedures抽屜,拖出“to procedure”塊;

2. 單擊procedure將過程名改為playNote;

3. 點擊procedure塊左上角的藍色方塊呼出內(nèi)部組件,將一個input x塊插入“inputs”塊;

4. 將input x塊中的x改為number;

5. 將set Sound1.Source to塊從Button1.Click事件處理程序中拖出,放在PlayNote過程內(nèi)“do”的右邊,Sound1.Play塊也將隨之移動;

6. 將1.wav塊拖入垃圾桶;

7. 從Text抽屜中拖出join塊放到set Sound1.Source to的插槽內(nèi);

8. 將鼠標懸停在playNote的number參數(shù)上,呼出并拖動get number塊,并將其放入join塊的第一個插槽中;

9. 從Text抽屜中拖出空文本塊,放在join塊的第二個插槽中;

將文本值設置為“.wav”。(切記不要輸入引號);

從Procedures抽屜中拖出call PlayNote塊,放到空的Button1.Click內(nèi);

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

現(xiàn)在,當Button1被點擊時,過程PlayNote將以數(shù)字1為參數(shù)被調(diào)用。該過程將Sound1.Source屬性設為“1.wav”,并播放該聲音。

創(chuàng)建一個Button2.Click塊,調(diào)用參數(shù)為2的PlayNote過程。(可以復制現(xiàn)有的PlayNote塊,將其移動到Button2.Click塊內(nèi),并將參數(shù)更改為2;也可以復制整個Button1.Click塊,然后將Button1改為Button2,再將參數(shù)1改為2。)程序如圖9-5所示。

{%}

圖 9-5 創(chuàng)建一個過程來演奏音符

告訴Android加載聲音

此時在手機上測試程序會讓你失望:第一次按鍵時,不但沒聽到預想的聲音,手機還彈出錯誤提示:“Error 703:Unable to play 1.wav”(不能播放1.wav);第二次再按同一個鍵時,才聽到聲音。這是因為Android系統(tǒng)是在程序運行時才加載聲音文件(只需加載一次),加載過程需要一點時間。第一次按鍵,當call Sound1 play塊開始執(zhí)行時,set Sound1.Source to塊的加載任務尚未完成,因此系統(tǒng)給出錯誤提示;等到第二次按鍵時,聲音文件已經(jīng)加載完成,因此可以正常播放。為什么前幾章沒有出現(xiàn)過這個問題?因為我們在組件設計器中預先設置了Sound組件的Source屬性為某個聲音文件,當程序啟動時,聲音文件會自動加載。而這里,直到程序啟動之后,我們也沒有對Sound1.Source進行設置,因此沒有對聲音做初始化。我們必須在程序啟動時直接加載聲音文件,如圖9-6所示。

{%}

圖 9-6 在應用啟動時加載聲音文件

 測試:在手機中重新啟動應用,按鍵之后立刻播放聲音。(如果你沒有聽到聲音,請確保手機上的媒體音量沒有被設置為靜音。)

實現(xiàn)其余的音符

兩個按鈕已經(jīng)實現(xiàn)了演奏音符的功能,現(xiàn)在需要回到組件設計器,加載其余六個聲音文件3.wav、4.wav、5.wav、6.wav、7.wav和8.wav,并添加其余六個音符。首先創(chuàng)建六個新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 在組件設計器中放置其余的聲音按鈕

回到塊編輯器中,為每個新按鈕創(chuàng)建Click塊并以相應的參數(shù)調(diào)用PlayNote過程。同樣,在Screen.Initialize中加載新的聲音文件,如圖9-8所示。

{%}

圖 9-8 對按鈕單擊事件編程,使得鍵盤與音調(diào)相對應

 測試:現(xiàn)在所有按鈕都已經(jīng)就緒,點擊不同按鈕會演奏不同的音符。

記錄并回放音符

用按鍵來彈奏音符的確有趣,但如果能錄制并播放歌曲豈不更好。為了實現(xiàn)回放功能,需要記錄彈奏的音符并加以保存。除了要記錄彈奏的音高(聲音文件),還要記錄兩個音符之間的時間長度,否則將無法表現(xiàn)兩個連續(xù)快彈音符與兩個間隔10秒的音符之間的差別。

我們需要維護兩個列表,每彈奏一個音符,兩個列表中都會各自添加一條記錄:

  • notes:包含與演奏的音符相對應的聲音文件名,按照演奏順序排列;

  • times:記錄音符演奏時的時間點。

 提示:在繼續(xù)之前,不妨復習一下在“總統(tǒng)測驗”中所學到的關(guān)于列表的知識。

我們可以從Clock組件中得到計時信息,因此也可以用來正確地設定音符的回放速度。

添加組件

在設計器中添加一個Clock組件及“播放”和“重置”按鈕,按鈕放在HorizontalArrangement中:

1. 拖入一個Clock組件,它將出現(xiàn)在“不可見組件”區(qū)域,取消勾選TimerEnabled屬性,因為我們希望在回放期間,計時器聽從我們的調(diào)遣,適時地啟動并完成計時;

2. 從layout組中拖出一個HorizontalArrangement組件放在按鈕下面,Width屬性設為“Fill parent”;

3. 從User Interface組中拖動一個按鈕,改名為PlayButton,Text屬性設為“播放”;

4. 拖出另一個按鈕并放在PlayButton右側(cè),改名為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 設置變量來記錄音符

塊的功能

每演奏一個音符,需要保存兩項數(shù)據(jù):聲音文件名(保存到notes列表),以及演奏瞬間的時刻(保存到times列表)。用Clock1.Now塊來記錄時刻,它返回當前時刻的時間值(例如,2011年3月12日上午8時33分14秒),精確到毫秒。這些數(shù)據(jù)可以通過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 為用戶的“重置”操作提供反饋

音符的回放

作為一個思想實驗,先來考慮如何實現(xiàn)音符的回放,而暫時忽略回放速度。我們可以(但不會)通過創(chuàng)建圖9-13中的那塊來實現(xiàn)這個暫時的目標:

  • 變量count用來跟蹤notes列表中當前正在播放的音符的索引(位置);

  • 新過程 PlayBackNote,用來播放當前音符,并移動到下一個音符;

  • 編寫PlayButton.Click事件處理程序,設置count為1,只要列表中有保存的音符,就調(diào)用PlayBackNote。

{%}

圖 9-13 回放被記錄下來的音符

塊的功能

這可能是你第一次看到能自我調(diào)用的過程。這件事乍一看好像不可能,但實際上這是計算機科學中一個非常重要的概念:強大的遞歸。

為了更好地了解遞歸的工作原理,我們來一步一步地探究,當用戶演奏了三個音符( 1.wav、 3.wav和6.wav),然后按下“播放”按鈕時,都發(fā)生了什么。PlayButton.Click首先判斷列表中是否保存了音符:由于notes列表長度3>0,列表不空,因此設定count等于1,并調(diào)用PlayBackNote:

1. 在第一次調(diào)用PlayBackNote時,count= 1:

  • Sound1.Source被設置為在notes中的第1項,即1.wav;

  • 調(diào)用Sound1.Play,播放1.wav;

  • 由于count值(1)小于notes的長度(3),因此count遞增為2,并再次調(diào)用PlayBackNote;

2. 第二次調(diào)用PlayBackNote時,count=2:

  • Sound1.Source被設置為notes中的第2項,即3.wav;

  • 調(diào)用Sound1.Play,播放3.wav;

  • 由于count(2)小于notes的長度(3),因此count遞增為3,并再次調(diào)用PlayBackNote;

3. 第三次調(diào)用PlayBackNote時,count=3:

  • Sound1.Source被設置為notes中的第3項,即6.wav;

  • 調(diào)用Sound1.Play,播放6.wav;

  • 由于count(3)不小于notes的長度(3),因此跳出if塊,回放結(jié)束。

 提示:雖然遞歸功能強大,但運用起來存在危險。來做一個思想實驗:問問自己,如果程序員忘了在PlayBackNote塊中插入count遞增的塊,會發(fā)生什么事情。

這里的遞歸是正確的,但這個例子中還有另一個問題:在兩次調(diào)用Sound1.Play之間幾乎沒有時間間隔(程序運行的速度非常快),因此每個音符都被下一個音符截斷,除了最后一個。所有音符(除了最后一個)都等不到播放完,Sound1的source屬性就已經(jīng)被改寫為下一個音符,并由Sound1.Play播放出來。為了獲得正確的行為,需要在兩次調(diào)用PlayBackNote之間添加延遲功能。

播放適當延遲的音符

延遲的設定與兩個音符之間的時間差有關(guān),我們用clock來為這個時間差計時。例如,如果時間差為3,000毫秒(3秒),則將Clock1.TimerInterval設置為3000,并啟動計時器;在計時結(jié)束時再調(diào)用PlayBackNote。對PlayBackNote的if塊做出修改,如圖9-14所示。創(chuàng)建Clock1.Timer事件并編寫事件處理程序,來說明計時結(jié)束時將發(fā)生的事情。

{%}

圖 9-14 在音符之間加入延遲

塊的功能

現(xiàn)在假設兩個列表中記錄了以下內(nèi)容:

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

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

如圖9-14所示,在PlayButton.Click中設置count為1,并調(diào)用PlayBackNote。

1. 第一次調(diào)用PlayBackNote時,count= 1:

  • Sound1.Source被設置為notes中的第1項,即“1.wav”;

  • 調(diào)用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秒之后,計時結(jié)束,定時器暫時禁用,并調(diào)用PlayBackNote。

2. 第二次調(diào)用PlayBackNote時,count= 2 :

  • Sound1.Source被設置為notes中的第2項,即“3.wav”;

  • 調(diào)用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秒之后,定時器暫時禁用,并調(diào)用PlayBackNote。

3. 第三次調(diào)用PlayBackNote時,count= 3 :

  • Sound1.Source被設置為notes中的第3項,即“6.wav”;

  • 調(diào)用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。

  • 類似問題也發(fā)生在PlayButton上,用戶可以在回放過程中再次點擊該按鈕。(想象一下會發(fā)生什么。) 在PlayButton.Click中禁用PlayButton,并將其Text屬性改為“播放中...... ”,并像ResetButton一樣,在PlayBackNote的ifelse塊中重新啟用該按鈕,并重置Text屬性。

  • 添加一個按鈕來顯示一首歌曲的名字,如“致愛麗絲”。當用戶單擊時,向notes及times列表中填寫相應的值,將count設定為1,并調(diào)用PlayBackNote。有一個非常有用的塊Clock1.MakeInstantFromMillis(用毫秒設置時間間隔),可以用來設定音符之間的延遲。

  • 如果用戶按下一個音符,然后去做別的事情了,幾小時后回來,又按下另一個音符,盡管音符可能屬于同一首歌,但這絕不是用戶的意圖。有兩種方法可以改進程序:(1)在一個合理的時間間隔后,停止記錄音符,如1分鐘;(2)通過對Clock1.TimerInterval使用Math抽屜中的max塊,來限制音符播放的時長。

  • 通過改變按鈕的外觀,如Text、BackgroundColor或ForegroundColor屬性,來形象地提示當前正在播放的音符。

小結(jié)

以下是本章涵蓋的概念:

  • 通過修改Sound組件的Source屬性,可以用一個而非八個Sound組件來播放不同音頻文件。記住要在應用初始化時加載聲音文件,以免運行時加載所引起的問題。(見圖9-6);

  • 列表(Lists)可以為程序提供存儲功能,可以在列表中保存用戶的操作記錄,并在以后對存儲內(nèi)容進行提取和再處理。我們使用這個功能來錄制及播放歌曲;

  • Clock組件可以用來確定當前時間,兩個時間值只差為我們提供了兩個事件之間的時間間隔;

  • Clock組件的TimerInterval屬性可以在程序中設置,就像我們設置兩個音符之間的時間間隔一樣;

  • 編寫一個能自我調(diào)用的過程不僅是可能的,有時也是必要的。這種強大的技術(shù)稱為遞歸。在編寫遞歸過程時,一定要確保為程序的退出設定一個基本條件,它的重要性遠大于為自我調(diào)用設定條件,否則程序?qū)⑾萑霟o限循環(huán)。

資源下載

1.wav

2.wav

3.wav

4.wav

5.wav

6.wav

7.wav

8.wav

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號