用Vue.js和Webpack開發Web線上鋼琴
緣起
由於童心未泯,之前在手機上玩過鋼琴模擬App,但是手機螢幕太小,始終覺得不過癮。其實對於我這個連基本樂理都不懂的“樂盲”來說,就算給我一臺真正的鋼琴,我也玩不轉。不過是圖個新鮮、權當娛樂罷了。最近剛好入手一臺帶觸控式螢幕的Lenovo Yoga 4 Pro,這倒給了我新的想象空間:大螢幕玩起來是不是更帶感?在Win10應用商店裡搜了下,還真有各種模擬鋼琴的應用,隨便選了一款安裝。結果非常令人失望,音效慘不忍聽,還各種閃退。這裡順便吐槽下win10的應用商店,裡面的很多應用不是經常安裝失敗,就是經常閃退,簡直沒法用啊。作為一名前端開發和堅定的Web支持者,客戶端不好用果斷轉向Web啊。本著儘量不重造輪子的原則,先在網上搜了一下。百度的搜尋結果幾乎都是那一個例子,也不知道是哪位哥們寫的,被到處引用。就那麼幾個鍵,怎麼玩?Google的結果也不盡如人意,不是打不開就是載入半天。算了,還是自己動手吧。
準備
我們知道,HTML5有音訊介面,播放聲音自然不在話下。這模擬鋼琴自然需要各種音階的音訊檔案吧,於是在網上搜了一通,找齊了88鍵鋼琴的音訊檔案。為什麼鋼琴有88個鍵?別問我,我是樂盲。看看這張鋼琴示意圖就知道了:
開工
最近一直在用Vue.js開發專案,配合Webpack神器構建打包,開發前端專案從來沒有如此方便。在此要特別感謝Vue.js的作者Evan You尤雨溪(知乎), 給我們貢獻了這麼好用的框架。
新建一個Vue.js專案非常簡單,可以用官方推薦的腳手架命令列工具vue-cli建立新工程。首先安裝這個工具:
npm install -g vue-cli
安裝好後執行命令生成工程模板:
vue init webpack piano
這裡我們用webpack作為構建工具,你也可以使用browserify。
就這麼簡單,一個Vue.js project誕生了,而且Webpack已經配置好。接下來執行命令安裝相關的node模組:
npm install
如果一切順利的話,專案就可以跑起來了:
npm run dev
介面
現在開始寫介面。雖然是樂盲,鋼琴鍵盤上有哪些鍵還是要搞清楚的。對於標準的88鍵鋼琴,總共有88個鍵,其中52個白色鍵,36個黑色鍵。分為低音區、中音區和高音區,每個區有三組。對於我們畫介面來說,重要的是找出其中的規律。最兩端的兩組先不管,其他的分組看上去都是一樣的:三白夾兩黑跟著四白夾三黑。
怎麼實現這個介面佈局呢?很簡單,黑白鍵都用button
元素表示,設定好寬高、背景色和邊框。白色的自然定位並排鋪開,黑色的用絕對定位,計算出對應的座標。這裡有個小細節,就是黑白鍵的DOM元素排列最好跟各音階的先後順序對應,這樣在計算黑鍵座標就比較方便。
既然有七個組的介面是一模一樣的,我們就把一組設計成一個元件好了。用Vue.js開發元件真的是太方便了,一個.vue檔案包含HTML template、script和style,就構成了一個獨立的元件。每組的音階範圍不一樣,通過元件的props
設定。來看元件的原始碼檔案Group.vue
<template>
<div class="group">
<button :class="{'white': whites.indexOf(n) > -1, 'black': blacks.indexOf(n) > -1}" v-for="n in 12" :style="{ left: calcLeft(n) + '%' }" data-note="{{start+n}}" @click="play(start+n)"><span v-show="n === 0">C</span></button>
</div>
</template>
<script>
import {notes} from '../notes.js';
const prefix = 'data:audio/mpeg;base64,';
const base = 3;
const keys = 12;
export default {
props: {
group: {
type: Number,
default: 0
}
},
data() {
return {
// note: changing this line won't causes changes
// with hot-reload because the reloaded component
// preserves its current state and we are modifying
// its initial state.
blacks: [1, 3, 6, 8, 10],
whites: [0, 2, 4, 5, 7, 9, 11]
}
},
computed: {
start() {
return this.group * keys;
}
},
methods: {
play(index) {
var audio = new Audio(prefix + notes[index + base]);
audio.play();
},
calcLeft(index) {
var unit = 14.29;
var i = this.blacks.indexOf(index);
if(i < 2) {
return unit * (0.75 + i);
}
return unit * (1.75 + i);
},
click(index) {
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.group {
font-size: 0;
position: relative;
display: flex;
flex-grow: 1;
}
button {
width: 14.29%;
flex: 1;
height: 300px;
display: inline-block;
border: 1px solid #ccc;
outline: 0;
padding: 0;
box-sizing: border-box;
}
button > span {
position: absolute;
bottom: 10px;
}
.white:active,
.white.active {
background: #ececec;
}
.white {
background: #fff;
}
.black {
background: #000;
border-color: #000;
height: 150px;
width: 7.15%;
position: absolute;
}
</style>
邏輯並不複雜,關鍵是處理細節。按鍵的寬度是用百分比的,高度固定。黑鍵的座標計算邏輯在方法calcLeft
裡,具體看程式碼好了,code
will talk.
你可能有個疑問:音訊內容哪來的?繼續看。
音訊處理
前面提到過,我從網上找到了鋼琴的88音階的音訊檔案,都是mp3格式的。但是我不想讓88個音分散在88個.mp3檔案裡,不然在彈奏的時候一個個檔案下載,可不太好。怎麼辦呢?我們知道圖片可以轉成base64的字串顯示在DOM裡。其實音訊檔案也一樣,用data:audio/mpeg;base64,XXXXXX
就可以了。寫了個Node程式,一次性將所有Mp3檔案都轉成了base64字串陣列備用:
var fs = require('fs');
var file = 'notes.json';
// function to encode file data to base64 encoded string
function base64_encode(file) {
// read binary data
var bitmap = fs.readFileSync(file);
// convert binary data to base64 encoded string
return new Buffer(bitmap).toString('base64');
}
fs.readdir('.', function(error, files) {
var content = "";
files.forEach((f, index) => {
if(/^\d/.test(f)) {
var data = base64_encode(f);
content += `"${data}",\n`;
}
});
fs.writeFileSync(file, content);
});
陣列內容放在一個單獨的檔案裡,作為模組引入。陣列元素的順序就是音階從低到高的順序。HTML5的Audio物件,支援從建構函式傳入base64資料,然後呼叫play()
就可以播放聲音了。
沒有觸控式螢幕咋玩?還有鍵盤啊。簡單起見,用三排字母按鍵對應中音區的三個組。監聽鍵盤keydown
事件,通過keyCode
區分不同的鍵,播放對應的音訊內容就好了。
總結
這個過程並不複雜,就是佈局和音訊處理需要處理一些細節。程式碼寫得很倉促,有些地方可以重構下。完整的原始碼可以在我的Github找到。喜歡的歡迎star,有閒工夫也可自己改進。最終效果點選這裡:http://kaysonli.github.io/piano/dist/