iT邦幫忙

2021 iThome 鐵人賽

DAY 2
1
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 2

Chapter1-DJ最愛的音頻動感圖像(I)基本流程圖 & 操作DOM介面

在開始前,還沒看過序章的朋友們,可以點擊進去,教學大綱和主題方向都寫在裡面囉!

看完這章節,你會學到...

https://ithelp.ithome.com.tw/upload/images/20210830/20135197Ds3P8jLnp2.jpg

地基一定要打穩,如果基本的還不會的話,建議先去w3school惡補一下!

前言

我們要做的是一個音樂遊戲,因此最重要的核心技術便是跟audio這個tag有關的語法,這章節就來把它!

咦?你問我不先設置環境嗎,沒錯XD,由於這是寫給入門者的文章,能簡單的地方就簡單,帶大家從實作中學習,因此只需要有大家都有的edge或chrome,和一個文字編輯器(推薦VScode)就可以開始囉。

首先,audio這個元件大家應該不陌生,早在無名小站那個年代,幾乎每個網站都會放一個音樂播放器,而且有不少都採用自動播放,筆者就經常不小心開到幾個有歌網站,一次播放好幾首歌,更甚者有時候還找不到在哪一個分頁,堪稱網頁中的幽靈。

<audio id="Music" controls>
    <source src="music/nameOfMyMusic.mp3">
</audio>

既然有現成的控制器能用,我們就先加上controls,這樣除錯方便

初次認識 Web Audio API(觀念篇)

在處理音頻的訊號時,我們要用到AudioContextAnalyserNode,前者能作為我們的音頻接口,後者能從中取出即時(當下)的頻率資料。

不好懂對吧,就想像我們現在要給自己的筆電接一個新買的喇叭,如果少了AudioContext,就像是筆電上面根本沒有插孔,這樣喇叭接不上去根本不能用;如果少了AnalyserNode,就像是我喇叭的插頭壞了,沒辦法從中取得音訊。(只是比喻不要太認真xd)

這樣應該就不難理解,AudioContext只需要建立一次即可(除非你想在筆電上多挖幾個洞XD),AnalyserNode就可以不只一次(可以用好幾個不同的喇叭接收音訊),並且為了持續的取得音訊,需要多次用到AnalyserNode的方法,不過我們並沒有要用到混音跟創作音樂,所以這次我們只需要各建立一個就好。

這邊先咐上今天的流程圖,就可以知道這兩個元件在哪裡作用了:
https://ithelp.ithome.com.tw/upload/images/20210909/2013519743AZq6gxeI.jpg

讓我們先把目光放在黃色的蔆形,在該流程圖中,蔆形是一個分歧點,如下圖,比方說,我們可以檢查使用者按下開始後,是否有先上傳音樂,如果有,就走方案A,反之就用方案B。而這張圖中則是利用監聽事件EventListener來等待使用者進行動作,(至於更多這方面的優化會等到第三周的時候,開始設計除錯機制,在資料有可能出錯的地方去作流程控制。)

https://ithelp.ithome.com.tw/upload/images/20210830/20135197W6iTkNyZKH.jpg

上傳音樂實作

那麼,從流程圖中,可以知道,用戶能做的動作有四種,分別為Play、Pause、Select、Upload,今天就從最基本也最重要的上傳開始吧!順便幫大家暖身一下(可能有些人疫情宅在家都懶了,很久沒寫程式XD)
https://ithelp.ithome.com.tw/upload/images/20210909/20135197u1suY2Vo63.jpg

<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取得完整的檔名(字串),只要把字串從中間的(.)給切開就能夠取得我們要的副檔名了,因此步驟很簡單:

  1. string.lastIndexOf方法取得(.)在字串中的索引(index)
  2. string.substring方法,透過索引(index)切開字串得到副檔名
  3. 如果附檔名不是我們預設的音樂附檔名,就跳出警示訊息
    (對這兩個方法不熟悉的朋友們,可以點進連結去看MDN的示例)
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)

這樣就可以搭配一開始給的流程圖來理解了,我們要設計一個流程為「當用戶上傳音樂檔案後,先取得該檔案的路徑,再檢查是不是音樂,最後設定音樂的路徑」,因此步驟就會很清楚:

  1. 先設計一個input欄位
  2. 事件監聽-當input取得檔案後(change),開始後續動作
    目標是誰---input
    什麼時候---change
    要做什麼---FileManager
  3. 取得路徑 URL.createObjectURL
  4. 檢查副檔名 Array
  5. 設定音樂路徑 audio.src

透過這個流程,可以清楚的知道分別需要那些語法來完成一連串的動作,這不管是在設計程式邏輯、或是在設計遊戲上都相當重要,可以先評估自己在哪個階段會遇到挑戰,雖然有些人天生邏輯很好,可以在腦內想得很清楚,不過在團隊合作中,溝通需要一個媒介,這也是LogicFlow存在的意義,不管是設計師、還是程序員,都應該懂得畫流程圖,

不過,對初學者來說肯定是邊做邊學習,可以先開始撰寫程式碼,到一個段落後,把邏輯試著畫成一張圖,講給別人聽,會是不錯的練習唷!

後記

其實我也是為了這次鐵人賽才畫圖的XD,選擇的是特別單純的畫法(也為了讓大家好懂),只有三個圖形,分別是流程起點/終點(圓角矩形)、處理流程(矩形)、抉擇點(稜形),之前獨立開發,都在自己腦內想好即可,不過,這次透過畫出來的方式,也讓我有了一次重構程式碼的機會,對於後續要加上的流程控制跟例外處理,更是能幫上大忙呢!

因為步驟講得比較仔細,篇幅拉長,感謝大家耐心觀看,希望能幫初學者解惑,如果有什麼問題歡迎在下面做詢問!

回家作業(思考題)

Q. 請問在今天已經完成的上傳流程中,在用戶的哪個操作、或是哪段程式碼可能會出現錯誤,甚至導致中斷呢?請試著在留言區回答吧!(提示:其實用戶不是你想的那麼聰明!)


上一篇
序章:最幸福的事,莫過於當你看到code變成一幅幅美麗的畫
下一篇
Chapter1-DJ最愛的音頻動感圖像(II)只要是認識Canvas的都覺得它很High歐
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31

尚未有邦友留言

立即登入留言