“我正在参加「码上掘金月赛第1期」,点击此处查看详情”
前言
又到一年起点时,2023FLAG:从零开始到提瓦特琴仙(奇怪的技能增加了),经常在b站刷到原琴玩家的视频,临渊羡鱼,不如退而结网,于是这次的月赛就实现一个web版的原琴。
实现过程
HTML
html主要分为两大区域
第一块是按键提示区,这里我们用position定位+for循环实现21个条移动轨迹。
<div class="screen">
<span class="line"></span>
<div
:class="move"
v-for="(i, idx) in keybox"
:key="idx"
:style="`left:${i[1] * 50}px;`"
>
{{ i[0] }}
</div>
</div>
后面加载midi会向keybox实时添加按键,结合animation实现下落,并在提示块划到提示线时高亮显示。
.fall {
position: absolute;
font-size: 25px;
font-family: Arial, Helvetica, sans-serif;
width: 50px;
height: 50px;
line-height: 50px;
text-align: center;
color: rgb(99, 175, 168);
border-radius: 5px;
background-color: rgb(237, 234, 218, 0.9);
top: -50px;
animation: fall 5s linear;
}
@keyframes fall {
0% {
top: -50px;
}
73% {
background-color: rgb(237, 234, 218, 0.9);
}
75% {
background-color: rgba(255, 248, 208, 1);
}
100% {
top: 450px;
}
}
第二块是按键区,这里需要实现的功能是点击事件以及键盘事件,通过playAudio(i)传入字母触发响应的音频。
<div class="qin">
<div
class="tone"
:style="{
background: `url(https://pic.imgdb.cn/item/${img[idx % 7]})`,
}"
@click="playAudio(i)"
v-for="(i, idx) in audioList"
:key="idx"
>
<div :class="`round ${roundclick[idx]}`"></div>
</div>
</div>
弹奏功能
在触发音频的同时给对应的键位添加roundclick样式实现涟漪效果。
playAudio(i) {
let idx = this.audioList.indexOf(i);
this.$set(this.roundclick, idx, "");
setTimeout(() => {
this.$set(this.roundclick, idx, "roundclick");
}, 10);
let audio = new Audio(MP3[i]);
audio.play();
}
//css
.roundclick {
animation: ripple 1s linear;
}
@keyframes ripple {
100% {
transform: scale(1.5);
opacity: 0;
}
}
接下来是键盘事件,当按下键盘时,通过调用playAudio(i)触发音频,这里需要多一部操作是,在按下的时候给每个按键一个状态,使得在未弹起时不会一直触发音频。
document.addEventListener("keydown", (e) => {
let i = e.key.toUpperCase();
let key = this.audioList.indexOf(i);
if (key > -1 && this.keyList[key]) {
this.playAudio(i);
this.keyList[key] = 0;
}
});
document.addEventListener("keyup", (e) => {
let i = e.key.toUpperCase();
let key = this.audioList.indexOf(i);
this.keyList[key] = 1;
});
自动播放
简单的事件操作后,我们就实现了琴的弹奏功能,接下来是自动演奏功能。 自动演奏是基于琴谱进行的,所以我们需要一套关于琴谱解析的方案。(以下关于乐理方面的知识都是现学的,可能存在一些不对的地方)
方案一
最简单阅读的是简谱,数字头上带点的对标第一行,底下有点的第三行,什么都没有的是第二行。
接下来是音符,在这张谱子中出现四分音符和八分音符以及一起弹的情况,所以我进行了如下转换
这里我将左右手部分分开转换,分别以两种方式,第一种是直接转换成键位字母,第二种则是以+—来区分音高。
【】中括号表示八分音符,以此类推【【】】包裹的则是16音符,未被包裹的则是四分音符。()表示此处需要同时按下多个键位。但是这个方案很快被否决了,因为还有许多符号需要我们去定义,并且转换谱子很累很累,哪里错了一时间也不好发现,于是就到b站上找那些自动演奏的方案,了解到可以通过midi的方式。
方案二
MIDI是一种数字音频技术,旨在允许不同类型的音乐设备(例如键盘、电子鼓、合成器、计算机等)进行通信和交互,从而实现音乐的创作、演奏、录制和编辑。可以将音符、控制信息和其他音乐参数(如音量、音色等)以数字形式传输到不同类型的设备中。并不包含音频数据本身,而是只包含音符、控制信息和其他音乐参数的数字指令。
基于上面的了解,我使用了midi-player-js这个库
import MidiPlayer from 'midi-player-js';
//初始化播放器并注册事件处理程序
const Player = new MidiPlayer.Player(function(event) {
console.log(event);
});
//导入mid
Player.loadFile('./test.mid');
//播放
Player.play();
loadFile仅限 Node.js,这里我们通过loadDataUri导入。
首先将midi文件转为base64 转换链接
选择dataurl
再将base64导入
Player.loadDataUri(this.test);
执行代码后,可以在控制台看到Player输出了乐谱的相关信息。
我们主要关注name、track和noteNumber三个参数
name有Note on以及Note off两个值,是音符开关,代表按下抬起,在钢琴中弹下琴键后,手是不能随便回收的,必须保证在按下的状态,因为立即抬起会影响发音,但这跟我们没有半毛钱关系,所以我们只需要取Note on。
noteNumber是音高编号- 范围从 0 到 127,这里我们要与键盘进行绑定。
track指的是轨道,可以通过track演奏指定的轨道
结合以上参数,我们在Player中添加以下代码
this.Player = new MidiPlayer.Player((e) => {
this.Player.setTempo(this.bpm);
if (e.name == "Note on") {
let audio = this.keymap[e.noteNumber];
//自动演奏
if (mode == 0) {
this.move = "rise";
this.keybox.push([audio,this.audioList.indexOf(audio)]);
this.playAudio(audio);
}
//练习弹奏
if (mode == 1) {
this.move = "fall";
this.keybox.push([audio,this.audioList.indexOf(audio),
]);
}
//只弹主调
if (mode == 2 && e.track == 2) {
this.move = "fall";
this.keybox.push([audio,this.audioList.indexOf(audio)),
]);
}
});
到这里功能其实大致上已经完整实现了,以下可以算扩展。
但是我发现他的信息中并没有表示几分音符,那么他是以什么解析出音符序列的呢?
观察midi的序列,不难发现应该是和tick有关的,但是为什么从1200开始?
这里离谱的是我在百度找到了这个相关解释
一开始直接代入发现对不上,然后看评论才发现半秒怎么会是30毫秒(自罚三道leetcode)。
chatgpt启动
我们直接获取文件头的信息即可,上文中谱子的时间分辨率为480。带入计算一下
两个四分休止符一个八分休止符2*480+480/2=1200,与输出信息的正好吻合。 这里说这么多其实是为了之后可能会实现的功能做铺垫(如果没更新,说明我淹没在生活里了)
功能拓展
实现midi解析、移调
大家阅读代码就可以看出,其实都是midi-player-js库帮了大忙,但是网上很多mid的文件导入进来是不能用的,因为原琴只有21个键,所以其他调子的谱是需要移调的。
谱子获取与编写
获取:谱子的获取网上有许多mid文件,但是不是全部都能用。
编写:目前是没有找到有什么简谱的编辑软件(能导出mid的)。
我这里先后使用了两个软件(都是五线谱):
MuseScore 4
Guitar Pro 7
这里也很离谱,我先是用MuseScore打谱,发现导出的mid有问题,不能用。于是导出Guitar Pro 7再用Guitar Pro 7导出mid,能用,但是丢失了些信息,比如目前谱子中出现了莫名其妙的连弹
这里的第二个音应该不重复弹的,Guitar Pro 7一转被弹出来了。
为什么没解决嘞?因为我好不容易才浅显的看懂并会编辑MuseScore 4中的五线谱,他又唰的一下在Guitar Pro 7中又变成了我看不懂的方式。
游戏内实现自动播放
之后会将会用electron实现。