Vue.js + Tone.js 開發Web鋼琴應用

稜鏡_jh發表於2019-04-01

原文連結

最近用Vue + Tone.js做了一款鋼琴類web應用,名字定為自由鋼琴(AutoPiano),人生如音樂,歡快且自由。 此文權當作該專案的總結和分享~

專案簡介

自由鋼琴(AutoPiano)是利用HTML5技術開發的線上鋼琴應用,致力於為鋼琴愛好者、音樂愛好者以及其他所有的創造者提供一個優雅、簡潔的平臺,在學習工作之餘可以享受鋼琴、音樂的美好。就類似於多年前Flash開發的鋼琴遊戲,自由鋼琴只是換了H5的技術,同時支援了鋼琴曲的自動播放功能。

AutoPiano支援鍵盤按鍵和滑鼠點選播放,同時琴鍵上會有按鍵和音名提示。另外,AutoPiano還有教學的功能,一種方式是快速入門,通過簡易的譜子按鍵進行演奏,另一種是演奏示例,通過鋼琴曲的自動播放來達到演示的目的。目前這兩個功能都在持續完善中,如下圖所示:

autopiano.png

體驗地址: crystalworld.gitee.io/qpiano/#/

開發這樣的應用需要樂理知識嗎?

當然。基本的樂理知識還是要知道的,比如 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上也有很多這樣的例子供參考,不一定採用上述實現:

codepen.io/search/pens…

相信只要合理地控制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兩個音軌,然後同步播放即可。這種實現雖然簡單,但有很多致命缺點:

  1. 不相容通用的計算機樂譜格式,如musicxml
  2. 不能完全表示音樂的所有維度,比如很多鋼琴譜不止有兩個音軌
  3. 過於抽象和複雜,不實用,很難製作這種識別格式
  4. 音樂專業人士: what are you 弄啥嘞?

所以筆者轉向另一種實現思路,解析musicxml,但奈何這個過程耗時耗力,目前只完成了一半,部分細節還沒有完全解析正確,如果讀者有好的想法,可以在評論區留言探討。

歡迎貢獻協作

FORK時,請遵循GPL開源協議。

最後

最後再貼一下體驗地址: crystalworld.gitee.io/qpiano/#/

歡迎體驗,分享。

解析musicxml的過程仍在進行中,如果某一天成功了,那麼示例演奏裡面就會加入海量的歌曲,以供學習,如果失敗了,額,那就是因為生活阻擋了我奮進的腳步。。。

1554105945684.jpg

原創不易,轉載分享時請註明出處~

相關文章