{"id":15033542,"url":"https://github.com/wscats/piano","last_synced_at":"2025-05-16T05:07:03.482Z","repository":{"id":50320752,"uuid":"84766582","full_name":"Wscats/piano","owner":"Wscats","description":"🎹Play the piano with the keyboard - 用键盘8个键演奏一首蒲公英的约定送给自己或月亮代表我的心送给她","archived":false,"fork":false,"pushed_at":"2024-07-22T14:33:18.000Z","size":7188,"stargazers_count":1147,"open_issues_count":11,"forks_count":154,"subscribers_count":46,"default_branch":"master","last_synced_at":"2025-05-16T05:06:56.225Z","etag":null,"topics":["eno","eno-loader","instruments","mp3","music","omi","omil","omil-loader","omil-snippets","piano","player"],"latest_commit_sha":null,"homepage":"http://wscats.gitee.io/piano/build/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Wscats.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"open_collective":"piano","custom":["https://gitee.com/wscats/piano#license"]}},"created_at":"2017-03-13T00:02:22.000Z","updated_at":"2025-05-14T08:56:11.000Z","dependencies_parsed_at":"2024-11-06T13:56:00.707Z","dependency_job_id":null,"html_url":"https://github.com/Wscats/piano","commit_stats":{"total_commits":78,"total_committers":3,"mean_commits":26.0,"dds":0.0641025641025641,"last_synced_commit":"461c1c7abf69ca1dd3abc55f27b139964ab47f67"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wscats%2Fpiano","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wscats%2Fpiano/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wscats%2Fpiano/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wscats%2Fpiano/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Wscats","download_url":"https://codeload.github.com/Wscats/piano/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254471060,"owners_count":22076585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["eno","eno-loader","instruments","mp3","music","omi","omil","omil-loader","omil-snippets","piano","player"],"created_at":"2024-09-24T20:21:38.199Z","updated_at":"2025-05-16T05:06:58.472Z","avatar_url":"https://github.com/Wscats.png","language":"JavaScript","funding_links":["https://opencollective.com/piano","https://gitee.com/wscats/piano#license","https://opencollective.com/piano/organization/0/website","https://opencollective.com/piano/organization/1/website","https://opencollective.com/piano/organization/2/website","https://opencollective.com/piano/organization/3/website","https://opencollective.com/piano/organization/4/website","https://opencollective.com/piano/organization/5/website","https://opencollective.com/piano/organization/6/website","https://opencollective.com/piano/organization/7/website","https://opencollective.com/piano/organization/8/website","https://opencollective.com/piano/organization/9/website"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003ch1\u003eOmi Piano\u003c/h1\u003e\n\n\n\u003cp\u003e\n  \u003cstrong\u003eBuild piano with Omi and Omi Snippets\u003c/strong\u003e\n  \u003cbr /\u003e\u003cbr /\u003e\n  \u003cstrong\u003e基于Omi和Omi Snippets构建钢琴应用\u003c/strong\u003e\n\u003c/p\u003e\n\n\n\u003cp\u003e\n  \u003csub\u003eMade with ❤︎ by\n    \u003ca href=\"https://github.com/Wscats\"\u003eEno Yao\u003c/a\u003e\n  \u003c/sub\u003e\n\u003c/p\u003e\n\n\u003cp\u003e\n\u003ca href=\"https://github.com/Wscats/piano\"\u003e\u003ca href=\"https://opencollective.com/piano\" alt=\"Financial Contributors on Open Collective\"\u003e\u003cimg src=\"https://opencollective.com/piano/all/badge.svg?label=financial+contributors\" /\u003e\u003c/a\u003e \u003cimg src=\"https://img.shields.io/badge/Star-500+-orange\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://wscats.github.io/piano/build/\"\u003e\u003cimg src=\"https://img.shields.io/badge/Version-5.20-brightgreen\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/Wscats/piano\"\u003e\u003cimg src=\"https://img.shields.io/badge/Github Page-Wscats-yellow\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/Wscats\"\u003e\u003cimg src=\"https://img.shields.io/badge/Author-Eno Yao-blueviolet\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\n\u003c!-- \u003ca href=\"https://github.com/Wscats/piano\"\u003e\u003cimg src=\"https://img.shields.io/badge/Star-500+-orange\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://wscats.github.io/piano/build/\"\u003e\u003cimg src=\"https://img.shields.io/badge/Version-5.20-brightgreen\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/Wscats/piano\"\u003e\u003cimg src=\"https://img.shields.io/badge/Github Page-Wscats-yellow\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/Wscats\"\u003e\u003cimg src=\"https://img.shields.io/badge/Author-Eno Yao-blueviolet\" /\u003e\u003c/a\u003e\n[![Netlify Status](https://api.netlify.com/api/v1/badges/b652768b-1673-42cd-98dd-3fd807b2ebca/deploy-status)](https://app.netlify.com/sites/determined-goldstine-52a037/deploys) --\u003e\n\n# Usage \n\n\u003cimg width=\"250px\" align=\"right\" src=\"./public/piano.gif\"/\u003e\n\n\u003e 体验地址： http://wscats.gitee.io/piano/build/ 或者 https://wscats.github.io/piano/build/ \n\n\n\u003e 项目地址： https://github.com/Wscats/piano\n\n\n\n\u003c!-- \u003cimg height=\"80px\" align=\"right\" src=\"https://raw.githubusercontent.com/Wscats/piano/master/public/demo.png\" /\u003e --\u003e\n\n用键盘8个键演奏一首蒲公英的约定送给996的自己或者一首月亮代表我的心给七夕的她，非常简单~\n\n这个项目仅仅用了几个简单的前端技术实现，献给每一位挚爱音乐的代码家🎹\n\n如果你喜欢或者对你有帮助，给我点个赞支持下吧😊\n\n# Develop \u0026 Installation\n\n\u003c!-- \u003cimg src=\"./public/demo.png\"\u003e --\u003e\n开发，构建和运行。\n\n```bash\n# 获取远程仓库代码\ngit clone https://github.com/Wscats/piano\n# 进入目录\ncd piano\n# 安装依赖\nnpm install\n# 启动项目\nnpm start\n# 在浏览器访问 http://localhost:3000\n```\n\n使用 npm 包管理器安装。\n\n```bash\nnpm install omi-piano\n```\n\n运行或者发布属于自己的演奏版本。\n\n```bash\n# 进入目录\ncd omi-piano\n# 安装依赖\nnpm install\n# 启动项目\nnpm start\n# 发布自已的演奏版本\nnpm run build\n```\n\n# 技术点和目录结构\n\n项目中没有使用市面主流的框架（React，Vue 和 Angular ）和热门的技术，而用的是 Omi 框架（`JSX+WebComponents`），还有 `Omil` 的单文件组件 `SFCs` 加载器，组件通讯基于`Proxy`特性，并结合了 VScode 的插件 `Eno-Snippets`基于`AST`和`正则`实时编译`.eno或.omi` 后缀组件减轻部分的 `Webpack` 的局部编译压力，当然其他同学们熟知的技术这里就不提及了。\n\n\u003c!-- \u003cimg width=\"560px\" align=\"right\" src=\"https://wscats.github.io/omi-docs/public/images/transfer.png\" /\u003e --\u003e\n\n\u003cimg width=\"580px\" align=\"right\" src=\"./public/transfer.png\" /\u003e\n\n- src\n  - assets\n  - element\n    - app-piano\n      - songs 钢琴简谱目录\n      - app-piano.eno 单文件组件\n      - app-piano.js 组件编译后的JS文件\n      - notes.js 键盘按键和音符的映射\n  - index.js 组件根容器，配置`Proxy`的通信方法\n- public\n  - samples/piano 钢琴单音符素材\n\n|app-piano.eno|开发中你需要编写的单文件组件|\n|-|-|\n|app-piano.js|经过`Eno-Snippets`修改或者保存文件`Hello.eno`后经过插件转化的`js`文件|\n\n如右图，左边的代码是我们编写的 `.eno` 后缀的单文件组件，右边是经过 `Eno Snippets` 生成的 `.js` 后缀文件。\n\n\n# 简单乐理知识\n\n首先我们先补习点音乐基础，提前收集好最基本的[钢琴单音素材](https://github.com/Wscats/piano/tree/master/public/samples/piano)，每个音符对应一份`.mp3`文件，用一个对象记录起来，类似下面这样，举个例子这里的`A`指的是`CDEFGAB`音名中`A`也就是`La`，这是最基本的乐理，有没有让你想起小时候上音乐课，画板上的五线谱。\n\n```js\nexport default {\n  A2: \"./samples/piano/a54.mp3\",\n  A3: \"./samples/piano/a69.mp3\",\n  A4: \"./samples/piano/a80.mp3\",\n  A5: \"./samples/piano/a74.mp3\",\n  A6: \"./samples/piano/a66.mp3\",\n  'A#3': \"./samples/piano/b69.mp3\",\n  'A#4': \"./samples/piano/b80.mp3\",\n  'A#5': \"./samples/piano/b74.mp3\",\n  'A#6': \"./samples/piano/b66.mp3\",\n  // other... \n}\n```\n\n当然这里我们使用数字来等价替代，降低初学者的难度，看下表`1`等价于`C`中音也就是`Do`，由于很多歌都会用到钢琴更密集的中间部分按键，所以我们默认中音对应数字键：\n\n\u003e `1 === C4 === Do`\n\n\n|数字键|1|2|3|4|5|6|7|\n|-|-|-|-|-|-|-|-|\n|音名|C4|D4|E4|F4|G4|A4|B4|\n|音符|Do|Re|Mi|Fa|Sol|La|Si|\n\n\u003cimg height=\"120px\" align=\"right\"  src=\"./public/keys.png\" /\u003e\n\n这里专门制作一张图方便我们理解，请看右图：\n\n当然实际情况还有全音和半音的区分，比如`A`的半音就是`A#`，还有中音，高音和倍高音，我们这里用`A4`表示中音，`A5`表示高音，`A6`表示倍高音，所以表格可以继续整理得更清晰，当我们要弹奏中音`A4`，只需要按键盘上的数字键`6`，如果要弹奏高音`A5`，只需要用组合键`Option+6`，我们只需要举一反三，就可以知道每个音符对应的键盘按键。\n\n|倍低音|C2|D2|E2|F2|G2|A2|B2|\n|-|-|-|-|-|-|-|-|\n|Shift键+(1-7)|Shift+1|Shift+2|Shift+3|Shift+4|Shift+5|Shift+6|Shift+7|\n|低音|C3|D3|E3|F3|G3|A3|B3|\n|Ctrl键+(1-7)|Ctrl+1|Ctrl+2|Ctrl+3|Ctrl+4|Ctrl+5|Ctrl+6|Ctrl+7|\n|中音|C4|D4|E4|F4|G4|A4|B4|\n|数字键1-7|1|2|3|4|5|6|7|\n|高音|C5|D5|E5|F5|G5|A5|B5|\n|Option键+(1-7)|Option+1|Option+2|Option+3|Option+4|Option+5|Option+6|Option+7|\n|倍高音|C6|D6|E6|F6|G6|A6|B6|\n|Command键+(1-7)|Command+1|Command+2|Command+3|Command+4|Command+5|Command+6|Command+7|\n|音符|Do|Re|Mi|Fa|Sol|La|Si|\n\n上面是全音表，这里附上半音表：\n\n|倍低半音|C#2|D#2|F#2|G#2|A#2|\n|-|-|-|-|-|-|\n|Shift+|Shift+q|Shift+w|Shift+e|Shift+r|Shift+t|\n|低半音|C#3|D#3|F#3|G#3|A#3|\n|Ctrl+|Ctrl+q|Ctrl+w|Ctrl+e|Ctrl+r|Ctrl+t|\n|中半音|C#4|D#4|F#4|G#4|A#4|\n|字母键|q|w|e|r|t|\n|高半音|C#5|D#5|F#5|G#5|A#5|\n|Option+|Option+q|Option+w|Option+e|Option+r|Option+t|\n|倍高半音|C#6|D#6|F#6|G#6|A#6|\n|Command+|Command+q|Command+w|Command+e|Command+r|Command+t|\n\n那么我们现在只需要用键盘上的5个`字母键(q,w,e,r,t)` + 4个`功能键(Shift,Control,Option和Command)` + 7个`数字键(1,2,3,4,5,6,7)`总共16个键，演奏钢琴60个单音(35个全音+25个半音)，实际情况一首简单的钢琴曲可以不需要用到那么多，用几个简单的和弦即可。\n\n# 构建钢琴界面\n\n有上面的前期准备，下面就是转化为我们的编程知识了，我们需要使用 HTML 来绘制我们的钢琴界面，我们可以参考 [codepen](https://codepen.io/search/pens?q=piano\u0026page=1\u0026order=popularity\u0026depth=everything) 和 [codesandbox](https://codesandbox.io/search?query=piano\u0026page=1\u0026configure%5BhitsPerPage%5D=12) 的素材，这里我用了 `flex` 布局配合阴影和过度实现钢琴的黑白键，里面用了 React 的 JSX 语法去遍历渲染黑白键。\n\n\u003cimg src=\"https://raw.githubusercontent.com/Wscats/piano/master/public/demo.png\" /\u003e\n\n```html\n\u003cdiv class=\"piano\"\u003e\n  {this.data.pianoKeys.map((item)=\u003e{return(\n  \u003cdiv class=\"piano-key\"\u003e\n    \u003cdiv data-type=\"white\" ref={e=\u003e{ this[item.white.name] = e }} class=\"piano-key__white\"\n      onClick={this.playNote.bind(this,item.white.name)} data-key={item.white.keyCode}\n      data-note={item.white.name}\u003e\n      \u003cspan class=\"piano-note\"\u003e{item.white.name}\u003c/span\u003e\n      \u003caudio preload=\"auto\" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name}\n        class='audioEle'\u003e\u003c/audio\u003e\n    \u003c/div\u003e\n    \u003cdiv data-type=\"black\" ref={e=\u003e{ this[item.black.name] = e }} style={{\n      display: item.black.name ? 'block' : 'none'\n    }} class=\"piano-key__black\" onClick={this.playNote.bind(this,item.black.name)} data-key={item.black.keyCode}\n      data-note={item.black.name}\u003e\n      \u003cspan class=\"piano-note\" style=\"color:#fff\"\u003e{item.black.name}\u003c/span\u003e\n      \u003caudio preload=\"auto\" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name}\n        class='audioEle'\u003e\u003c/audio\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  )})}\n\u003c/div\u003e\n```\n\n可以观察 CSS 的源代码，分别对应写黑键和白键的样式，还可以另外写多一个样式，用于键盘或者鼠标点击琴键时候的效果，可以简单给它加一个背景色即可，整体实现不会太复杂，具体可以调整样式的参数来打造属于自己的钢琴风格。\n\n```css\n.piano {\n  margin: 0 200px;\n  background: linear-gradient(-65deg, #000, #222, #000, #666, #222 75%);\n  border-top: .8rem solid #282828;\n  box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828;\n  display: flex;\n  height: 80vh;\n  height: 20vh;\n  justify-content: center;\n  overflow: hidden;\n  padding-bottom: 2%;\n  padding-left: 2.5%;\n  padding-right: 2.5%;\n}\n.piano-key {\n  color: blue;\n  flex: 1;\n  margin: 0 .1rem;\n  max-width: 8.8rem;\n  position: relative;\n}\n\n.piano-key__white {\n  display: flex;\n  flex-direction: column-reverse;\n  background: linear-gradient(-30deg, #f8f8f8, #fff);\n  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);\n  height: 100%;\n  position: relative;\n}\n\n.piano-key__black {\n  display: flex;\n  flex-direction: column-reverse;\n  background: linear-gradient(-20deg, #222, #000, #222);\n  box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4);\n  border-width: .2rem .4rem 1.2rem;\n  border-style: solid;\n  border-color: #666 #222 #111 #555;\n  height: 60%;\n  left: 100%;\n  position: absolute;\n  transform: translateX(-50%);\n  top: 0;\n  width: 70%;\n  z-index: 1;\n}\n```\n\n# 播放钢琴音\n\n当我们实现完钢琴界面，我们就需要为每个按键匹配声音，这里使用 HTML5 的 `\u003caudio\u003e` 标签，它可以装载着钢琴的音符，当我们触发鼠标点击事件或者键盘点击事件的时候，我们就让它播放，在钢琴没播放之前我们使用属性值 `preload=\"auto\"` 让其预加载。\n```html\n\u003caudio preload=\"auto\" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name} class='audioEle'\u003e\u003c/audio\u003e\n```\n播放只要用`ref`属性获取琴音的节点，然后对其触发方法控制播放逻辑，`audio.currentTime = 0`重置播放进度和`audio.play()`执行播放，当触发播放的同时可以用延时器实现按键动画。\n\n```js\nplayNote(name) {\n  let audio = this[name].childNodes[1]\n  this[name].style.background = `linear-gradient(-20deg, #3330fb, #000, #222)`\n  let timer = setTimeout(() =\u003e {\n    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)`\n    clearTimeout(timer)\n  }, 1000)\n  audio.currentTime = 0;\n  audio.play();\n}\n```\n\n\u003cimg align=\"right\" width=\"500\" src=\"./public/keycode.png\" /\u003e\n\n完成 `\u003caudio\u003e` 的音频处理之后，就需要让键盘事件与其绑定逻辑了，这里需要了解键盘的 `keycode`，键盘每个实体按键都会对应有一个按键码，根据按键码用 `JS` 键盘事件监听来判断按键是否被摁住。\n\n\n我们使用 `window.document.onkeydown` 来监听页面全局的键盘事件，然后判断事件对象 `e.altKey`，`e.ctrlKey`，`e.metaKey` 和 `e.shiftKey` 这四个功能键是否被触发，再判断数字键是否被触发，最后判断字母键是否被触发。\n\n```js\ndocument.onkeydown = (event) =\u003e {\n  var e = event || window.event || arguments.callee.caller.arguments[0];\n  let playNote = (key) =\u003e {\n    if (e.shiftKey === true) {\n      this.playNote(`${key}2`)\n    } else if (e.altKey === true) {\n      this.playNote(`${key}5`)\n    } else if (e.ctrlKey === true) {\n      this.playNote(`${key}3`)\n    } else if (e.metaKey === true) {\n      this.playNote(`${key}6`)\n      e.returnValue = false;\n    } else {\n      this.playNote(`${key}4`)\n    }\n  }\n  if (e \u0026\u0026 49 \u003c= e.keyCode \u0026\u0026 e.keyCode \u003c= 55) {\n    switch (e.keyCode) {\n      case 49:\n        playNote('C')\n        break;\n      case 50:\n        playNote('D')\n        break;\n      case 51:\n        playNote('E')\n        break;\n      case 52:\n        playNote('F')\n        break;\n      case 53:\n        playNote('G')\n        break;\n      case 54:\n        playNote('A')\n        break;\n      case 55:\n        playNote('B')\n        break;\n    }\n  }\n  if (e \u0026\u0026 (81 === e.keyCode || e.keyCode === 87 || e.keyCode === 69 || e.keyCode === 82 || e.keyCode === 84)) {\n    switch (e.keyCode) {\n      case 81:\n        playNote('C#')\n        break;\n      case 87:\n        playNote('D#')\n        break;\n      case 69:\n        playNote('F#')\n        break;\n      case 82:\n        playNote('G#')\n        break;\n      case 84:\n        playNote('A#')\n        break;\n    }\n  }\n};\n```\n\n# 音符同步显示\n\n\u003cimg src=\"./public/demo.gif\" /\u003e\n\n每自动按一个钢琴键，可以看到音符在下面跳动并自动高亮，这里面涉及钢琴组件和底部文字组件的通信。我们使用的是 Omi 自带的 Store 功能来实现组件的通信，本质上它是基于 Proxy 对数据进行劫持，当我们改变一个数据的时候，可以实时映射最新的状态到另外一个组件，从而完成组件的通信，这里我设置了一个 `count` 和 `song` 作为两个组件的通信值，`count` 记录的是点击到了第几个音符，而 `song` 是正在播放的钢琴曲谱。\n\n```js\nrender(\u003cmy-app /\u003e, '#root', {\n    data: {\n        count: 0,\n        song: []\n    },\n    sub() {\n        this.data.count--\n    },\n    add() {\n        this.data.count++\n    },\n    setSong(song) {\n        // 构建新的数组，给它下标值来做索引\n        let melody = [];\n        song.map((item, index) =\u003e {\n            melody.push({\n                ...item,\n                index\n            })\n        })\n        // 处理成每30个音符一个数组，自动播放时候自动显示按键\n        for (let j = 0; j \u003c melody.length; j += 30) {\n            this.data.song.push(melody.slice(j, j + 30))\n        }\n    }\n})\n```\n\n# 自动播放\n\n以下就是关于如何自动播放的逻辑，如果要演奏复杂的歌曲，特别是多和弦的情况下，我们可以编写好歌谱，然后交给编程自动演奏，下面是`周杰伦《蒲公英的约定》`的钢琴简谱，我们用数组把每个按键的音符记录下来，然后只要用定时器或者递归把每个音符取出来给函数识别，然后再触发对应的 `\u003caudio\u003e` 标签播放即可，这里解释下数组里面的每一项，如果字符串里面是数字的话就对应中音，也就是如果是`'3'`，那就只需要按键盘的`3`，如果是`'+3'`那就是高音，那就是前面提到的用组合键 `option + 3`，如果是 `+1..`，那就是告诉编程，这里要停顿两个节拍，我们自己实际演奏的时候就在这里稍微停顿下控制旋律即可。\n```js\nconst song = [\n    '3', '4',\n    '5', '5', '5', '6', '7', '+1..',\n    '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',\n    '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',\n    '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2',\n    // 将愿望...\n    '+2..', '3', '4', '5',\n    // 折飞机寄成信...\n    '5', '5', '5', '6', '7', '+1..',\n    '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',\n    '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',\n    '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2..',\n    // 一起长大的约定...\n    '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..',\n    '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',\n    // 说好要一起旅行...\n    '3', '5', '+1.', '+3', '+3.', '+4', '+2..',\n    // 是你如今...\n    '+2', '+5', '7', '+1..',\n    // 唯一坚持的任性\n    '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1',\n    // 在走廊...\n    '3', '4',\n    '5', '5', '5', '6', '7', '+1..',\n    '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',\n    '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',\n    '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2',\n    // 一起长大的约定...\n    '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..',\n    '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',\n    // 说好要一起旅行...\n    '3', '5', '+1.', '+3', '+3.', '+4', '+2..',\n    // 是你如今...\n    '+2', '+5', '7', '+1..',\n    // 唯一坚持的任性...\n    '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1',\n    // 一起长大的约定...\n    '+6', '+5', '+3', '+2', '+1', '+3.', '+4', '+2..',\n    '+6', '+5', '7', '+1..',\n    // 与你聊不完的曾经...\n    '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',\n    // 而我已经分不清...\n    '3', '5', '+1', '+3', '+3.', '+2', '+2', '+2..', '+2', '+5', '7', '+2', '+1', '+1',\n    // 还是错过的爱情...\n    '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1..'\n]\nexport default [...song]\n```\n有了上面的数组，我们只需要编写一个递归函数函数来遍历数组，然后根据这种类数字的简谱，把它转为音符 `CDEFGAB`，一开始的时候我用了定时器实现读谱函数，后来发现，用定时器比较难控制，音符之间的停顿时间，相反用递归会比较容易实现，但是递归同样很难实现暂停播放功能，因为从外部中断递归函数也比较复杂，所以同学们如果要自己实现钢琴的话，在这个地方要稍微注意一下。下面代码中出现的 `Promise` 配合 `await, async` 和定时器就是接受一个时间变量，来控制音符之间的停顿时间，而外层`if(offset \u003c song.length \u0026\u0026 this.store.data.song.length \u003e 0)`判断条件左边的条件是判断索引值要小于简谱数组的长度，右边就是外层传入的判断值作为递归函数的终止边界条件。\n```js\nplaySong(song) {\n  this.setSong([...song])\n  let offset = 0\n  let time = 0\n  let playSong = async () =\u003e {\n    // 右边是从外部来中断递归\n    if (offset \u003c song.length \u0026\u0026 this.store.data.song.length \u003e 0) {\n      switch (typeof song[offset]) {\n        // 简谱2演奏方法 根据 ++12345--6. 简单旋律情况\n        case 'string':\n          let letters = song[offset].match(/[0-9]/g)\n          switch (letters.length) {\n            case 1:\n              time = this.handleString(song, offset)\n              break\n            default:\n              time = this.handleStrings(song, offset)\n              break\n          }\n          break\n        // 简谱1演奏方法 根据 CDEFGAB，复杂旋律情况，比如有和弦\n        case 'object':\n          console.log(song[offset]['note'])\n          time = song[offset]['time'];\n          this.playNote(song[offset]['note'])\n          break;\n        case 'number':\n          // 休止符\n          switch (song[offset]) {\n            case 0:\n              time = 1000\n              break\n          }\n          break\n      }\n      await new Promise((resolve) =\u003e {\n        let timer = setTimeout(() =\u003e {\n          clearInterval(timer)\n          resolve()\n        }, time)\n      })\n      offset++\n      // 自定义事件，跟下面底部的音符自动跳动结合\n      this.add()\n      playSong()\n    } else {\n      // 暂停播放\n      clearTimeout(this.timer)\n      this.store.data.song = []\n      this.store.data.count = 0\n      return\n    }\n  }\n  playSong()\n}\n```\n\n## 蒲公英的约定\n\n看完上面的数组简谱当然肯定会有同学问，上文的数组里面不止运用到8个键吧，如果仔细观察，就会发现这里只用了中音和高音，也就是纯数字键`(1-7)`和`Option`键的配合，连半音都没用到，所以实际止用到了8个键而已，所以上面给编程识别的简谱，转化我们人类识别的键盘谱，只需要稍微调整为下面的按键组合即可。\n```js\n'3', '4', '5', '5', '5', '6', '7', 'Option+1..',\n'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',\n'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',\n'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2',\n// 将愿望...\n'Option+2..', '3', '4', '5',\n// 折飞机寄成信...\n'5', '5', '5', '6', '7', 'Option+1..',\n'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',\n'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',\n'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2..',\n// 一起长大的约定...\n'3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..',\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',\n// 说好要一起旅行...\n'3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..',\n// 是你如今...\n'Option+2', 'Option+5', '7', 'Option+1..',\n// 唯一坚持的任性\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1',\n// 在走廊...\n'3', '4', '5', '5', '5', '6', '7', 'Option+1..',\n'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',\n'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',\n'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2',\n// 一起长大的约定...\n'3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..',\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',\n// 说好要一起旅行...\n'3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..',\n// 是你如今...\n'Option+2', 'Option+5', '7', 'Option+1..',\n// 唯一坚持的任性...\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1',\n// 一起长大的约定...\n'Option+6', 'Option+5', 'Option+3', 'Option+2', 'Option+1', 'Option+3.', 'Option+4', 'Option+2..',\n'Option+6', 'Option+5', '7', 'Option+1..',\n// 与你聊不完的曾经...\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',\n// 而我已经分不清...\n'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',\n// 还是错过的爱情...\n'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1..'\n```\n\n## 月亮代表我的心\n\n我们还可以演奏另一首耳熟能详的的钢琴曲《月亮代表我的心》。\n```js\n'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',\n\n'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',\n\n'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',\n\n'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',\n\n'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',\n\n'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',\n\n'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',\n\n'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',\n\n'2', '3', '2', 'Ctrl+6', 'Ctrl+7', '1', '2', '1'\n```\n\n# Contributing \n\n感谢音乐和编程的陪伴！也致敬各位奋斗于996的代码家，欢迎分享，也期待您贡献代码，提 PR ，在 [issue](https://github.com/Wscats/piano/issues/new) 中讨论问题，或者说说您的建议，音乐不曾辜负任何人，正如 Leehom Wang 歌曲中唱到：\n\n\u003e 如果世界太危险，只有音乐最安全，带着我进梦里面，让歌词都实现！  —— 《我们的歌》\n\n### Code Contributors\n\nThis project exists thanks to all the people who contribute.\n\u003ca href=\"https://github.com/Wscats/piano/graphs/contributors\"\u003e\u003cimg src=\"https://opencollective.com/piano/contributors.svg?width=890\u0026button=false\" /\u003e\u003c/a\u003e\n\n### Financial Contributors\n\nBecome a financial contributor and help us sustain our community.\n\n#### Individuals\n\n\u003ca href=\"https://opencollective.com/piano\"\u003e\u003cimg src=\"https://opencollective.com/piano/individuals.svg?width=890\"\u003e\u003c/a\u003e\n\n#### Organizations\n\nSupport this project with your organization. Your logo will show up here with a link to your website.\n\n\u003ca href=\"https://opencollective.com/piano/organization/0/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/0/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/1/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/1/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/2/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/2/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/3/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/3/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/4/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/4/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/5/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/5/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/6/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/6/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/7/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/7/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/8/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/8/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/piano/organization/9/website\"\u003e\u003cimg src=\"https://opencollective.com/piano/organization/9/avatar.svg\"\u003e\u003c/a\u003e\n\n# Stargazers over time\n\n[![Stargazers over time](https://starchart.cc/Wscats/piano.svg?variant=adaptive)](https://starchart.cc/Wscats/piano)\n\n# License\n\nOmi Piano is released under the \n[MIT](http://opensource.org/licenses/MIT)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwscats%2Fpiano","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwscats%2Fpiano","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwscats%2Fpiano/lists"}