vue实现web版风物之诗琴

634 阅读5分钟

“我正在参加「码上掘金月赛第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;
    });


自动播放

简单的事件操作后,我们就实现了琴的弹奏功能,接下来是自动演奏功能。 自动演奏是基于琴谱进行的,所以我们需要一套关于琴谱解析的方案。(以下关于乐理方面的知识都是现学的,可能存在一些不对的地方

方案一

image.png

最简单阅读的是简谱,数字头上带点的对标第一行,底下有点的第三行,什么都没有的是第二行。

image.png

接下来是音符,在这张谱子中出现四分音符和八分音符以及一起弹的情况,所以我进行了如下转换

image.png 这里我将左右手部分分开转换,分别以两种方式,第一种是直接转换成键位字母,第二种则是以+—来区分音高。 【】中括号表示八分音符,以此类推【【】】包裹的则是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 image.png 再将base64导入 Player.loadDataUri(this.test);

执行代码后,可以在控制台看到Player输出了乐谱的相关信息。 image.png 我们主要关注name、track和noteNumber三个参数

name有Note on以及Note off两个值,是音符开关,代表按下抬起,在钢琴中弹下琴键后,手是不能随便回收的,必须保证在按下的状态,因为立即抬起会影响发音,但这跟我们没有半毛钱关系,所以我们只需要取Note on。

noteNumber是音高编号- 范围从 0 到 127,这里我们要与键盘进行绑定。 image.png

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)),
            ]);
          }
      });

到这里功能其实大致上已经完整实现了,以下可以算扩展。

但是我发现他的信息中并没有表示几分音符,那么他是以什么解析出音符序列的呢?

image.png 观察midi的序列,不难发现应该是和tick有关的,但是为什么从1200开始? 这里离谱的是我在百度找到了这个相关解释

image.png 一开始直接代入发现对不上,然后看评论才发现半秒怎么会是30毫秒(自罚三道leetcode)。

chatgpt启动

image.png 我们直接获取文件头的信息即可,上文中谱子的时间分辨率为480。带入计算一下

image.png

两个四分休止符一个八分休止符2*480+480/2=1200,与输出信息的正好吻合。 这里说这么多其实是为了之后可能会实现的功能做铺垫(如果没更新,说明我淹没在生活里了

功能拓展

实现midi解析、移调

大家阅读代码就可以看出,其实都是midi-player-js库帮了大忙,但是网上很多mid的文件导入进来是不能用的,因为原琴只有21个键,所以其他调子的谱是需要移调的。

谱子获取与编写

获取:谱子的获取网上有许多mid文件,但是不是全部都能用。

编写:目前是没有找到有什么简谱的编辑软件(能导出mid的)。

我这里先后使用了两个软件(都是五线谱):

MuseScore 4

image.png

Guitar Pro 7

image.png

这里也很离谱,我先是用MuseScore打谱,发现导出的mid有问题,不能用。于是导出Guitar Pro 7再用Guitar Pro 7导出mid,能用,但是丢失了些信息,比如目前谱子中出现了莫名其妙的连弹

image.png

这里的第二个音应该不重复弹的,Guitar Pro 7一转被弹出来了。

为什么没解决嘞?因为我好不容易才浅显的看懂并会编辑MuseScore 4中的五线谱,他又唰的一下在Guitar Pro 7中又变成了我看不懂的方式。

游戏内实现自动播放

之后会将会用electron实现。