我今天要和你們分享的是一個我本身寫的音樂網頁小程序,這個網頁程序主要分爲兩個部分--即時演奏(LivePlay)和編曲(Arranger)。即時演奏就是指按下鼠標/鍵盤/手機屏幕就能夠即刻發聲,編曲是指提早寫好「譜子」而後播放。javascript
這個音樂程序如今僅有網頁版,因爲我使用Javascript(和HTML,CSS)寫成,因此理論上未來它能夠移植到Android和iOS上,也能夠改爲電腦程序,固然也能夠改裝成微信小程序!html
我是一個Javascript和Web初學者,這個音樂小程序並不複雜,因此若是有喜歡音樂,或在學習Web前端,學習canvas繪圖的朋友,你們能夠一塊兒探討程序的機理,體悟美妙的音樂!前端
網頁示範: https://sien75.github.io/MusicMaker/liveplay , 在瀏覽器中打開就能夠啦(ie,edge除外,手機記得橫屏)html5
個人github主頁就是 https://github.com/sien75,看完整代碼來這裏就能夠,歡迎加星,不勝感激^ >< ^java
最初,學校的C++課程有寫程序的課題任務,我萌生了作一個編曲&即時演奏的音樂程序的念頭,因而我在網上不斷查找,找到了MIDI(Musical Instrument Digital Interface,樂器的數字接口)這玩意。使用Windows的MIDI消息api--midiOutShortMSG(...),能夠發送MIDI消息,而後Windows利用自帶的MIDI音色庫生成聲音。我花費了一個月的時間,用MFC實現了一個簡陋的音樂程序。以後,我想進一步把這個程序寫下去,使程序更完善,可是我發現本身寫的爛代碼本身根本不肯回顧……c++
並且MFC是一個比較老的東西了,因此我想丟掉以前的代碼,從新寫一個程序(話說在我不停地「備份-格式化磁盤-換系統」中,那份原始代碼終於被我刪掉了……)。我想我不是已經會c++了嘛,因此我最初嘗試用Qt寫。然而我發現Qt沒有關於MIDI的api,我也在網上搜索了好一陣子,也沒有找到合適的第三方庫,因而就不了了之了。git
還有我想實現跨平臺的程序,既然Qt & C++不能用了,我想繼續用C#寫下去。緣由以下:1 C#看起來和C++挺像的,應該容易學習;2 VisualStudio + C#號稱天下無敵宇宙第一,且跨平臺很輕鬆;3 C#也可使用Windows的MIDI api,我不用再愁發不出聲的問題了;4 看看「C#」這名字,命名人確定很喜歡音樂,這個語言寫音樂程序確定很適合。github
然而以後再次放棄,具體緣由忘記了,多是我一直想學習Web安全領域,因此我火燒眉毛要開始前端之路了。因而花費了一些時間學習HTML,學習CSS,學習Javascript(強烈推薦《Javascript高級程序設計》)。web
據說w3c有個Web MIDI Api,我想:何不用這個東西實現音樂程序呢?並且這個瀏覽器自己就是跨平臺的,這樣正好符合個人要求。然而Web MIDI Api是爲了在瀏覽器上使用MIDI硬件設備的,並不能直接解決個人問題。與是我又花了很長時間,不停地找,無數次想放棄,可是最終,我找到了一個perfect的東西(大神的東西……) https://github.com/surikov/webaudiofont。canvas
這不是MIDI,MIDI發聲原理是主控器(好比MIDI鍵盤)發送信號,經音序器(Sequencer)處理,使內置音樂播放器調用音源,進而使揚聲器發聲。因此MIDI傳輸的是數字符號,用來表示音樂的起伏。這個庫就是模仿的這一過程,咱們能夠經過鍵盤鼠標手機觸摸屏(至關於主控器)進行編輯,而後經過html5的Web Audio Api(至關於Windows的內置音樂播放器)播放音源發聲,這裏的音源文件,那位大神也已經準備好了,https://github.com/surikov/webaudiofontdata,這裏面有一百多種樂器的音源(即MIDI的那些標準樂器,好比鋼琴吉他貝斯尺八)。而這個庫就是一個Javascript版的音序器,它已經能夠實現發出不一樣聲調不一樣音色的聲音的功能。
因而,我就開始寫代碼,以後的事情有章可循,比以前的迷茫要好一些了。
接下來我就說一下這個程序的具體代碼,閱讀前確保您已掌握HTML,CSS,Javascript,HTML5 canvas繪圖和一些音樂基本知識。
程序分爲兩個部分,即時演奏(LivePlay)和編曲(Arranger),目前只實現了LivePlay模塊,Arranger正在碼代碼中。來看一下LivePlay模塊的使用,放圖片:
如圖,界面中心是五個鍵組,每一個鍵組有7個白鍵,因此一共有35個白鍵,分別表明音調C2 D2 E2 F2 G2 A2 B2 C3 D3 ... A6 B6。其中C4~B4便是一般所說的do re mi fa so la si啦。除了白鍵,還有25個黑鍵,這些就是相應的半調C# D# F# G# A#了。用鼠標點擊黑白鍵,或點擊後拖動,皆可發出聲音。用鍵盤控制方法以下:
按下對應的鍵,就能夠發聲。K鍵或左方向鍵能夠向左切換鍵組,同理L鍵或右方向鍵能夠向右切換鍵組。鍵組從小字一組切換到小字二組的示意圖以下:
切換鍵組後鍵盤上相應的12個鍵就能夠控制當前鍵組的12個音調了。
因爲手機沒有鍵盤,因此不存在切換鍵組的問題,可是使用的時候記得橫屏。
界面上部有4個下拉框,分別能夠改變音色,八度升降,鍵盤控制的鍵組和鍵組數目,這些改變是顯而易見的,你們本身試一試吧。
最後,右上角的swith to arranger能夠跳轉到本程序的編曲(Arranger)部分(正在施工中)。
這是本程序的代碼根目錄,其中arranger和liveplay即爲程序的兩個主要模塊,sound存放音源,browser.js用於檢測客戶端類型(主要看看是否是在用手機瀏覽本站),index.html是程序主頁(固然這個主頁如今沒什麼用,會自動跳轉到liveplay/index.html),webaudiofontplayer.js是js音序器。
在liveplay裏,有7個文件:
首先,index.html是網頁入口。main.js的功能是定義頁面整體設置函數和初始化函數,三個「eventhandler」文件是處理事件(好比下拉框的選項選擇啦,鍵盤按下啦……)的,而後myAudio.js和myCanvas.js分別定義了MyAudio()和MyCanvas()兩個構造函數,分別用於處理聲音和繪圖部分。網頁運行流程以下:
剛打開時會運行main.js中的init()函數,該函數進行整體設置的初始化,並分別調用myAudio.js和myCanvas.js中的初始化函數進行聲音部分和繪圖部分的初始化,初始化完畢後,程序等待用戶事件的發生。若是用戶在電腦端按下鍵盤或用鼠標點擊琴鍵,會觸發PCEventHandlers.js中的響應函數;若是用戶在手機端觸摸琴鍵區,會觸發mobileEventHandlers.js中的響應函數;若是用戶操做下拉框,會觸發eventHandlers.js中的響應函數。全部響應函數會實際上調用main.js,myAudio.js或myCanvas.js中的函數進行具體的操做,以完成所需效果。
你們想,這個程序顯示上最重要的就是canvas區域,而聲音不須要顯示區域,因此,index.html文件仍是很是簡短的。在index.html中,主要的就有四個select標籤控制音色,八度,鍵盤所控鍵組和鍵組數目,和一個canvas標籤。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, 6 maximum-scale=1.0, user-scalable=no"> 7 <title>LivePlay 即時演奏</title> 8 </head> 9 <body style="user-select:none; margin:0; overflow:hidden; background:#222; 10 font-family:'Lucida Console',Monaco,monospace"> 11 <div style="margin-bottom:5px"> 12 <h1 style="font-size:25px; color:#888; display:inline-block; margin:4px 0 0 5px; 13 border:3px solid; border-radius:5px">MusicMaker</h1> 14 <h1 style="font-size:28px; display:inline-block; color:#888; margin:6px 20px 0 0;">LivePlay</h1> 15 <select id="selectInstruments"> 16 <option value="0000_Aspirin:n">piano</option> 17 <option value="0390_Aspirin:y">bass</option> 18 </select> 19 <select id="octive"> 20 <option value="0" id="o0">八度:0</option> 21 <option value="1" id="o1">八度:+1</option> 22 <option value="-1" id="on1">八度:-1</option> 23 <option value="2" id="o2">八度:+2</option> 24 <option value="-2" id="on2">八度:-2</option> 25 </select> 26 <select id="keyboardGroup"> 27 <option value="0" id="k1">鍵盤控制小字一組</option> 28 <option value="-2" id="k4">鍵盤控制大字組</option> 29 <option value="-1" id="k2">鍵盤控制小字組</option> 30 <option value="1" id="k3">鍵盤控制小字二組</option> 31 <option value="2" id="k5">鍵盤控制小字三組</option> 32 </select> 33 <select id="groupNum"> 34 <option value="5" id="g5">鍵組數目:5</option> 35 <option value="4" id="g4">鍵組數目:4</option> 36 <option value="3" id="g3">鍵組數目:3</option> 37 <option value="2" id="g2">鍵組數目:2</option> 38 <option value="1" id="g1">鍵組數目:1</option> 39 </select> 40 <span id="loading" style="font-size:20px; color:#f22; margin-left:10px; display:none ">Loading...</span> 41 <a href="../arranger/index.html" style="font-size:15px; color:#888; 42 float:right; margin-top:20px" id="switch">switch to arranger</a> 43 </div> 44 <canvas id="canvas"></canvas> 45 <script type="text/javascript" src="../browser.js"></script> 46 <script type="text/javascript" src="../webAudiofontPlayer.js"></script> 47 <script type="text/javascript" src="myCanvas.js"></script> 48 <script type="text/javascript" src="myAudio.js"></script> 49 <script type="text/javascript" src="main.js"></script> 50 <script type="text/javascript" src="eventHandlers.js"></script> 51 <script type="text/javascript" src="PCEventHandlers.js"></script> 52 <script type="text/javascript" src="mobileEventHandlers.js"></script> 53 </body> 54 </html>
index.html很是簡單,第5,6行是禁止手機瀏覽器雙擊放大和雙指放大的。
第9行的user-select:none,是禁止鼠標選取內容的,本程序使用過程當中會拖動鼠標,因此咱們必須禁止默認的拖動選中。
第44行就是一個canvas畫布,咱們會在js裏對其進行設置,咱們接下來的很大一部分工做就是針對這個畫布的。
接下來咱們就來分析js文件。
main.js
剛纔說過,main.js有兩個部分,初始化函數的定義和整體設置函數的定義。圖示,這兩部分,分別有3個函數:
這是一個初始化的大致的流程圖,紅色箭頭表明初始化流程進行路線,黑色箭頭表明初始化函數的調用狀況。
handleOctive()和handleKeyboardGroup()兩個函數要按照當時的鍵組數目進行調整,先討論handleOctive()。
咱們一共有60個鍵,分別對應音值24~83這60個音調。若是調整鍵組數目爲4個,那麼就會有12*4 = 48個鍵,它們對應24~71這48個音調,因此這時能夠升八度,使其對應於36~83這48個音調。若是調整鍵組數目爲3個,那麼就會一共有12*3 = 36個鍵,初始對應於36~71這36個音調,因此既能夠升八度到48~83,也能夠降八度到24~59。
那麼,鍵組數目與能夠升降八度的狀況有以下對應:
5 ~ 無; 4 ~ (+1); 3 ~ (-1, +1); 2 ~ (+2, -1, +1); 1 ~ (-2, +2, -1, +1)
因此咱們定義以下數組:
var octs = ['n2', '2', 'n1', '1'];
實現按照順序隱藏或顯示相應的八度調整選項。
再討論handleKeyboardGroup()。
這個就更好理解了,有幾個鍵組,電腦鍵盤就能夠控制幾個鍵組。(注:這五個鍵組名字依次爲「大字組」,「小字組」,「小字一組」,「小字二組」。「小字三組」)
handleOctive()和handleKeyboardGroup()主要是在調整下拉框的內容,好比鍵組數目爲4時,那麼屏幕上有「大字組」,「小字組」,「小字一組」和「小子二組」,這時屏幕上並無「小字三組」,控制鍵盤所選鍵組下拉框裏再顯示「鍵盤控制小字三組」,就不合適了。
eventHandlers.js
這個文件包含4個下拉框的響應函數。另外,它還包含一些全局變量和全局函數的定義,用於PCEventHandlers.js和mobileEventHandlers.js中的響應函數。
4個onchange響應函數很簡單,沒什麼好說的。
我把這些全局變量和全局函數集中到這裏,是爲了方便管理與查看,因爲是全局的,因此另外兩個文件(PCEventHandlers.js和mobileEventHandlers.js)的響應函數照樣可使用。
clickOn:鼠標按到琴鍵上,值變爲true;鼠標擡起,值變爲false。當鼠標拖動時,利用該值能夠判斷用戶是否在「按着琴鍵拖動」
positionListener:當鼠標按下並拖動時,positionListener.a用於記錄上一個位置的對應音調值,以判斷當前位置相對於上一個位置是否變化了琴鍵(把它定義爲Object是爲了按引用傳遞^~^)
noteRecord,rectRecord:當前鼠標點擊或拖動的位置會有對應音調和對應琴鍵區域的兩個值,記錄於這兩個變量,這兩個值分別傳遞到聲音和繪圖相關函數便可發出聲音和顏色變換
noteOnJudge:這是記錄鍵盤上12個音調鍵按下或擡起的變量,擡起則爲0,按下則爲1
keyUpAndDownTable:這裏面的十二個值記錄着鍵盤上A,W,S,E,D,F,T,G,Y,H,U和J的鍵盤碼,按照順序,分別表明C,C#,D,D#,E,F,F#,G,G#,A,A#和B這12個音調
computerKeyboardGroup:記錄當前電腦鍵盤控制的鍵組,中央C鍵所在鍵組爲0,中央鍵組左鄰居鍵組爲-1,再往左爲-2,右邊爲正,當有4個鍵組時,相應鍵組值如圖所示:
noteRecordRect:用於觸控時,記錄某音調對應的琴鍵區域
getPos:轉換座標
PCEventHandlers.js
這個文件包含着3個鼠標響應函數,和2個鍵盤響應函數。
對於3個鼠標事件(按下,拖動和擡起),咱們但願:按下時打開音調,琴鍵區域塗成彩色;按住並拖動致變換琴鍵區域時,關閉上一個音調,打開當前音調,將前一個區域塗成黑色或白色,當前區域塗成彩色;擡起時關閉音調,並將當前琴鍵區域塗回黑色或白色。
打開音調和將當前琴鍵區繪製成彩色的兩個函數以下:
1 myAudio.startNote(note); 2 myCanvas.paintKey(rect, 'click');
關閉音調和將當前琴鍵區塗回黑色或白色的函數以下:
1 myAudio.stopNote(note); 2 myCanvas.paintKey(rect, 'release');
在以上幾個函數中,參數note是一個整形值,範圍是24~83,表明音調;參數rect是一個對象,裏面包含了記錄琴鍵的區域的數值,和顏色數值,這個對象的結構咱們要到myCanvas()中具體說。
如下語句
1 clickOn = true;
2 clickOn = false;
第1行是在onmousedown()中的語句,第二行是在onmouseup()中的語句。clickOn就是前面eventHandlers.js中的全局變量,clickOn爲true時,表明鼠標已經按下而且按到了琴鍵區域,這時只要鼠標掃過不一樣的琴鍵區域,就會發聲。
下面第一個函數能夠將鼠標的位置點轉換成音調值,而第二個函數能夠將音調值轉換成相應的琴鍵區域。
1 myCanvas.positionToNote(pos.x, pos.y), 2 myCanvas.noteToRect(note);
下面這個函數是檢測拖動時鼠標位置是否在改變琴鍵區域,好比鼠標點擊到了C鍵,再拖動到了D鍵,在鼠標剛剛到達D鍵時,此時下面的函數返回true,其餘時候返回false。按在C鍵而只在C鍵區域內移動,並非真正的移動,此時下面的函數時時返回false。此外,當鼠標點移出琴鍵區或從「外面」移到琴鍵區域時,也視爲改變了琴鍵區域,下面的函數也會在改變的瞬刻返回true。
1 myCanvas.ifPositionChanged(pos.x, pos.y, positionListener);
對於2個鍵盤事件(按下和擡起),咱們但願按下時打開音調,將當前琴鍵區塗成彩色;擡起時關閉音調,將琴鍵區域塗回黑白色。
在eventHandlers.js中,咱們定義了keyUpAndDownTable用於按順序從0~11存放了A,W,S,E,D,F,T,G,Y,H,U和J這些「音調鍵」的鍵盤碼;還定義了noteOnjudge,在這裏noteOnJudge(0) = 1表明A鍵處於按下的狀態,noteOnJudge(4) = 0表明D鍵處於擡起的狀態。noteOnJudge用處是這樣的:在有音調鍵按下時,不容許切換鍵組,即此時按「K」,「L」,左方向鍵或右方向鍵不起做用。這樣作的目的是防止「卡鍵「--鍵組移走了,音調就沒法關閉了。
onkeydown函數有3部分,按下「K「或左方向鍵,且全部音調鍵擡起,向左切換鍵組;按下」L「或右方向鍵,且全部音調鍵擡起,向右切換鍵組;按下音調鍵,打開音調,琴鍵區域繪成彩色。
其中的
1 myCanvas.paintIndicator(computerKeyboardGroup);
是繪製指示符的。指示符就是屏幕上當前鍵組上方的三個紅綠藍色的四分之三圓,用來指示當前鍵組。
onkeyup函數只有1個部分,擡起音調鍵,關閉音調,琴鍵區域恢復到黑色或白色。
mobileEventHandlers.js
這個文件包含着3個觸摸響應函數。
上面的兩個preventDefault是分別爲了阻止手機瀏覽器上滾動事件和長按彈出菜單事件,這兩個事件都會影響使用效果。
3個響應函數分別處理觸摸開始,滑動和觸摸結束。固然,觸摸開始的時候打開音調,琴鍵塗成彩色;觸摸結束時關閉音調,琴鍵塗成黑色或白色。
重點看一下canvas.ontouchmove這個函數,我以爲這是響應函數中最難實現的一個。先貼代碼:
1 canvas.ontouchmove = function() { 2 var pos, trues = new Array(); 3 for (var i = 0; i < event.targetTouches.length; i++) { 4 pos = getPos(event.targetTouches[i]); 5 var n = myCanvas.positionToNote(pos.x, pos.y), 6 r = myCanvas.noteToRect(n); 7 if(trues.indexOf(n) < 0) trues.push(n); 8 if(!noteRecordRect[n]) { 9 noteRecordRect[n] = true; 10 myAudio.startNote(n); 11 myCanvas.paintKey(r, 'click'); 12 } 13 } 14 for( var i=24; i < 84; i++) 15 if(noteRecordRect[i] && trues.indexOf(i) < 0) { 16 myAudio.stopNote(i); 17 myCanvas.paintKey(myCanvas.noteToRect(i), 'release'); 18 noteRecordRect[i] = false; 19 } 20 };
函數有兩個大部分,分別是4~14行和15~20行的for語句。
event.targetTouches表明屏幕區域的全部觸摸點(此外event.changedTouches表明變化的觸摸點,注意區分),trues數組會記錄此次拖動事件的全部手指激活的琴鍵的音調,而此前在eventHandlers.js中定義的noteRecordRect數組則是記錄的直到上次拖動事件全部手指激活的琴鍵的音調。那麼,第8行的意思是:上次拖動事件手指未到達本琴鍵區域,可是此次到達了——這就是說手指剛剛觸摸本琴鍵,因此這時打開音調,琴鍵繪製彩色。第15行的意思是:雖然上次手指觸摸了本琴鍵區域,可是此次卻沒有——這就是說手指剛剛離開本琴鍵,因此這時關閉音調,琴鍵繪製回黑色或白色。
這個「觸摸拖動」響應函數,和「鼠標拖動」響應函數不一樣的一點在於能夠多點拖動。這裏不是很好懂,我也不太好敘述出來,你們能夠本身琢磨琢磨^ >< ^。
myAudio.js
這個文件裏存的就是管理聲音的構造函數了,小夥伴們能夠看一下,如何藉助webAudiofontPlayer庫的api,進行聲音操做。
你們都知道,js能夠用構造函數生成對象,在這裏就能夠用
1 var myAudio = new MyAudio();
這句來實現。
構造函數內部有一些內部變量,和一些函數。this.init函數就是在main.js中init()調用的聲音部分初始化函數,this.importScript會調用this.loadScript,完成引入並解碼音源文件的任務,this.setOrGetOctive能夠設置或得到當前的八度值,this.startNote和this.stopNote則是打開和關閉單一音調的。
最簡單的狀況下,webAudiofontPlayer如下列方式實現音調的打開和關閉。
1 var AudioContextFunc = window.AudioContext || window.webkitAudioContext; 2 var audioContext = new AudioContextFunc(); 3 var player=new WebAudioFontPlayer(); 4 player.loader.decodeAfterLoading(audioContext,' 5 _tone_0250_SoundBlasterOld_sf2);//解碼 6 var a = player.queueWaveTable(audioContext, audioContext.destination 7 , _tone_0250_SoundBlasterOld_sf2, 0, 12*4+7, 2);//打開音調,最後面三個參數分別是起始播放時間,音調高低,音量
8 a.cancel();//關閉音調
音源文件的加載過程有可能花費一些時間,this.loadScript函數會在url指向的音源文件加載完成後再調用callback函數。咱們能夠再this.importScript函數中看到下面這段代碼:
1 this.loadScript('../sound/'+ tag + '_sf2_file.js', function() { 2 player.loader.decodeAfterLoading(audioContext, '_tone_' + tag + '_sf2_file'); 3 loadedInstruments[loadedInstruments.length] = tag; 4 document.getElementById('loading').style.display = 'none'; 5 });
在這裏咱們在this.importScript中調用了this.loadScript函數,在加載完成" '../sound/' + tag + '_sf2_file.js' "文件後執行後面的函數。後面的函數中,第一句是解碼剛剛加載的音源文件;第二句是將已經加載的樂器音源文件記錄在loadedInstruments數組中,待下次須要使用該樂器時避免重複加載;第三句是隱藏掉頁面上的loading標誌,告知用戶資源加載完畢,可使用了。
在myAudio.js中還有一個continuousTable,這個數組用來表示樂器的連續性問題。好比鼓,打擊一下只會相對瞬時響一聲,而且存在回聲;但要是口琴就會有一個時間延續問題。因此,若是樂器是連續的,咱們能夠先將播放時間設置爲999秒,帶用戶擡起鼠標或鍵盤時使用cancel()方法,關閉音調;若是樂器是不連續的,咱們能夠規定一個時間,只要按下鍵盤或鼠標,即打開音調,時間到了自動中止,要使它再次打開須要再次激發。