Piano
🎹Play the piano with the keyboard - 用键盘8个键演奏一首蒲公英的约定送给自己或月亮代表我的心送给她
Install / Use
/learn @Wscats/PianoREADME
Usage
<img width="250px" align="right" src="./public/piano.gif"/>体验地址: http://wscats.gitee.io/piano/build/ 或者 https://wscats.github.io/piano/build/
<!-- <img height="80px" align="right" src="https://raw.githubusercontent.com/Wscats/piano/master/public/demo.png" /> -->项目地址: https://github.com/Wscats/piano
用键盘8个键演奏一首蒲公英的约定送给996的自己或者一首月亮代表我的心给七夕的她,非常简单~
这个项目仅仅用了几个简单的前端技术实现,献给每一位挚爱音乐的代码家🎹
如果你喜欢或者对你有帮助,给我点个赞支持下吧😊
Develop & Installation
<!-- <img src="./public/demo.png"> -->开发,构建和运行。
# 获取远程仓库代码
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
技术点和目录结构
项目中没有使用市面主流的框架(React,Vue 和 Angular )和热门的技术,而用的是 Omi 框架(JSX+WebComponents),还有 Omil 的单文件组件 SFCs 加载器,组件通讯基于Proxy特性,并结合了 VScode 的插件 Eno-Snippets基于AST和正则实时编译.eno或.omi 后缀组件减轻部分的 Webpack 的局部编译压力,当然其他同学们熟知的技术这里就不提及了。
- 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 后缀文件。
简单乐理知识
首先我们先补习点音乐基础,提前收集好最基本的钢琴单音素材,每个音符对应一份.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|
<img height="120px" align="right" src="./public/keys.png" />这里专门制作一张图方便我们理解,请看右图:
当然实际情况还有全音和半音的区分,比如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();
}
<img align="right" width="500" src="./public/keycode.png" />
完成 <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
Related Skills
node-connect
339.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
339.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.9kCommit, push, and open a PR
