> 体验地址: 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 歌曲中唱到:
> 若是世界太危险,只有音乐最安全,带着我进梦里面,让歌词都实现💞 —— 《咱们的歌》