緣起
由于童心未泯,之前在手機(jī)上玩過鋼琴模擬App证逻,但是手機(jī)屏幕太小乐埠,始終覺得不過癮。其實(shí)對于我這個連基本樂理都不懂的“樂盲”來說囚企,就算給我一臺真正的鋼琴丈咐,我也玩不轉(zhuǎn)。不過是圖個新鮮龙宏、權(quán)當(dāng)娛樂罷了棵逊。最近剛好入手一臺帶觸摸屏的Lenovo Yoga 4 Pro,這倒給了我新的想象空間:大屏幕玩起來是不是更帶感银酗?在Win10應(yīng)用商店里搜了下辆影,還真有各種模擬鋼琴的應(yīng)用,隨便選了一款安裝黍特。結(jié)果非常令人失望蛙讥,音效慘不忍聽,還各種閃退灭衷。這里順便吐槽下win10的應(yīng)用商店次慢,里面的很多應(yīng)用不是經(jīng)常安裝失敗,就是經(jīng)常閃退今布,簡直沒法用啊经备。作為一名前端開發(fā)和堅(jiān)定的Web支持者,客戶端不好用果斷轉(zhuǎn)向Web啊部默。本著盡量不重造輪子的原則侵蒙,先在網(wǎng)上搜了一下。百度的搜索結(jié)果幾乎都是那一個例子傅蹂,也不知道是哪位哥們寫的纷闺,被到處引用。就那么幾個鍵份蝴,怎么玩犁功?Google的結(jié)果也不盡如人意,不是打不開就是加載半天婚夫。算了浸卦,還是自己動手吧。
準(zhǔn)備
我們知道案糙,HTML5有音頻接口限嫌,播放聲音自然不在話下靴庆。這模擬鋼琴自然需要各種音階的音頻文件吧,于是在網(wǎng)上搜了一通怒医,找齊了88鍵鋼琴的音頻文件炉抒。為什么鋼琴有88個鍵?別問我稚叹,我是樂盲焰薄。看看這張鋼琴示意圖就知道了:
開工
最近一直在用Vue.js開發(fā)項(xiàng)目扒袖,配合Webpack神器構(gòu)建打包塞茅,開發(fā)前端項(xiàng)目從來沒有如此方便。在此要特別感謝Vue.js的作者Evan You尤雨溪(知乎)僚稿, 給我們貢獻(xiàn)了這么好用的框架凡桥。
新建一個Vue.js項(xiàng)目非常簡單蟀伸,可以用官方推薦的腳手架命令行工具vue-cli創(chuàng)建新工程蚀同。首先安裝這個工具:
npm install -g vue-cli
安裝好后執(zhí)行命令生成工程模板:
vue init webpack piano
這里我們用webpack作為構(gòu)建工具,你也可以使用browserify啊掏。
就這么簡單蠢络,一個Vue.js project誕生了,而且Webpack已經(jīng)配置好迟蜜。接下來執(zhí)行命令安裝相關(guān)的node模塊:
npm install
如果一切順利的話刹孔,項(xiàng)目就可以跑起來了:
npm run dev
訪問http://localhost:8080就可以看到默認(rèn)的歡迎界面。至此娜睛,項(xiàng)目的搭建算是完成了髓霞。
界面
現(xiàn)在開始寫界面。雖然是樂盲畦戒,鋼琴鍵盤上有哪些鍵還是要搞清楚的方库。對于標(biāo)準(zhǔn)的88鍵鋼琴,總共有88個鍵障斋,其中52個白色鍵纵潦,36個黑色鍵。分為低音區(qū)垃环、中音區(qū)和高音區(qū)邀层,每個區(qū)有三組。對于我們畫界面來說遂庄,重要的是找出其中的規(guī)律寥院。最兩端的兩組先不管,其他的分組看上去都是一樣的:三白夾兩黑跟著四白夾三黑涛目。
怎么實(shí)現(xiàn)這個界面布局呢秸谢?很簡單经磅,黑白鍵都用button
元素表示,設(shè)置好寬高钮追、背景色和邊框预厌。白色的自然定位并排鋪開,黑色的用絕對定位元媚,計(jì)算出對應(yīng)的坐標(biāo)轧叽。這里有個小細(xì)節(jié),就是黑白鍵的DOM元素排列最好跟各音階的先后順序?qū)?yīng)刊棕,這樣在計(jì)算黑鍵坐標(biāo)就比較方便炭晒。
既然有七個組的界面是一模一樣的,我們就把一組設(shè)計(jì)成一個組件好了甥角。用Vue.js開發(fā)組件真的是太方便了网严,一個.vue文件包含HTML template、script和style嗤无,就構(gòu)成了一個獨(dú)立的組件震束。每組的音階范圍不一樣,通過組件的props
設(shè)定当犯。來看組件的源碼文件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>
邏輯并不復(fù)雜垢村,關(guān)鍵是處理細(xì)節(jié)。按鍵的寬度是用百分比的嚎卫,高度固定嘉栓。黑鍵的坐標(biāo)計(jì)算邏輯在方法calcLeft
里,具體看代碼好了拓诸,code will talk.
你可能有個疑問:音頻內(nèi)容哪來的侵佃?繼續(xù)看。
音頻處理
前面提到過奠支,我從網(wǎng)上找到了鋼琴的88音階的音頻文件馋辈,都是mp3格式的。但是我不想讓88個音分散在88個.mp3文件里胚宦,不然在彈奏的時候一個個文件下載首有,可不太好。怎么辦呢枢劝?我們知道圖片可以轉(zhuǎn)成base64的字符串顯示在DOM里井联。其實(shí)音頻文件也一樣,用data:audio/mpeg;base64,XXXXXX
就可以了您旁。寫了個Node程序烙常,一次性將所有Mp3文件都轉(zhuǎn)成了base64字符串?dāng)?shù)組備用:
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);
});
數(shù)組內(nèi)容放在一個單獨(dú)的文件里,作為模塊引入。數(shù)組元素的順序就是音階從低到高的順序蚕脏。HTML5的Audio對象侦副,支持從構(gòu)造函數(shù)傳入base64數(shù)據(jù),然后調(diào)用play()
就可以播放聲音了驼鞭。
沒有觸摸屏咋玩秦驯?還有鍵盤啊。簡單起見挣棕,用三排字母按鍵對應(yīng)中音區(qū)的三個組译隘。監(jiān)聽鍵盤keydown
事件,通過keyCode
區(qū)分不同的鍵洛心,播放對應(yīng)的音頻內(nèi)容就好了固耘。
總結(jié)
這個過程并不復(fù)雜,就是布局和音頻處理需要處理一些細(xì)節(jié)词身。代碼寫得很倉促厅目,有些地方可以重構(gòu)下。完整的源碼可以在我的Github找到法严。喜歡的歡迎star损敷,有閑工夫也可自己改進(jìn)。最終效果點(diǎn)擊這里:http://kaysonli.github.io/piano/dist/