> 體驗地址: https://wscats.github.io/piano/build/css
> 項目地址: https://github.com/Wscats/pianohtml
用鍵盤8個鍵演奏一首蒲公英的約定送給996的本身或月亮表明個人心給七夕的她,很是簡單~前端
這個項目僅僅用了幾個簡單的前端技術實現,獻給每一位摯愛音樂的代碼家🎹git
若是你喜歡或者對你有幫助,給我點個贊支持下吧😊github
技術點和目錄結構
項目中沒有使用市面主流的框架(React,Vue 和 Angular )和熱門的技術,而用的是Omi框架(JSX+WebComponents
),還有 Omil
的單文件組件 SFCs
加載器,組件通信基於Proxy
特性,並結合了 VScode 的插件 Eno-Snippets
基於AST
和正則
實時編譯.eno或.omi
後綴組件減輕部分的 Webpack
的局部編譯壓力,固然其餘同窗們熟知的技術這裏就不說起了。web
- src
- assets
- element
- app-piano
- songs 鋼琴簡譜目錄
- app-piano.eno 單文件組件
- app-piano.js 組件編譯後的JS文件
- notes.js 鍵盤按鍵和音符的映射
- app-piano
- index.js 組件根容器,配置
Proxy
的通訊方法
- public
- samples/piano 鋼琴單音符素材
app-piano.eno | 開發中你須要編寫的單文件組件 |
---|---|
app-piano.js | 通過Eno-Snippets 修改或者保存文件Hello.eno 後通過插件轉化的js文件 |
以下圖,左邊的代碼是咱們編寫的 .eno 後綴的單文件組件,右邊是通過 Eno Snippets 生成的 .js 後綴文件。npm
Develop & Installation
開發,構建和運行。編程
# 獲取遠程倉庫代碼 git clone https://github.com/Wscats/piano # 進入目錄 cd piano # 安裝依賴 npm install # 啓動項目 npm start # 在瀏覽器訪問 http://localhost:3000
使用 npm 包管理器安裝。數組
npm install omi-piano
運行或者發佈屬於本身的演奏版本。瀏覽器
# 進入目錄 cd omi-piano # 安裝依賴 npm install # 啓動項目 npm start # 發佈自已的演奏版本 npm run build
簡單樂理知識
首先咱們先補習點音樂基礎,提早收集好最基本的鋼琴單音素材,每一個音符對應一份.mp3
文件,用一個對象記錄起來,相似下面這樣,舉個例子這裏的A
指的是CDEFGAB
音名中A
也就是La
,這是最基本的樂理,有沒有讓你想起小時候上音樂課,畫板上的五線譜。
export default { A2: "./samples/piano/a54.mp3", A3: "./samples/piano/a69.mp3", A4: "./samples/piano/a80.mp3", A5: "./samples/piano/a74.mp3", A6: "./samples/piano/a66.mp3", 'A#3': "./samples/piano/b69.mp3", 'A#4': "./samples/piano/b80.mp3", 'A#5': "./samples/piano/b74.mp3", 'A#6': "./samples/piano/b66.mp3", // other... }
固然這裏咱們使用數字來等價替代,下降初學者的難度,看下錶1
等價於C
中音也就是Do
,因爲不少歌都會用到鋼琴更密集的中間部分按鍵,因此咱們默認中音對應數字鍵:
> 1 === C4 === Do
數字鍵 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
音名 | C4 | D4 | E4 | F4 | G4 | A4 | B4 |
音符 | Do | Re | Mi | Fa | Sol | La | Si |
這裏專門製做一張圖方便咱們理解:
固然實際狀況還有全音和半音的區分,好比A
的半音就是A#
,還有中音,高音和倍高音,咱們這裏用A4
表示中音,A5
表示高音,A6
表示倍高音,因此表格能夠繼續整理得更清晰,當咱們要彈奏中音A4
,只須要按鍵盤上的數字鍵6
,若是要彈奏高音A5
,只須要用組合鍵Option+6
,咱們只須要觸類旁通,就能夠知道每一個音符對應的鍵盤按鍵。
倍低音 | C2 | D2 | E2 | F2 | G2 | A2 | B2 |
---|---|---|---|---|---|---|---|
Shift鍵+(1-7) | Shift+1 | Shift+2 | Shift+3 | Shift+4 | Shift+5 | Shift+6 | Shift+7 |
低音 | C3 | D3 | E3 | F3 | G3 | A3 | B3 |
Ctrl鍵+(1-7) | Ctrl+1 | Ctrl+2 | Ctrl+3 | Ctrl+4 | Ctrl+5 | Ctrl+6 | Ctrl+7 |
中音 | C4 | D4 | E4 | F4 | G4 | A4 | B4 |
數字鍵1-7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
高音 | C5 | D5 | E5 | F5 | G5 | A5 | B5 |
Option鍵+(1-7) | Option+1 | Option+2 | Option+3 | Option+4 | Option+5 | Option+6 | Option+7 |
倍高音 | C6 | D6 | E6 | F6 | G6 | A6 | B6 |
Command鍵+(1-7) | Command+1 | Command+2 | Command+3 | Command+4 | Command+5 | Command+6 | Command+7 |
音符 | Do | Re | Mi | Fa | Sol | La | Si |
上面是全音表,這裏附上半音表:
倍低半音 | C#2 | D#2 | F#2 | G#2 | A#2 |
---|---|---|---|---|---|
Shift+ | Shift+q | Shift+w | Shift+e | Shift+r | Shift+t |
低半音 | C#3 | D#3 | F#3 | G#3 | A#3 |
Ctrl+ | Ctrl+q | Ctrl+w | Ctrl+e | Ctrl+r | Ctrl+t |
中半音 | C#4 | D#4 | F#4 | G#4 | A#4 |
字母鍵 | q | w | e | r | t |
高半音 | C#5 | D#5 | F#5 | G#5 | A#5 |
Option+ | Option+q | Option+w | Option+e | Option+r | Option+t |
倍高半音 | C#6 | D#6 | F#6 | G#6 | A#6 |
Command+ | Command+q | Command+w | Command+e | Command+r | Command+t |
那麼咱們如今只須要用鍵盤上的5個字母鍵(q,w,e,r,t)
+ 4個功能鍵(Shift,Control,Option和Command)
+ 7個數字鍵(1,2,3,4,5,6,7)
總共16個鍵,演奏鋼琴60個單音(35個全音+25個半音),實際狀況一首簡單的鋼琴曲能夠不須要用到那麼多,用幾個簡單的和絃便可。
構建鋼琴界面
有上面的前期準備,下面就是轉化爲咱們的編程知識了,咱們須要使用 HTML 來繪製咱們的鋼琴界面,咱們能夠參考 codepen 和 codesandbox 的素材,這裏我用了 flex
佈局配合陰影和過分實現鋼琴的黑白鍵,裏面用了 React 的 JSX 語法去遍歷渲染黑白鍵。
<div class="piano"> {this.data.pianoKeys.map((item)=>{return( <div class="piano-key"> <div data-type="white" ref="{e=">{ this[item.white.name] = e }} class="piano-key__white" onClick={this.playNote.bind(this,item.white.name)} data-key={item.white.keyCode} data-note={item.white.name}> <span class="piano-note">{item.white.name}</span> <audio preload="auto" src="{this.data.notes[item.white.name]}" hidden="true" data-note="{item.white.name}" class="audioEle"></audio> </div> <div data-type="black" ref="{e=">{ this[item.black.name] = e }} style={{ display: item.black.name ? 'block' : 'none' }} class="piano-key__black" onClick={this.playNote.bind(this,item.black.name)} data-key={item.black.keyCode} data-note={item.black.name}> <span class="piano-note" style="color:#fff">{item.black.name}</span> <audio preload="auto" src="{this.data.notes[item.white.name]}" hidden="true" data-note="{item.white.name}" class="audioEle"></audio> </div> </div> )})} </div>
能夠觀察 CSS 的源代碼,分別對應寫黑鍵和白鍵的樣式,還能夠另外寫多一個樣式,用於鍵盤或者鼠標點擊琴鍵時候的效果,能夠簡單給它加一個背景色便可,總體實現不會太複雜,具體能夠調整樣式的參數來打造屬於本身的鋼琴風格。
.piano { margin: 0 200px; background: linear-gradient(-65deg, #000, #222, #000, #666, #222 75%); border-top: .8rem solid #282828; box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828; display: flex; height: 80vh; height: 20vh; justify-content: center; overflow: hidden; padding-bottom: 2%; padding-left: 2.5%; padding-right: 2.5%; } .piano-key { color: blue; flex: 1; margin: 0 .1rem; max-width: 8.8rem; position: relative; } .piano-key__white { display: flex; flex-direction: column-reverse; background: linear-gradient(-30deg, #f8f8f8, #fff); box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px 0 0 #fff, 0 4px 3px rgba(0, 0, 0, .7), inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px -1px 15px rgba(0, 0, 0, .5), -3px 4px 6px rgba(0, 0, 0, .5); height: 100%; position: relative; } .piano-key__black { display: flex; flex-direction: column-reverse; background: linear-gradient(-20deg, #222, #000, #222); box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4); border-width: .2rem .4rem 1.2rem; border-style: solid; border-color: #666 #222 #111 #555; height: 60%; left: 100%; position: absolute; transform: translateX(-50%); top: 0; width: 70%; z-index: 1; }
播放鋼琴音
當咱們實現完鋼琴界面,咱們就須要爲每一個按鍵匹配聲音,這裏使用 HTML5 的 <audio>
標籤,它能夠裝載着鋼琴的音符,當咱們觸發鼠標點擊事件或者鍵盤點擊事件的時候,咱們就讓它播放,在鋼琴沒播放以前咱們使用屬性值 preload="auto"
讓其預加載。
<audio preload="auto" src="{this.data.notes[item.white.name]}" hidden="true" data-note="{item.white.name}" class="audioEle"></audio>
播放只要用ref
屬性獲取琴音的節點,而後對其觸發方法控制播放邏輯,audio.currentTime = 0
重置播放進度和audio.play()
執行播放,當觸發播放的同時能夠用延時器實現按鍵動畫。
playNote(name) { let audio = this[name].childNodes[1] this[name].style.background = `linear-gradient(-20deg, #3330fb, #000, #222)` let timer = setTimeout(() => { this[name].getAttribute('data-type') === 'white' ? this[name].style.background = `linear-gradient(-30deg, #f8f8f8, #fff)` : this[name].style.background = `linear-gradient(-20deg, #222, #000, #222)` clearTimeout(timer) }, 1000) audio.currentTime = 0; audio.play(); }
完成 <audio>
的音頻處理以後,就須要讓鍵盤事件與其綁定邏輯了,這裏須要瞭解鍵盤的 keycode
,鍵盤每一個實體按鍵都會對應有一個按鍵碼,根據按鍵碼用 JS
鍵盤事件監聽來判斷按鍵是否被摁住。
咱們使用 window.document.onkeydown
來監聽頁面全局的鍵盤事件,而後判斷事件對象 e.altKey
,e.ctrlKey
,e.metaKey
和 e.shiftKey
這四個功能鍵是否被觸發,再判斷數字鍵是否被觸發,最後判斷字母鍵是否被觸發。
document.onkeydown = (event) => { var e = event || window.event || arguments.callee.caller.arguments[0]; let playNote = (key) => { if (e.shiftKey === true) { this.playNote(`${key}2`) } else if (e.altKey === true) { this.playNote(`${key}5`) } else if (e.ctrlKey === true) { this.playNote(`${key}3`) } else if (e.metaKey === true) { this.playNote(`${key}6`) e.returnValue = false; } else { this.playNote(`${key}4`) } } if (e && 49 <= e.keyCode && e.keyCode <= 55) { switch (e.keyCode) { case 49: playNote('C') break; case 50: playNote('D') break; case 51: playNote('E') break; case 52: playNote('F') break; case 53: playNote('G') break; case 54: playNote('A') break; case 55: playNote('B') break; } } if (e && (81 === e.keyCode || e.keyCode === 87 || e.keyCode === 69 || e.keyCode === 82 || e.keyCode === 84)) { switch (e.keyCode) { case 81: playNote('C#') break; case 87: playNote('D#') break; case 69: playNote('F#') break; case 82: playNote('G#') break; case 84: playNote('A#') break; } } };
音符同步顯示
每自動按一個鋼琴鍵,能夠看到音符在下面跳動並自動高亮,這裏面涉及鋼琴組件和底部文字組件的通訊。咱們使用的是 Omi 自帶的 Store 功能來實現組件的通訊,本質上它是基於 Proxy 對數據進行劫持,當咱們改變一個數據的時候,能夠實時映射最新的狀態到另一個組件,從而完成組件的通訊,這裏我設置了一個 count
和 song
做爲兩個組件的通訊值,count
記錄的是點擊到了第幾個音符,而 song
是正在播放的鋼琴樂譜。
render(<my-app />, '#root', { data: { count: 0, song: [] }, sub() { this.data.count-- }, add() { this.data.count++ }, setSong(song) { // 構建新的數組,給它下標值來作索引 let melody = []; song.map((item, index) => { melody.push({ ...item, index }) }) // 處理成每30個音符一個數組,自動播放時候自動顯示按鍵 for (let j = 0; j < melody.length; j += 30) { this.data.song.push(melody.slice(j, j + 30)) } } })
自動播放
如下就是關於如何自動播放的邏輯,若是要演奏複雜的歌曲,特別是多和絃的狀況下,咱們能夠編寫好歌譜,而後交給編程自動演奏,下面是周杰倫《蒲公英的約定》
的鋼琴簡譜,咱們用數組把每一個按鍵的音符記錄下來,而後只要用定時器或者遞歸把每一個音符取出來給函數識別,而後再觸發對應的 <audio>
標籤播放便可,這裏解釋下數組裏面的每一項,若是字符串裏面是數字的話就對應中音,也就是若是是'3'
,那就只須要按鍵盤的3
,若是是'+3'
那就是高音,那就是前面提到的用組合鍵 option + 3
,若是是 +1..
,那就是告訴編程,這裏要停頓兩個節拍,咱們本身實際演奏的時候就在這裏稍微停頓下控制旋律便可。
const song = [ '3', '4', '5', '5', '5', '6', '7', '+1..', '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2', // 將願望... '+2..', '3', '4', '5', // 折飛機寄成信... '5', '5', '5', '6', '7', '+1..', '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2..', // 一塊兒長大的約定... '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..', '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', // 說好要一塊兒旅行... '3', '5', '+1.', '+3', '+3.', '+4', '+2..', // 是你現在... '+2', '+5', '7', '+1..', // 惟一堅持的任性 '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1', // 在走廊... '3', '4', '5', '5', '5', '6', '7', '+1..', '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2', // 一塊兒長大的約定... '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..', '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', // 說好要一塊兒旅行... '3', '5', '+1.', '+3', '+3.', '+4', '+2..', // 是你現在... '+2', '+5', '7', '+1..', // 惟一堅持的任性... '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1', // 一塊兒長大的約定... '+6', '+5', '+3', '+2', '+1', '+3.', '+4', '+2..', '+6', '+5', '7', '+1..', // 與你聊不完的曾經... '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', // 而我已經分不清... '3', '5', '+1', '+3', '+3.', '+2', '+2', '+2..', '+2', '+5', '7', '+2', '+1', '+1', // 仍是錯過的愛情... '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1..' ] export default [...song]
有了上面的數組,咱們只須要編寫一個遞歸函數函數來遍歷數組,而後根據這種類數字的簡譜,把它轉爲音符 CDEFGAB
,一開始的時候我用了定時器實現讀譜函數,後來發現,用定時器比較難控制,音符之間的停頓時間,相反用遞歸會比較容易實現,可是遞歸一樣很難實現暫停播放功能,由於從外部中斷遞歸函數也比較複雜,因此同窗們若是要本身實現鋼琴的話,在這個地方要稍微注意一下。下面代碼中出現的 Promise
配合 await, async
和定時器就是接受一個時間變量,來控制音符之間的停頓時間,而外層if(offset < song.length && this.store.data.song.length > 0)
判斷條件左邊的條件是判斷索引值要小於簡譜數組的長度,右邊就是外層傳入的判斷值做爲遞歸函數的終止邊界條件。
playSong(song) { this.setSong([...song]) let offset = 0 let time = 0 let playSong = async () => { // 右邊是從外部來中斷遞歸 if (offset < song.length && this.store.data.song.length > 0) { switch (typeof song[offset]) { // 簡譜2演奏方法 根據 ++12345--6. 簡單旋律狀況 case 'string': let letters = song[offset].match(/[0-9]/g) switch (letters.length) { case 1: time = this.handleString(song, offset) break default: time = this.handleStrings(song, offset) break } break // 簡譜1演奏方法 根據 CDEFGAB,複雜旋律狀況,好比有和絃 case 'object': console.log(song[offset]['note']) time = song[offset]['time']; this.playNote(song[offset]['note']) break; case 'number': // 休止符 switch (song[offset]) { case 0: time = 1000 break } break } await new Promise((resolve) => { let timer = setTimeout(() => { clearInterval(timer) resolve() }, time) }) offset++ // 自定義事件,跟下面底部的音符自動跳動結合 this.add() playSong() } else { // 暫停播放 clearTimeout(this.timer) this.store.data.song = [] this.store.data.count = 0 return } } playSong() }
蒲公英的約定
看完上面的數組簡譜固然確定會有同窗問,上文的數組裏面不止運用到8個鍵吧,若是仔細觀察,就會發現這裏只用了中音和高音,也就是純數字鍵(1-7)
和Option
鍵的配合,連半音都沒用到,因此實際止用到了8個鍵而已,因此上面給編程識別的簡譜,轉化咱們人類識別的鍵盤譜,只須要稍微調整爲下面的按鍵組合便可。
'3', '4', '5', '5', '5', '6', '7', 'Option+1..', 'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..', 'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6', '6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2', // 將願望... 'Option+2..', '3', '4', '5', // 折飛機寄成信... '5', '5', '5', '6', '7', 'Option+1..', 'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..', 'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6', '6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2..', // 一塊兒長大的約定... '3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..', 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..', // 說好要一塊兒旅行... '3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', // 是你現在... 'Option+2', 'Option+5', '7', 'Option+1..', // 惟一堅持的任性 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1', // 在走廊... '3', '4', '5', '5', '5', '6', '7', 'Option+1..', 'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..', 'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6', '6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2', // 一塊兒長大的約定... '3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..', 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..', // 說好要一塊兒旅行... '3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', // 是你現在... 'Option+2', 'Option+5', '7', 'Option+1..', // 惟一堅持的任性... 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1', // 一塊兒長大的約定... 'Option+6', 'Option+5', 'Option+3', 'Option+2', 'Option+1', 'Option+3.', 'Option+4', 'Option+2..', 'Option+6', 'Option+5', '7', 'Option+1..', // 與你聊不完的曾經... 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..', // 而我已經分不清... '3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+2', 'Option+2', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+2', 'Option+1', 'Option+1', // 仍是錯過的愛情... 'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1..'
月亮表明個人心
咱們還能夠演奏另外一首耳熟能詳的的鋼琴曲《月亮表明個人心》。
'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1', 'Ctrl+6', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1', 'Ctrl+6', '2', '3', '2', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+5', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', '1', '1', '1', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', 'Ctrl+6', 'Ctrl+7', '1', '2', '1', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1', 'Ctrl+6', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1', 'Ctrl+6', '2', '3', '2', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+5', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', '1', '1', '1', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', 'Ctrl+6', 'Ctrl+7', '1', '2', '1'
Contributing
感謝音樂和編程的陪伴!也致敬各位奮鬥於996的代碼家,歡迎分享,也期待您貢獻代碼,提 PR ,在 issue 中討論問題,或者說說您的建議,正如 Leehom Wang 歌曲中唱到:
> 若是世界太危險,只有音樂最安全,帶着我進夢裏面,讓歌詞都實現💞 —— 《咱們的歌》