Skip to content

60. 音乐频谱可视化

Published:

前面学了用 Audio 来播放音乐,音频相关的还有一个 AudioAnalyser 可以用来分析频谱:

image.png

我们知道,声音是由振动产生的,这个振动不是单一频率的振动,而是不同频率的振动的复合。

任何一个声音都可以分析出一个频谱:

image.png

横轴是频率,纵轴是幅度。

任何一个声音都是不同频率的声音做不同幅度的振动的复合结果。

而用 AudioAnalyser 就可以分析出声音的频谱数据。

那我们就可以用这种频谱数据来实现音频可视化。

image.png

创建项目:

npx create-vite audio-analyser

image.png

进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';

const scene = new THREE.Scene();

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);

const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);

const width = window.innerWidth;
const height = window.innerHeight;

const helper = new THREE.AxesHelper(500);
scene.add(helper);

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(500, 600, 800);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

先跑下:

npm run dev

image.png

image.png

然后加入音频:

这个音频下载下来:

image.png

放到 public 目录下:

image.png

然后用 AudioLoader 加载之后,用 Audio 来播放:

image.png

const listener = new THREE.AudioListener();
const audio = new THREE.Audio( listener );

const loader = new THREE.AudioLoader();
loader.load('./superman.mp3', function ( buffer ) {
  audio.setBuffer( buffer );
});

document.body.addEventListener('click', () => {
  audio.pause();
  audio.play();
})

点击网页的时候播放音乐。

有的时候点击没反应,先暂停再播放就好了。

jaudio

然后加上 AudioAnalyser 来做分析。

image.png

const analyser = new THREE.AudioAnalyser(audio);

function render() {
    const data = analyser.getFrequencyData();
    console.log(data);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

创建 AudioAnalyser,传入要分析的音频。

在每帧渲染的时候用 getFrequencyData 拿到频谱数据打印下:

2025-04-14 23.04.01.gif

这个数组有 1024 个元素:

image.png

我们可以每 50 个算一个平均值

image.png

这样就是 21 组。

然后用立方体画出来就行。

分组用 lodash 的 chunk 方法,安装下:

npm install --save lodash-es

image.png

image.png

用 chunk 分组,然后 map 之后用 sum 求和。

import _ from 'lodash-es';
function calc() {
  const frequencyData = analyser.getFrequencyData();

  const sumArr = _.map(_.chunk(frequencyData, 50), (arr) => {
    return _.sum(arr);
  });

  return sumArr;
}

function render() {
    console.log(calc())
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

image.png

现在打印的就是 21 个元素的数组了。

接下来的事情就简单了,把它作为高度,画 21 个立方体就好了。

image.png

const group = new THREE.Group();
for(let i = 0; i < 21; i++) {
  const geometry = new THREE.BoxGeometry(100, 500, 100);
  const material = new THREE.MeshPhongMaterial({
    color: 'orange'
  });
  const mesh = new THREE.Mesh(geometry,material);
  mesh.position.y = 250;
  mesh.position.x = i * 150;
  group.add(mesh);
}
scene.add(group);

2025-04-14 23.37.11.gif

我们把这个 group 整体往 x 轴负方向移动一下,然后改下相机位置:

image.png

camera.position.set(0, 1000, 2000);
group.position.x = -1500;
group.position.y = -500;

去掉 AxesHelper,看下效果:

image.png

然后每帧计算拿到频谱数据的时候,改变一下立方体的高度:

image.png

for(let i = 0; i< group.children.length;i++) {
    const box = group.children[i];
    const height = sumArr[i] / 10;
    box.geometry.dispose();
    box.geometry = new THREE.BoxGeometry(100, height, 100);
    box.position.y = height / 2;
}

高度变了,position.y 也得变,这样才能底部和 x 轴平齐。

创建新 BoxGeometry 要把之前的那个的 cpu 资源释放掉,调用 geometry.dispose()

看下效果:

2025-04-15 00.12.58.gif

然后你还可以根据高度来设置不同的顶点颜色,实现高度的区分。

像我们之前做颜色渐变柱状图时那样:

image.png

image.png

const positions = box.geometry.attributes.position;
const colorsArr = [];
const color1 = new THREE.Color('blue');
const color2 = new THREE.Color('red');
for (let i = 0; i < positions.count; i++) {
    const percent = positions.getY(i) / 300;
    const c = color1.clone().lerp(color2, percent);
    colorsArr.push(c.r, c.g, c.b); 
}
const colors = new Float32Array(colorsArr);
box.geometry.attributes.color = new THREE.BufferAttribute(colors, 3);

这里到达 300 高度红色,否则蓝色,根据高度计算一个比例,然后用 color.lerp 计算颜色插值,设置到 geometry.attributes.color

启用顶点颜色需要设置 vertexColors 为 true

image.png

看下效果:

2025-04-15 00.30.06.gif

这样,音乐频谱的可视化就做好了。

这里顺便加一下性能分析的 Stats 工具:

image.png

const stats = new Stats();
document.body.appendChild( stats.domElement );

function render() {
    updateHeight();
    stats.update();
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

image.png

每秒 60 帧,很流畅。

案例代码上传了小册仓库

总结

这节我们实现了音乐频谱可视化的效果。

用 AudioAnalyser 拿到音频频谱数据,然后用 lodash 分组之后求和,最后得到一个 20 多个元素的数组,然后用 BoxGeometry 画立方体来可视化。

每帧修改 box 的高度和 position.y 就好了。

我们还通过自定义顶点颜色实现了根据高度来设置颜色的渐变色效果。

后面用到音频频谱的分析,就可以用 AudioAnalyser 来做。

评论