前言 前段時間一時興起想學一下吉他,但是一門樂器要演奏成“能聽”的程度也不是一天兩天的事情,對我這種音樂基礎為 0 的人來說學習周期太長了,不想耗費太多時間在學習樂器上面,於是想找個取巧的方法。 最終方案就是做了個簡單粗陋的微信小程式 Demo 去彈奏吉他樂,勉強算是成功吧,可以很簡單地彈奏出樂曲。 ...
前言
前段時間一時興起想學一下吉他,但是一門樂器要演奏成“能聽”的程度也不是一天兩天的事情,對我這種音樂基礎為 0 的人來說學習周期太長了,不想耗費太多時間在學習樂器上面,於是想找個取巧的方法。
最終方案就是做了個簡單粗陋的微信小程式 Demo 去彈奏吉他樂,勉強算是成功吧,可以很簡單地彈奏出樂曲。
大概一小時的學習周期(含學習簡譜時間),可以彈出傳統樂器學習一兩個月的效果。
但是這個小程式有一個 BUG ,導致它也只能算是一個失敗品。
文章最後會附上開源代碼地址,且容我先拋出這塊磚,希望能引來玉吧。
使用
這裡貼出微信小程式的二維碼,小程式的名字叫冒牌吉他手,大家可以體驗一下。
主界面
本小程式有四個模式:自由模式、簡單模式、靈魂模式和自動模式。
在自由模式下,您可以按 1,2,3,4,5,6,7 等七個音符按鍵,分別對應 do(哆)、re(來)、mi(咪)、fa(發)、sol(唆)、la(拉)、si(西)。同時可以切換高低音和節拍按鍵來分別控制音高和音長。
在簡單模式下,程式會自動根據簡譜識別出音高和音長,您只需要按七個音符按鍵即可。預設設置的簡譜是《成都》。
在靈魂模式下,程式會自動根據簡譜識別出音高、音長和音符,您只需要按一個音符按鍵即可。
在自動模式下,程式會自動根據簡譜彈奏音樂,您無需操作。
編輯簡譜時,會自動定位到簡譜最末尾的位置,且只支持新增和刪除。
刪除:點擊刪除按鈕會直接刪除最末尾的簡譜符號。
新增:按下相應的音符鍵,會結合當前的高低音和節拍按鍵,新增對應的簡譜符號。
另外編輯模式下會多出來兩個新的按鍵,分別是音符按鍵最左側的“0”鍵和音符按鍵最右側的“|”鍵。
“0”鍵代表簡譜上的休止符。
“|”鍵表示表示簡譜的分隔符,只用來標識,不輸入也是可以的。
方案與原理
這個小程式完全是前端代碼實現,無需服務端,並且不需要音頻資源文件。
之所以做成這樣,是因為微信小程式上面現在放資源文件是要給騰訊打錢的,而單純的代碼是不需要的 o(∩_∩)o
技術上的實現說起來也很簡單,使用 AudioContext 來實現,音頻文件是預先將 mp3 進行 base64 編碼,使用時再轉換成AudioContext的 buffer 數組來實現。
在做這個之前我對AudioContext一無所知,網上這類材料也很少,算是非常冷門了,主要是參考了webaudiofont這個項目來實現。
但是這裡有個問題,微信小程式的AudioContext是自己實現的一套機制,與 web 標準的AudioContext不一樣,而且微信小程式這裡另外提供了一個WebAudioContext的,不過還是與 AudioContext 標準介面有些差異。
所以我這裡也做了一些相容上的處理。
順便說一下,在代碼中簡單修改一下音源文件 0255_GeneralUserGS_sf2_file.js 為其他音源文件,也可以彈奏其他的樂器,比如鋼琴嗩吶之類的。
關鍵代碼
以下貼出播放音樂的關鍵代碼:
import { _tone_0255_GeneralUserGS_sf2_file } from "../../utils/0255_GeneralUserGS_sf2_file";
let actx = wx.createWebAudioContext();
let player = new WebAudioFontPlayer();
player.adjustPreset(actx, _tone_0255_GeneralUserGS_sf2_file);
// 根據音高,音符,節拍來播放音樂
playMusic(type, char, pai, time = 0) {
if (char === "0") {
return;
}
const pitch = levels[type] * 12 + note[char - 1];
player.queueWaveTable(
actx,
actx.destination,
_tone_0255_GeneralUserGS_sf2_file,
time,
pitch,
TIME_RATE * parseFloat(pai),
1
);
},
在引入吉他音頻源文件_tone_0255_GeneralUserGS_sf2_file後,我們先調用微信小程式的wx.createWebAudioContext函數,創建一個AudioContext實例,然後使用封裝好的WebAudioFontPlayer類構建一個播放器實例player,然後調用player.adjustPreset函數預先解析音頻源文件。
接著在具體播放音樂的函數中playMusic,我們使用player.queueWaveTable播放音樂。
傳入的實參分別表示:
* actx // AudioContext實例
* actx.destination // AudioContext實例的渲染設備,一般就是揚聲器之類的
* _tone_0255_GeneralUserGS_sf2_file // 音頻源文件
* time // 音頻響起的起始時間
* pitch // 音高
* TIME_RATE * parseFloat(pai) // 這裡是根據傳入的節拍計算出這個音調播放的持續時間
* 1 // 這裡表示的是音量
另外註意一點:
const pitch = levels[type] * 12 + note[char - 1];
這行代碼是我參考相關文件得出的計算音高的計算公式,保持固定就好了,其中涉及的魔法數字也是沒辦法的事情。
除了以上的關鍵代碼,其他的代碼比如輸入簡譜,自動定位當前的簡譜,以及播放音樂的控制邏輯大多也不算難,所以就不單獨列出了。
缺陷與失敗的修複方案
這個小程式在自由模式、簡單模式和靈魂模式下運行還算較好。
但是在真實手機上,自動模式是有問題的(在 PC 的開發者工具上自動模式沒有問題)。
這個問題就是自動模式下,播放的聲音有雜音。
自動模式嘗試過兩個方案:
一個方案使用的是 setTimeout,播放一個音調後再播放下一個音調,在 PC 上表現良好,在手機上會有延遲,雜音問題很小。
一個方案使用的是 queueWaveTable 自己的時間機制去播放,沒有延遲,但是雜音問題嚴重。
由於這兩套方案在開發者工具上都表現良好,只在真機上有問題,猜測只能是移動端性能不佳,或者是微信小程式的WebAudioContext實現有問題。
我實際發佈的小程式採用的是第一種方案,不過第二種我也在代碼中實現了,在代碼中已註釋。
其實我更傾向於第二種方案,因為 setTimeout 在真機上的延遲聽起來像亂彈的,只是第二種的雜音問題實在太嚴重才不得不選第一種。
總的來說,在真機上,自動模式都不咋樣。
總結
因為對這個方面實在沒什麼深入研究的欲望,隨著興趣減淡,也就偃旗息鼓了。
雖然自動模式確實存在問題,但是靈魂模式也不錯了,也更好玩一點。
也算是做了一些東西的,如果有後來者可以解決這個問題,或者有感興趣的可以在這個基礎上再修複或者拓展吧。
這裡給出開源的倉庫地址:fake-guitarist
作者:韓子盧出處:https://www.cnblogs.com/vvjiang/
本博客文章均為作者原創,轉載請註明作者和原文鏈接。