Imitation NetEase Cloud-360 degree reverb

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.
Please add image description

The effect is shown as follows:
Please add image description

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:
Please add image description

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)
})

Guess you like

Origin blog.csdn.net/xo19882011/article/details/133162901