在開始前,還沒看過序章的朋友們,可以點擊進去,教學大綱和主題方向都寫在裡面囉!
地基一定要打穩,如果基本的還不會的話,建議先去w3school惡補一下!
我們要做的是一個音樂遊戲,因此最重要的核心技術便是跟audio這個tag有關的語法,這章節就來把它!
咦?你問我不先設置環境嗎,沒錯XD,由於這是寫給入門者的文章,能簡單的地方就簡單,帶大家從實作中學習,因此只需要有大家都有的edge或chrome,和一個文字編輯器(推薦VScode)就可以開始囉。
首先,audio這個元件大家應該不陌生,早在無名小站那個年代,幾乎每個網站都會放一個音樂播放器,而且有不少都採用自動播放,筆者就經常不小心開到幾個有歌網站,一次播放好幾首歌,更甚者有時候還找不到在哪一個分頁,堪稱網頁中的幽靈。
<audio id="Music" controls>
<source src="music/nameOfMyMusic.mp3">
</audio>
既然有現成的控制器能用,我們就先加上controls,這樣除錯方便
在處理音頻的訊號時,我們要用到AudioContext和AnalyserNode,前者能作為我們的音頻接口,後者能從中取出即時(當下)的頻率資料。
不好懂對吧,就想像我們現在要給自己的筆電接一個新買的喇叭,如果少了AudioContext,就像是筆電上面根本沒有插孔,這樣喇叭接不上去根本不能用;如果少了AnalyserNode,就像是我喇叭的插頭壞了,沒辦法從中取得音訊。(只是比喻不要太認真xd)
這樣應該就不難理解,AudioContext只需要建立一次即可(除非你想在筆電上多挖幾個洞XD),AnalyserNode就可以不只一次(可以用好幾個不同的喇叭接收音訊),並且為了持續的取得音訊,需要多次用到AnalyserNode的方法,不過我們並沒有要用到混音跟創作音樂,所以這次我們只需要各建立一個就好。
這邊先咐上今天的流程圖,就可以知道這兩個元件在哪裡作用了:
讓我們先把目光放在黃色的蔆形,在該流程圖中,蔆形是一個分歧點,如下圖,比方說,我們可以檢查使用者按下開始後,是否有先上傳音樂,如果有,就走方案A,反之就用方案B。而這張圖中則是利用監聽事件EventListener來等待使用者進行動作。
那麼,從流程圖中,可以知道,用戶能做的動作有四種,分別為Play、Pause、Select、Upload,今天就從最基本也最重要的上傳開始吧!順便幫大家暖身一下(可能有些人疫情宅在家都懶了,很久沒寫程式XD)
<button id="Upload-beautify">- 上傳音樂 -</button>
<div class="hidden">
<input id="Upload" type="file" value="- 上傳音樂 -" accept="audio/*">
</div>
<script>
document.querySelector("#Upload-beautify").addEventListener(
"click", function(){
document.querySelector("#Upload").click();
}, false);
</script>
因為input不好客製化造型,這邊讓用戶按下button後,再呼叫input
這邊也要提醒一下,下面這兩種寫法都是一樣的,如果你複製這段代碼到開發者工具的console中,會得到"true",不過我會推薦大家用左邊的寫法,因為當自己或別人在讀code時,從HTML文件看到了這個ID,要到JS中去搜尋的時候,比起"Upload",多了井字號"#Upload"幾乎可以立刻找到,在維護上相對會輕鬆許多。
document.querySelector("#Upload") === document.getElementById("Upload")
console真的很好用,若沒指定動作,會直接像console.log一樣回傳值給你,不過小心別打錯字了,如果兩邊ID都打錯,很可能也會得到true唷!因為都找不到有這個ID的元件,會回傳null。
接著來實作上傳機制,值得注意的是,input標籤內的accept="audio/*"
屬性只是一個基本的(防笨)機制,實測就會發現,用戶在上傳檔案的時候是"預設"找尋音訊檔,然而用戶還是能手賤(X)去傳其他檔案,更關鍵的問題是,在手機上瀏覽時,各個裝置對這個設定的接受程度也不一,因此防範未然,待會最好還是作一個檢查機制。
let Upload = document.querySelector("#Upload");
Upload.addEventListener("change", FileManager, false);
function FileManager(){
console.log(this); // 會印出呼叫該函式的人(初學者這樣想就好)
console.log(this.files); // 會印出用戶上傳的所有檔案
console.log(this.files[0]); // 會印出用戶上傳的第一個檔案(或唯一的)
}
這邊我們設計一個檔案管理函式FileManager,等到用戶上傳檔案後,才開始動作,也因為這裡觀念蠻重要的,所以通通印出來讓大家看一下,這邊的this的指的是這個input元件本身,關於this的用法又是一個大坑,只需要知道這邊的this會指向這個「ID名為Upload的input元件」為就好,畢竟用onchange呼叫該事件的就是它,這個this不是它還能是誰呢,這個問題很有趣可以思考看看。
那麼檔案會傳到哪裡去呢?當然是在這個物件身上囉,因此透過this.files能瀏覽用戶上傳的檔案"們",為什麼我強調"們",是因為它是一個陣列,即使用戶只上傳一個檔案,仍會以陣列的方式讀取,第一筆資料會存在this.files[0]。
知道這點後,我們可以透過 URL.createObjectURL來取得檔案的路徑,如果你還是萌新,可能會想為什麼不直接用資料夾路徑取得資料,寫程式的時候不管是引入外部js、css、甚至圖片影片等等,不都是直接提供相對路徑嗎?其實這是一個保護機制,上傳檔案的時候,用戶的電腦只需要授權網頁使用這一個檔案,並提供一個假的路徑(暫存)blob:接一串英文數字。若直接把整個資料夾路徑奉上,豈不是被看光光了,而近年來瀏覽器更是透過CORS做了嚴格的限制,想要拿別人的資料,必須得有對方的同意。
function FileManager(){
document.getElementById("Music").src = URL.createObjectURL(this.files[0]);
}
透過這樣短短的一行就完成基本的上傳音樂了,不過別忘了唷!我們還要來做檢查機制,常見的音樂副檔名有wav、mp3、m4a,因此我們先建立一個陣列,放進合格的附檔名,接著透過this.files[0].name取得完整的檔名(字串),只要把字串從中間的(.)給切開就能夠取得我們要的副檔名了,因此步驟很簡單:
function FileManager(){
// 可接受的附檔名Exts
let validExts = new Array(".wav", ".mp3", ".m4a");
let index = this.files[0].name.lastIndexOf('.')
let fileExt = this.files[0].name.substring(index);
if (validExts.indexOf(fileExt) < 0) {
console.warn("檔案類型錯誤,可接受的副檔名有: " + validExts.toString());
return; //可寫可不寫,取決於後面還有沒有程式碼要執行
}
let audio = document.getElementById("Music");
audio.src = URL.createObjectURL(this.files[0]);
}
接下來幫大家回憶一下,當我們仔細拆解一下監聽事件的邏輯,就會得到三個關鍵:
WHO -- 目標是誰
WHEN -- 什麼時候
WHAT -- 要做什麼
就以我們剛剛的上傳檔案事件為例:
Upload.addEventListener("change", FileManager, false);
// (WHO) (WHEN) (What)
這樣就可以搭配一開始給的流程圖來理解了,我們要設計一個流程為「當用戶上傳音樂檔案後,先取得該檔案的路徑,再檢查是不是音樂,最後設定音樂的路徑」,因此步驟就會很清楚:
透過這個流程,可以清楚的知道分別需要那些語法來完成一連串的動作,這不管是在設計程式邏輯、或是在設計遊戲上都相當重要,可以先評估自己在哪個階段會遇到挑戰,雖然有些人天生邏輯很好,可以在腦內想得很清楚,不過在團隊合作中,溝通需要一個媒介,這也是LogicFlow存在的意義,不管是設計師、還是程序員,都應該懂得畫流程圖,
不過,對初學者來說肯定是邊做邊學習,可以先開始撰寫程式碼,到一個段落後,把邏輯試著畫成一張圖,講給別人聽,會是不錯的練習唷!
其實我也是為了這次鐵人賽才畫圖的XD,選擇的是特別單純的畫法(也為了讓大家好懂),只有三個圖形,分別是流程起點/終點(圓角矩形)、處理流程(矩形)、抉擇點(稜形),之前獨立開發,都在自己腦內想好即可,不過,這次透過畫出來的方式,也讓我有了一次重構程式碼的機會,對於後續要加上的流程控制跟例外處理,更是能幫上大忙呢!
因為步驟講得比較仔細,篇幅拉長,感謝大家耐心觀看,希望能幫初學者解惑,如果有什麼問題歡迎在下面做詢問!
Q. 請問在今天已經完成的上傳流程中,在用戶的哪個操作、或是哪段程式碼可能會出現錯誤,甚至導致中斷呢?請試著在留言區回答吧!(提示:其實用戶不是你想的那麼聰明!)