原文連接css
Hate 996? Come Here & Relax~html
最近用Vue + Tone.js作了一款鋼琴類web應用,名字定爲自由鋼琴(AutoPiano),人生如音樂,歡快且自由。vue
此文權看成該項目的總結和分享~ios
自由鋼琴(AutoPiano)是利用HTML5技術開發的在線鋼琴應用,致力於爲鋼琴愛好者、音樂愛好者以及其餘全部的創造者提供一個優雅、簡潔的平臺,在學習工做之餘能夠享受鋼琴、音樂的美好。就相似於多年前Flash開發的鋼琴遊戲,自由鋼琴只是換了H5的技術,同時支持了鋼琴曲的自動播放功能。git
AutoPiano支持鍵盤按鍵和鼠標點擊播放,同時琴鍵上會有按鍵和音名提示。另外,AutoPiano還有教學的功能,一種方式是快速入門
,經過簡易的譜子按鍵進行演奏,另外一種是演奏示例
,經過鋼琴曲的自動播放來達到演示的目的。目前這兩個功能都在持續完善中,以下圖所示:github
體驗地址: http://crystalworld.gitee.io/...項目地址: https://github.com/WarpPrism/...web
固然。基本的樂理知識仍是要知道的,好比 CDEFGAB 音名、五線譜、調式、節奏等等仍是要懂一點的。篇幅所限,這裏就不展開討論了,推薦兩個網站:編程
其餘的就是編程知識了,以及如何將樂理知識轉化爲程序邏輯。AutoPiano目前採用的技術架構是vue框架 + tone.js。架構
能夠用CSS或貼圖。筆者這裏直接用css實現了,考慮到鋼琴有黑鍵和白鍵,且黑鍵和白鍵有序地排列成 7:5的模式,因此實現起來並不複雜。app
<div class="piano-key-wrap"> <div class="piano-key wkey" v-for="note in Notes" :key="note.keyCode" :data-keyCode = "note.keyCode" v-if="note.type=='white'" @click="clickPianoKey($event, note.keyCode)"></div> <div class="bkey-wrap bkey-wrap1"> <div class="piano-key bkey" v-for="note in Notes" :key="note.keyCode" :data-keyCode = "note.keyCode" v-if="note.type=='black' && note.id >= 36 && note.id <= 40" @click="clickPianoKey($event, note.keyCode)"></div> </div> </div>
.piano-wrap { width: 90%; margin: 20px auto; .piano-key-wrap { width: 100%; background: @dark; overflow: hidden; position: relative; .wkey { display: inline-block; width: 2.775%; height: 100%; margin: 0 auto; background: linear-gradient(white 10%, rgb(251, 251, 251) 92%, rgb(220, 220, 220) 93%, white 97%); border: solid 1px @dark; border-radius: 0 0 5px 5px; position: relative; &:active { background: linear-gradient(#eee 10%, #ddd 60%, #bbb 93%, #ccc 97%); } } .wkey-active { background: linear-gradient(#eee 10%, #ddd 60%, #bbb 93%, #ccc 97%); } .bkey-wrap { width: 20%; height: 0; position: absolute; top: 0; } .bkey-wrap1 {left: 0;} .bkey-wrap2 {left: 19.5%;} .bkey-wrap3 {left: 39%;} .bkey-wrap4 {left: 58.3%;} .bkey-wrap5 {left: 77.7%;} .bkey { display: inline-block; width: 10%; height: 70%; background: linear-gradient(#000 10%, rgb(86, 86, 86) 85%, #000 90%); border-radius: 0 0 3px 3px; position: absolute; top: 0; overflow: hidden; &:active { background: linear-gradient(rgb(86, 86, 86) 10%, #000 90%, #222 100%); } } .bkey-active { background: linear-gradient(rgb(86, 86, 86) 10%, #000 90%, #222 100%); } .bkey:nth-child(1) {left: 9%;} .bkey:nth-child(2) {left: 23%;} .bkey:nth-child(3) {left: 50%;} .bkey:nth-child(4) {left: 65%;} .bkey:nth-child(5) {left: 79%;} } }
codepen上也有不少這樣的例子供參考,不必定採用上述實現:
https://codepen.io/search/pen...
相信只要合理地控制css變量和數值,你們能作出更好的 Piano 界面。
實現音頻播放,最簡單的就是利用HTML5 中的 audio
標籤,經過觸發audio的play和pause方法,實現對音頻的控制,筆者一開始就是這麼實現的。
// <div class="audios-wrap" id="audios-wrap"> // <audio src="" id="preloadAudio" ref="preloadAudio"></audio> // </div> // 預先爲每一個音符都創建一個audio元素 initAudioDom() { var vm = this for (let i = 0; i< vm.Notes.length; i++) { var note = vm.Notes[i] $('.audios-wrap').append(`<audio src='${note.url}' hidden='true' data-id='audio${i}' class='audioEle'>`); } }, // 觸發某個audio元素的播放 playNote(url) { var vm = this if (!url || typeof url != 'string') return; var audios = $('.audioEle'); for (let i = 0; i< audios.length; i++) { let audio = audios[i]; if (audio.src.indexOf(url) > -1) { var cloneAudioNode = audio.cloneNode() cloneAudioNode.play() cloneAudioNode.remove() break; } } }
上述是個人第一種實現方式,即不一樣音符觸發不一樣audio的播放。以後也許是出於好奇,嘗試了 Tone.js,經過Tone.js + 內置採樣器實現對音頻播放更有效的控制,固然,其提供的不少複雜功能都還沒用上。。。
// 初始化合成器 this.synth = SmapleLibrary.load({ instruments: "piano" }).toMaster() // 合成器觸發音頻釋放 playNote(notename = 'C4', duration = '2n') { if (!this.synth) return this.synth.triggerAttackRelease(notename, duration); }
嗯,如今的代碼就符合音樂美學和代碼美學了,美滋滋。固然筆者也指望Tone.js能快點完善中文文檔,否則上手仍是很吃力的,感興趣的小夥伴能夠先去其官網研究一番。
這一部分應該是開發整個應用最難的地方了,由於音樂或者說曲譜自己是至關複雜的,根據百度百科的描述,五線譜起源於希臘,歷經上千年不斷完善才成爲如今的曲譜標準。而簡譜的出現則要晚的多,但依然五臟俱全,能夠說,簡譜也不簡單。
筆者的實現思路是,以一種曲譜格式爲載體,將曲譜轉換爲一種程序可識別的格式,而後導入到程序中進行播放,這種可識別格式以下所示,也是目前所採用的:
{ name: '小星星', step: 'C', speed: '100', playState: '', mainTrack: ['1(1)',' 1(1)',' 5(1)',' 5(1)',' 6(1)',' 6(1)',' 5(2)',' 4(1)',' 4(1)',' 3(1)',' 3(1)',' 2(1)',' 2(1)',' 1(2)',' 5(1)',' 5(1)',' 4(1)',' 4(1)',' 3(1)',' 3(1)',' 2(2)',' 5(1)',' 5(1)',' 4(1)',' 4(1)',' 3(1)',' 3(1)',' 2(2)',' 1(1)',' 1(1)',' 5(1)',' 5(1)',' 6(1)',' 6(1)',' 5(2)',' 4(1)',' 4(1)',' 3(1)',' 3(1)',' 2(1)',' 2(1)',' 1(2)', '1<(1)', '1<(1)', '5<(1)', '5<(1)', '6<(1)', '6<(1)', '5<(2)', '4<(1)', '4<(1)', '3<(1)', '3<(1)', '2<(1)', '2<(1)', '1<(2)', '5<(1)', '5<(1)', '4<(1)', '4<(1)', '3<(1)', '3<(1)', '2<(2)', '5<(1)', '5<(1)', '4<(1)', '4<(1)', '3<(1)', '3<(1)', '2<(2)', '1<(1)', '1<(1)', '5<(1)', '5<(1)', '6<(1)', '6<(1)', '5<(2)', '4<(1)', '4<(1)', '3<(1)', '3<(1)', '2<(1)', '2<(1)', '1<(2)'], backingTrack: ['1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '6>(0.5)', '4>(0.5)', '6>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '6>(0.5)', '4>(0.5)', '6>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '7>>(0.5)', '5>(0.5)', '2>(0.5)', '5>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '3>(0.5)', '5>(0.5)',' 1(0.5)', '1>(0.5)', '4>(0.5)', '6>(0.5)',' 1(0.5)', '1>(0.5)', '3>(0.5)', '5>(0.5)',' 1(0.5)', '5>>(0.5)', '7>>(0.5)', '2>(0.5)', '5>(0.5)', '1>(0.5)', '3>(0.5)', '5>(0.5)',' 1(0.5)', '1>(0.5)', '4>(0.5)', '6>(0.5)',' 1(0.5)', '1>(0.5)', '3>(0.5)', '5>(0.5)',' 1(0.5)', '5>>(0.5)', '7>>(0.5)', '2>(0.5)', '5>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '6>(0.5)', '4>(0.5)', '6>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1>(0.5)', '6>(0.5)', '4>(0.5)', '6>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '7>>(0.5)', '5>(0.5)', '2>(0.5)', '5>(0.5)', '1>(0.5)', '5>(0.5)', '3>(0.5)', '5>(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '6(0.25)', '4(0.5)', '6(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '6(0.25)', '4(0.5)', '6(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '7>(0.75)', '5(0.25)', '2(0.5)', '5(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '3(0.25)', '5(0.5)', '1<(0.5)', '1(0.75)', '4(0.25)', '6(0.5)', '1<(0.5)', '1(0.75)', '3(0.25)', '5(0.5)', '1<(0.5)', '5>(0.75)', '7>(0.25)', '2(0.5)', '5(0.5)', '1(0.75)', '3(0.25)', '5(0.5)', '1<(0.5)', '1(0.75)', '4(0.25)', '6(0.5)', '1<(0.5)', '1(0.75)', '3(0.25)', '5(0.5)', '1<(0.5)', '5>(0.75)', '7>(0.25)', '2(0.5)', '5(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '6(0.25)', '4(0.5)', '6(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '1(0.75)', '6(0.25)', '4(0.5)', '6(0.5)', '1(0.75)', '5(0.25)', '3(0.5)', '5(0.5)', '7>(0.75)', '5(0.25)', '2(0.5)', '5(0.5)', '1>(2)'] }
額,是否是很複雜,很臃腫。。。它以簡譜爲載體,經過特殊符號來標記音高和時長,從而產生mainTrack和backingTrack兩個音軌,而後同步播放便可。這種實現雖然簡單,但有不少致命缺點:
因此筆者轉向另外一種實現思路,解析musicxml,但奈何這個過程耗時耗力,目前只完成了一半,部分細節尚未徹底解析正確,若是讀者有好的想法,能夠在評論區留言探討。
沒想到短期內能有這麼多star(`・ω・´),嚇得晚上下班回去又繼續碼代碼。。。不過此項目仍不完善,還在不斷更新中,特別是入門彈奏譜子比較少,目前只有:
都是筆者一個一個手打出來的T_T,能力有限,會的就這麼多,因此是時候見證社區的力量
了。
FORK時,請遵循GPL開源協議。
最後再貼一下體驗地址: http://crystalworld.gitee.io/...
歡迎體驗,分享。
解析musicxml的過程仍在進行中,若是某一天成功了,那麼示例演奏裏面就會加入海量的歌曲,以供學習,若是失敗了,額,那就是由於生活阻擋了我奮進的腳步。。。
原創不易,轉載分享時請註明出處~