最近用Vue + Tone.js做了一款鋼琴類web應用,名字定為自由鋼琴(AutoPiano),人生如音樂,歡快且自由。 此文權當作該專案的總結和分享~
專案簡介
自由鋼琴(AutoPiano)是利用HTML5技術開發的線上鋼琴應用,致力於為鋼琴愛好者、音樂愛好者以及其他所有的創造者提供一個優雅、簡潔的平臺,在學習工作之餘可以享受鋼琴、音樂的美好。就類似於多年前Flash開發的鋼琴遊戲,自由鋼琴只是換了H5的技術,同時支援了鋼琴曲的自動播放功能。
AutoPiano支援鍵盤按鍵和滑鼠點選播放,同時琴鍵上會有按鍵和音名提示。另外,AutoPiano還有教學的功能,一種方式是快速入門
,通過簡易的譜子按鍵進行演奏,另一種是演奏示例
,通過鋼琴曲的自動播放來達到演示的目的。目前這兩個功能都在持續完善中,如下圖所示:
開發這樣的應用需要樂理知識嗎?
當然。基本的樂理知識還是要知道的,比如 CDEFGAB 音名、五線譜、調式、節奏等等還是要懂一點的。篇幅所限,這裡就不展開討論了,推薦兩個網站:
其他的就是程式設計知識了,以及如何將樂理知識轉化為程式邏輯。AutoPiano目前採用的技術架構是vue框架 + tone.js。
鋼琴介面效果是怎麼寫的?
可以用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上也有很多這樣的例子供參考,不一定採用上述實現:
相信只要合理地控制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
- 不能完全表示音樂的所有維度,比如很多鋼琴譜不止有兩個音軌
- 過於抽象和複雜,不實用,很難製作這種識別格式
- 音樂專業人士: what are you 弄啥嘞?
所以筆者轉向另一種實現思路,解析musicxml,但奈何這個過程耗時耗力,目前只完成了一半,部分細節還沒有完全解析正確,如果讀者有好的想法,可以在評論區留言探討。
歡迎貢獻協作
- 貢獻首頁展示的隨機歌詞: github.com/WarpPrism/A…
- 貢獻快速入門的彈奏方法: github.com/WarpPrism/A…
FORK時,請遵循GPL開源協議。
最後
最後再貼一下體驗地址: crystalworld.gitee.io/qpiano/#/
歡迎體驗,分享。
解析musicxml的過程仍在進行中,如果某一天成功了,那麼示例演奏裡面就會加入海量的歌曲,以供學習,如果失敗了,額,那就是因為生活阻擋了我奮進的腳步。。。
原創不易,轉載分享時請註明出處~