仿网易云-360度混响

2023-09-22 11:22:55

一直在用网易云音乐听歌,感觉他的这个动效还是挺不错的,最近也是想试试canvas绘图相关的。尝试了几次之后感觉效果还不错,不过距离网易云的还是有些差距。

本期准备仿照制作如下效果:

偷偷使用最近比较流行的罗刹海市的音乐来展示这个效果。
请添加图片描述

效果展示如下:
请添加图片描述

效果展示网站
参考文档

具体的流程大体上就是获取音频数据,然后根据音频数据绘制在canvas上,不同的绘制方式就能有很多惊艳的效果,毕竟还是数学最令人着迷了。

提取音频数据

普通的audio标签无法提取音频数据,查了一下需要使用到 AudioContext 这个浏览器内置对象。
使用来说也比较简单,大体上就是创建,加载音频,播放等几个环境,在播放的时候通过链接音频处理节点获取音频数据。

几个比较重要的点如下:

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)

剩下的就是把这些内容给组织起来了。

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混响效果

看画面他像从圆心发射的线条,只不过用一个内圆把中间给盖上了。
那么接下来就是先画一个放射线,只需要按照均衡的角度来画一下就完事了。
本例子这个效果把园分了128条线,并且把其实坐标给从圆心偏移到内圆的边上。
canvas画线的API如下:

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();

效果如下:
请添加图片描述

为了让这个更好看一点,看到好像能够在这个线的外层再画一层,只不过颜色看起来比较浅。

另外接上时域数据之后,发现跳动比较大,然后加了一层缓存,减缓曲线的回落速度。

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

整体效果代码如下:

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

使用的时候

初始化一个player,然后定时刷新canvas以显示效果。

var player;
var effectName;
onMounted( () => {
  player = new Player('myCanvas', effectName)
  setInterval(() => {
    if (player) {
      requestAnimationFrame(player.effect.draw.bind(player.effect))
    }
  }, 50);
  player.play(url)
})
更多推荐

PostgreSQL serial类型

serial类型和序列postgresql序列号(SERIAL)类型包括smallserial(smallint,short),serial(int)bigserial(bigint,longlongint)不管是smallserial,serial还是bigserial,其范围都是(1,922337203685477

Python实现简单的爬虫功能

目录一、导入必要的库和模块二、发送HTTP请求三、解析HTML文件四、存储数据五、完整代码示例六、注意事项总结随着互联网的普及,信息获取的需求越来越大,而爬虫技术则成为了一种常见的信息获取方式。在Python中,有许多库和模块可以用于实现简单的爬虫功能。本文将介绍如何使用Python实现简单的爬虫功能,包括导入必要的库

std::thread简单使用

std::thread是C++标准库中用于多线程编程的类。它允许你创建和管理线程,使程序能够并发执行不同的任务。以下是关于std::thread的详细介绍以及几个示例说明:创建线程你可以使用std::thread构造函数创建新的线程,并将要执行的函数传递给它。以下是创建线程的基本示例:#include<iostream

c++ 学习 之 静态存储区域 和常量字符串的联系

什么是静态存储区域静态存储区域(StaticStorageArea)是计算机程序运行时用于存储全局变量、静态变量和字符串字面值等数据的一种特殊内存区域。静态存储区域具有以下特点:生存周期:静态存储区域中的数据在程序启动时分配,在程序结束时才会释放。这意味着这些数据在整个程序的执行期间都存在,不会随着函数的调用而创建或销

备战2024秋招面试题-查看Linux的进程

前言:\textcolor{Green}{前言:}前言:💞快秋招了,那么这个专栏就专门来记录一下,同时呢整理一下常见面试题💞部分题目来自自己的面试题,部分题目来自网络整理给我冲学习目标:面试题:算法题:完成?学习目标:Linux有哪些命令查看Linux的进程算法题:排序链表面试题:Linux有那些命令?文件和目录管

四川百幕晟科技:抖店精选联盟怎么使用?

近年来,电商平台的兴起让很多人纷纷加入进来,希望通过在网上销售产品来赚取更多的利润。在这个竞争激烈的市场中,如何找到稳定的渠道来推广自己的产品成为了每个卖家的追求。抖店精选联盟是一个不错的选择,可以帮助卖家快速提升销量。1.如何使用抖店精选联盟?1.注册成为联盟会员首先,您需要在抖店精选联盟官网注册。注册过程比较简单,

Azure Kubernetes Service中重写规则踩坑小记录

前言最近在做标准产品在不同云平台中的部署验证,有幸体验了一下微软的Azure。负责采购的运维部门这次采用了ApplicationGateway来搭配AKS(AzureKubernetesService)对外暴露服务,正好借着这个机会来体验一下ApplicationGateway。应用场景域名api.demo.com指向

探索科技地图:通向未来的科技之路

科技地图是一张连接现实与未来的路线图,它标示着创新的方向和科技的潜力。在这个信息爆炸的时代,我们深陷于新技术和新理念的海洋中,科技地图为我们提供了一颗指南针,帮助我们更好地了解和探索科技的前沿。科技地图的起源:科技的演化之路科技地图并非一夜之间出现,它承载着几十年科技发展的积淀。从最早的计算机革命,到移动互联网的崛起,

深入理解Linux网络笔记(一):内核是如何接收网络包的

本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动是Intel的igb网卡驱动Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source1、内核是如何接收网络包的1)、Linux网络收包总览在TCP/IP网络分层模型里,整个协议

vue3学习源码笔记(小白入门系列)------ 重点!响应式原理 代码逐行分析

目录备注响应式数据创建ref和reactive核心作用第一轮的依赖收集发生时机setup阶段去更改了响应式数据会发生依赖收集吗派发更新派发更新是什么时候触发的?扩展:setup阶段响应式数据被修改会触发组件更新吗vue是如何根据派发更新来触发组件的更新渲染的?组件副作用函数执行时有多个响应式数据更新是如何保证组件只会触

一个手机ip从这个城市去到另一个城市多久会变

随着现代社会的互联网和移动通信的普及,手机IP地址的变化成为了一个备受关注的话题。当我们从一个城市移动到另一个城市时,我们可能会好奇手机IP地址会在多长时间内发生变化。下面虎观代理小二二将详细介绍一下。手机IP地址会随着其所连接的网络的变化而发生改变。当您从一个城市移动到另一个城市,手机可能会重新连接到新的基站或使用不

热文推荐