原文連接css
最近用Vue + Tone.js作了一款鋼琴類web應用,名字定爲自由鋼琴(AutoPiano),人生如音樂,歡快且自由。 此文權看成該項目的總結和分享~html
自由鋼琴(AutoPiano)是利用HTML5技術開發的在線鋼琴應用,致力於爲鋼琴愛好者、音樂愛好者以及其餘全部的創造者提供一個優雅、簡潔的平臺,在學習工做之餘能夠享受鋼琴、音樂的美好。就相似於多年前Flash開發的鋼琴遊戲,自由鋼琴只是換了H5的技術,同時支持了鋼琴曲的自動播放功能。vue
AutoPiano支持鍵盤按鍵和鼠標點擊播放,同時琴鍵上會有按鍵和音名提示。另外,AutoPiano還有教學的功能,一種方式是快速入門
,經過簡易的譜子按鍵進行演奏,另外一種是演奏示例
,經過鋼琴曲的自動播放來達到演示的目的。目前這兩個功能都在持續完善中,以下圖所示:ios
體驗地址: www.autopiano.cngit
固然。基本的樂理知識仍是要知道的,好比 CDEFGAB 音名、五線譜、調式、節奏等等仍是要懂一點的。篇幅所限,這裏就不展開討論了,推薦兩個網站:github
其餘的就是編程知識了,以及如何將樂理知識轉化爲程序邏輯。AutoPiano目前採用的技術架構是vue框架 + tone.js。web
能夠用CSS或貼圖。筆者這裏直接用css實現了,考慮到鋼琴有黑鍵和白鍵,且黑鍵和白鍵有序地排列成 7:5的模式,因此實現起來並不複雜。編程
<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上也有不少這樣的例子供參考,不必定採用上述實現:bash
相信只要合理地控制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,但奈何這個過程耗時耗力,目前只完成了一半,部分細節尚未徹底解析正確,若是讀者有好的想法,能夠在評論區留言探討。
FORK時,請遵循GPL開源協議。
最後再貼一下體驗地址: www.autopiano.cn
歡迎體驗,分享。
解析musicxml的過程仍在進行中,若是某一天成功了,那麼示例演奏裏面就會加入海量的歌曲,以供學習,若是失敗了,額,那就是由於生活阻擋了我奮進的腳步。。。
原創不易,轉載分享時請註明出處~