我们看吉他谱时,经常看到拍号,例如6/8。它的含义是一拍是一个八分音符,一小节有六拍。四分音符的时长是一秒,即60拍/分钟。基于这样的背景知识,我们就可以根据一些定时循环的包来实现节拍器。
这边依然采用的ToneJs。我们需要认识几个类,Transport、Loop。
Transport是一个计时器类。它有两个属性值得关注:bpm和timeSignature。
bpm
表示每分钟的拍子数
timeSignature
表示拍号,用数组表示,例如6/8拍表达为[6, 8]。需要注意的是,这个属性最后会返回 6 / 8 * 4 = 3,默认值是4,即标准的4/4拍。
Loop是一个循环类,用于循环执行一个回调方法,我们可以在这个回调中进行语音播放,实现打节拍的效果。
需要注意的是,如果只是在每一拍都播放一次声音,我们是无法区分重音和弱音的,因此,应该写两个循环,一个专门播放重音的拍子,一个播放所有的拍子。
<template>
<div>
<div style="margin: 10px">
<v-text-field v-model="bpm" label="bpm"></v-text-field>
<v-select v-model="timeSignature" label="timeSignature" :items="timeSignatureList"></v-select>
<v-btn @click="start">{{ isPlaying ? '暂停' : '开始' }}</v-btn>
</div>
</div>
</template>
<script>
import { Oscillator, Transport, Loop } from 'tone';
export default {
name: 'Beat',
data() {
return {
bpm: 0,
timeSignature: '',
timeSignatureList: ['2/4', '3/4', '4/4', '3/8', '6/8'],
isPlaying: false,
}
},
mounted() {
this.bpm = 120;
this.timeSignature = '4/4';
},
watch: {
bpm(val) {
Transport.bpm.value = val;
},
},
beforeUnmount() {
this.stop();
},
methods: {
start() {
if (this.isPlaying) {
this.stop();
} else {
const osc1 = new Oscillator().toDestination();
const osc2 = new Oscillator().toDestination();
const res = this.timeSignature.split('/');
Transport.timeSignature = res.map(a => Number(a)); // [6, 8] 返回 6 / 8 * 4 表示 实际拍数和标准拍数的比例
// 创建一个每拍触发的事件
this.loopA = new Loop((time) => {
osc1.start(time).stop(time + 0.1);
}, res[1] + "n").start(0);
// 重音时间间隔:标准一拍的秒数 *(实际拍数 / 标准拍数) = 实际一拍的秒数
this.loopB = new Loop((time) => {
osc2.start(time).stop(time + 0.1);
}, 60 / this.bpm * Transport.timeSignature).start(0);
Transport.start();
}
this.isPlaying = !this.isPlaying;
},
stop() {
Transport?.stop();
this.loopA?.stop();
this.loopB?.stop();
}
}
}
</script>
这个功能已经集成到了我的个人网站YUERGS中,快来试试吧