文章目录
- 背景
- 实现
- Transport
- Loop
- 代码
- 在线尝试
背景
我们看吉他谱时,经常看到拍号,例如6/8。它的含义是一拍是一个八分音符,一小节有六拍。四分音符的时长是一秒,即60拍/分钟。基于这样的背景知识,我们就可以根据一些定时循环的包来实现节拍器。
实现
这边依然采用的ToneJs。我们需要认识几个类,Transport、Loop。
Transport
Transport是一个计时器类。它有两个属性值得关注:bpm和timeSignature。
bpm
表示每分钟的拍子数
timeSignature
表示拍号,用数组表示,例如6/8拍表达为[6, 8]。需要注意的是,这个属性最后会返回 6 / 8 * 4 = 3,默认值是4,即标准的4/4拍。
Loop
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中,快来试试吧😉