I have been using NetEase Cloud Music to listen to songs, and I feel that its animation effect is quite good. Recently, I also want to try canvas drawing related things. After trying it a few times, I feel that the effect is pretty good, but there is still a gap between it and NetEase Cloud.
This issue is going to imitate the following effects:
The music of the recently popular Rakshasa Haishi is secretly used to demonstrate this effect.
The effect is shown as follows:
Effect display website
reference document
The specific process is generally to obtain the audio data, and then draw it on the canvas based on the audio data. Different drawing methods can have many amazing effects. After all, mathematics is the most fascinating.
Extract audio data
Ordinary audio tags cannot extract audio data. After checking, you need to use the built-in browser object AudioContext.
It is also relatively simple to use. Basically, it is to create, load audio, play and other environments. During playback, the audio data is obtained by linking the audio processing node.
Some of the more important points are as follows:
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext()
// 创建播放节点
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.bufferSourceNode.connect(this.audioCtx.destination)
// 创建分析器,从这个分析器中能够得到频域跟时域的数据,不过频域的数据分散不够均匀,感觉还是时域的展示效果好一些
this.analyser = this.audioCtx.createAnalyser()
this.analyser.fftSize = this.sampleRate
this.analyser.smoothingTimeConstant = 1
this.player.bufferSourceNode.connect(this.analyser)
this.analyser.getByteTimeDomainData(data)
All that's left is to organize the content.
class Player {
constructor(canvasName) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext()
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.canvas = document.getElementById(canvasName)
this.audioBuffer = null
this.initEffect()
}
stop() {
this.audioCtx.suspend()
}
resume() {
this.audioCtx.resume()
}
initEffect() {
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.bufferSourceNode.connect(this.audioCtx.destination)
// 显示效果,可以随时替换
this.effect = new DefaultEffect(this)
}
play(url) {
let that = this
if (that.audioBuffer) {
that.initEffect()
}
fetch(url, {
method: 'get',
responseType: 'arraybuffer'
}).then(res => {
return res.arrayBuffer();
}).then(arraybuffer => {
that.audioCtx.decodeAudioData(arraybuffer, function(buffer) {
that.audioBuffer = buffer
that.bufferSourceNode.buffer = buffer
that.bufferSourceNode.start(0)
});
})
}
}
360 reverb effect
Looking at the picture, it looks like lines radiating from the center of a circle, but with an inner circle covering the middle.
Then the next step is to draw a radial line first. You only need to draw it at a balanced angle and you're done.
This effect in this example divides the circle into 128 lines, and offsets the actual coordinates from the center of the circle to the edge of the inner circle.
The API for canvas line drawing is as follows:
let jiaodu = i * 360 / count
let sx = Math.sin(jiaodu * Math.PI / 180) * minRadius
let sy = Math.cos(jiaodu * Math.PI / 180) * minRadius
let d = Math.max(0, this.lastData[i] - 127)
// let d = data[i] - 90
let endRatio = (minRadius + (d / 128) * 100) / minRadius
let ex = Math.sin(jiaodu * Math.PI / 180) * minRadius * endRatio
let ey = Math.cos(jiaodu * Math.PI / 180) * minRadius * endRatio
sx += centerX
sy += centerY
ex += centerX
ey += centerY
this.ctx.beginPath();
this.ctx.moveTo(ex, ey); // 起点
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round"; // 圆角,看起来更好看一点
this.ctx.lineTo(lex, ley); // 终点
this.ctx.strokeStyle = 'rgba(255,0,0,0.1)'; // 颜色
this.ctx.stroke();
The effect is as follows:
To make this look better, I see that I can paint another layer on the outside of this line, but the color will look lighter.
In addition, after connecting the time domain data, it was found that the jitter was relatively large, so a layer of buffering was added to slow down the decline of the curve.
for(let i = 0; i < this.sampleRate; i++) {
if (this.lastData[i]> 8) this.lastData[i] -= 8
this.lastData[i] = Math.max(this.lastData[i], data[i])
}
The overall effect code is as follows:
class DefaultEffect {
constructor(player) {
this.player = player
this.width = player.canvas.width
this.height = player.canvas.height
this.ctx = player.canvas.getContext('2d')
this.sampleRate = 128
this.audioCtx = player.audioCtx
this.lastData = new Uint8Array(this.sampleRate)
this.analyser = this.audioCtx.createAnalyser()
this.analyser.fftSize = this.sampleRate
this.analyser.smoothingTimeConstant = 1
this.player.bufferSourceNode.connect(this.analyser)
}
// 当暂停的时候使用这个输出默认的效果,是动画不那么呆板
idleData(delta) {
let data = []
for(let i = 0; i < this.sampleRate; i++) {
data.push((Math.sin(i + delta/1000) + 1) * 20 + 127)
}
return data
}
getData(delta) {
var data = new Uint8Array(this.sampleRate)
if (this.audioCtx) {
if (this.audioCtx.state == 'running') {
this.analyser.getByteTimeDomainData(data)
} else {
data = this.idleData(delta)
}
} else {
data = this.idleData(delta)
}
return data
}
draw(delta) {
this.ctx.clearRect(0,0,this.width,this.height)
let data = this.getData(delta)
for(let i = 0; i < this.sampleRate; i++) {
if (this.lastData[i]> 8) this.lastData[i] -= 8
this.lastData[i] = Math.max(this.lastData[i], data[i])
}
let centerX = this.width/2
let centerY = this.height/2
let minRadius = 150
let count = 128
for(let i = 0; i < count; i++) {
let jiaodu = i * 360 / count
let sx = Math.sin(jiaodu * Math.PI / 180) * minRadius
let sy = Math.cos(jiaodu * Math.PI / 180) * minRadius
let d = Math.max(0, this.lastData[i] - 127)
// let d = data[i] - 90
let endRatio = (minRadius + (d / 128) * 100) / minRadius
let ex = Math.sin(jiaodu * Math.PI / 180) * minRadius * endRatio
let ey = Math.cos(jiaodu * Math.PI / 180) * minRadius * endRatio
sx += centerX
sy += centerY
ex += centerX
ey += centerY
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round";
this.ctx.lineTo(ex, ey);
this.ctx.strokeStyle = 'red';
this.ctx.stroke();
let lex = Math.sin(jiaodu * Math.PI / 180) * (minRadius * endRatio + 30)
let ley = Math.cos(jiaodu * Math.PI / 180) * (minRadius * endRatio + 30)
lex += centerX
ley += centerY
this.ctx.beginPath();
this.ctx.moveTo(ex, ey);
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round";
this.ctx.lineTo(lex, ley);
this.ctx.strokeStyle = 'rgba(255,0,0,0.1)';
this.ctx.stroke();
}
}
}
when using
Initialize a player, and then refresh the canvas regularly to display the effect.
var player;
var effectName;
onMounted( () => {
player = new Player('myCanvas', effectName)
setInterval(() => {
if (player) {
requestAnimationFrame(player.effect.draw.bind(player.effect))
}
}, 50);
player.play(url)
})