很難相信,利用技術(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 | 記錄音符之間的延遲 |
用戶界面中包含了從低音C到高音C的大調(diào)五聲(七音符)音階的八個音符鍵盤,本節(jié)將創(chuàng)建這樣的音樂鍵盤。
首先創(chuàng)建前兩個木琴鍵,用按鈕來實現(xiàn):
1. 從面板(palette)的user interface組中拖出一個按鈕,保留Button1的名稱,我們希望它像木琴的鍵一樣,是一個洋紅色(Magenta)的長條,因此做如下設置:
BackgroundColor屬性:為洋紅色(Magenta);
Text屬性:為“C”;
Width屬性:為“Fill parent”,使其占滿屏幕;
2. 重復上述步驟創(chuàng)建第二個按鈕,名為Button2,放在Button1下面。Width及Height屬性值同Button1,但BackgroundColor屬性設為紅色,Text屬性設置為“D”。
(稍后將重復步驟2來創(chuàng)建其余六個音符按鈕。)在組件設計器中看起來如圖9-2所示。
圖 9-2 用按鈕來充當音符按鍵
在手機上的顯示看起來與此相似,只是兩個彩色按鈕之間沒有空白。
我們不能讓木琴沒有聲音,創(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塊的第二個插槽中;
現(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 在應用啟動時加載聲音文件
測試:在手機中重新啟動應用,按鍵之后立刻播放聲音。(如果你沒有聽到聲音,請確保手機上的媒體音量沒有被設置為靜音。)
兩個按鈕已經(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的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:包含與演奏的音符相對應的聲音文件名,按照演奏順序排列;
提示:在繼續(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
當用戶按下“重置”按鈕時,我們希望清空這兩個列表。由于用戶看不到清空帶來的任何變化,因此添加一個Sound1.Vibrate塊,通過振動來告知用戶按鍵生效了,這種設置對用戶來說是非常友好的。圖9-12顯示了這一功能用到的塊。
圖 9-12 為用戶的“重置”操作提供反饋
作為一個思想實驗,先來考慮如何實現(xiàn)音符的回放,而暫時忽略回放速度。我們可以(但不會)通過創(chuàng)建圖9-13中的那塊來實現(xiàn)這個暫時的目標:
變量count用來跟蹤notes列表中當前正在播放的音符的索引(位置);
新過程 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;
2. 第二次調(diào)用PlayBackNote時,count=2:
Sound1.Source被設置為notes中的第2項,即3.wav;
調(diào)用Sound1.Play,播放3.wav;
3. 第三次調(diào)用PlayBackNote時,count=3:
Sound1.Source被設置為notes中的第3項,即6.wav;
調(diào)用Sound1.Play,播放6.wav;
提示:雖然遞歸功能強大,但運用起來存在危險。來做一個思想實驗:問問自己,如果程序員忘了在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
如圖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;
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;
Clock1.Timer計時開始,間隔3秒之后,定時器暫時禁用,并調(diào)用PlayBackNote。
3. 第三次調(diào)用PlayBackNote時,count= 3 :
Sound1.Source被設置為notes中的第3項,即“6.wav”;
調(diào)用Sound1.Play來播放6.wav;
下面是一些可供探討的備選方案:
目前,在回放過程中,沒有對用戶點擊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塊,來限制音符播放的時長。
以下是本章涵蓋的概念:
通過修改Sound組件的Source屬性,可以用一個而非八個Sound組件來播放不同音頻文件。記住要在應用初始化時加載聲音文件,以免運行時加載所引起的問題。(見圖9-6);
列表(Lists)可以為程序提供存儲功能,可以在列表中保存用戶的操作記錄,并在以后對存儲內(nèi)容進行提取和再處理。我們使用這個功能來錄制及播放歌曲;
Clock組件可以用來確定當前時間,兩個時間值只差為我們提供了兩個事件之間的時間間隔;
Clock組件的TimerInterval屬性可以在程序中設置,就像我們設置兩個音符之間的時間間隔一樣;
更多建議: