1. 程式人生 > >用Vue.js和Webpack開發Web線上鋼琴

用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/