问答1 问答5 问答50 问答500 问答1000
网友互助专业问答平台

用Three.js和AudioContext实现音乐频谱的3D可视化

提问网友 发布时间:45分钟前
声明:本网页内容为用户发布,旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。
E-MAIL:1656858193@qq.com
1个回答
热心网友 回答时间:2024-09-19 02:47

最近听了一首很好听的歌《一路生花》,于是就想用Three.js做个音乐频谱的可视化,最终效果是这样的:

代码地址在这里:https://github.com/QuarkGluonPlasma/threejs-exercize

这个效果的实现能学到两方面的内容:

AudioContext对音频解码和各种处理

Three.js的3d场景绘制

那还等什么,我们开始吧。

思路分析

要做音乐频谱可视化,首先要获取频谱数据,这个用AudioContext的api。

AudioContext的api可以对音频解码并对它做一系列处理,每一个处理步骤叫做一个Node。

我们这里需要解码之后用analyser来拿到频谱数据,然后传递给audioContext做播放。所以有三个处理节点:Source、Analyser、Destination

contextaudioCtx=newAudioContext();constsource=audioCtx.createBufferSource();constanalyser=audioCtx.createAnalyser();audioCtx.decodeAudioData(音频二进制数据,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});

先对音频解码,创建BufferSource的节点来保存解码后的数据,然后传入Analyser获取频谱数据,最后传递给Destination来播放。

调用source.start()开始传递音频数据,这样analyser就能够拿到音乐频谱的数据了,Destination也能正常的播放。

analyser拿到音频频谱数据的api是这样的:

constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);

每一次能拿到的frequencyData有1024个元素,可以按50个分为一份,算下平均值,这样只会有1024/50=21个频谱单元数据。

之后就可以用Three.js把这些频谱数据画出来了。

21个数值,可以绘制成21个立方体BoxGeometry,材质的话,用MeshPhongMaterial(因为这个反光的计算方式是一个姓冯的人提出来的,所以叫Phong),它的特点是表面可以反光,如果用MeshBasicMaterial,是不反光的。

之后加入花瓣雨效果,这个我们之前实现过,就是用Sprite(永远面向相机的一个平面)做贴图,然后一帧帧做位置的改变。

通过“漫天花雨”来入门Three.js

之后分别设置灯光、相机就可以了:

灯光我们用点光源PointLight,从一个位置去照射,配合Phong的材质可以做到反光的效果。

相机用透视相机PerspectiveCamera,它的特点是从一个点去看,会有近大远小的效果,比较有空间感。而正交相机OrthographicCamera因为是平行投影,就没有近大远小的效果,不管距离多远的物体都是一样大。

之后通过Renderer渲染出来,然后用requestAnimationFrame来一帧帧的刷新就可以了。

接下来我们具体写下代码:

代码实现

我们先通过fetch拿到服务器上的音频文件,转成ArrayBuffer。

ArrayBuffer是JS语言提供的用于存储二进制数据的api,和它类似的还有Blob和Buffer,区别如下:

ArrayBuffer是JS语言本身提供的用于存储二进制数据的通用API

Blob是浏览器提供的API,用于文件处理

Buffer是Node.js提供的API,用于IO操作

这里,我们毫无疑问要用ArrayBuffer来存储音频的二进制数据。

fetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){});

然后用AudioContext的api做解码和后续处理,分为Source、Analyser、Destination3个处理节点:

letaudioCtx=newAudioContext();letsource,analyser;functiongetData(){source=audioCtx.createBufferSource();analyser=audioCtx.createAnalyser();returnfetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){audioCtx.decodeAudioData(arrayBuffer,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});});};

获取音频,用AudioContext处理之后,并不能直接播放,因为浏览器做了*。必须得用户主动做了一些操作之后,才能播放音频。

为了绕过这个*,我们监听mousedown事件,用户点击之后,就可以播放了。

functiontriggerHandler(){getData().then(function(){source.start(0);//从0的位置开始播放create();//创建Three.js的各种物体render();//渲染});document.removeEventListener('mousedown',triggerHandler)}document.addEventListener('mousedown',triggerHandler);

之后可以创建3D场景中的各种物体:

创建立方体:

因为频谱为1024个数据,我们50个分为一组,就只需要渲染21个立方体:

constcubes=newTHREE.Group();constSTEP=50;constCUBE_NUM=Math.ceil(1024/STEP);for(leti=0;i<CUBE_NUM;i++){constgeometry=newTHREE.BoxGeometry(10,10,10);constmaterial=newTHREE.MeshPhongMaterial({color:'yellowgreen'});constcube=newTHREE.Mesh(geometry,material);cube.translateX((10+10)*i);cubes.add(cube);}cubes.translateX(-(10+10)*CUBE_NUM/2);scene.add(cubes);

立方体的物体Mesh,分别设置几何体是BoxGeometry,长宽高都是10,材质是MeshPhongMaterial,颜色是黄绿色。

每个立方体要做下x轴的位移,最后整体的分组再做下位移,移动整体宽度的一半,达到居中的目的。

频谱就可以通过这些立方体来做可视化。

之后是花瓣,用Sprite创建,因为Sprite是永远面向相机的平面。贴上随机的纹理贴图,设置随机的位置。

constFLOWER_NUM=400;/***花瓣分组*/constpetal=newTHREE.Group();varflowerTexture1=newTHREE.TextureLoader().load("img/flower1.png");varflowerTexture2=newTHREE.TextureLoader().load("img/flower2.png");varflowerTexture3=newTHREE.TextureLoader().load("img/flower3.png");varflowerTexture4=newTHREE.TextureLoader().load("img/flower4.png");varflowerTexture5=newTHREE.TextureLoader().load("img/flower5.png");varimageList=[flowerTexture1,flowerTexture2,flowerTexture3,flowerTexture4,flowerTexture5];for(leti=0;i<FLOWER_NUM;i++){varspriteMaterial=newTHREE.SpriteMaterial({map:imageList[Math.floor(Math.random()*imageList.length)],});varsprite=newTHREE.Sprite(spriteMaterial);petal.add(sprite);sprite.scale.set(40,50,1);sprite.position.set(2000*(Math.random()-0.5),500*Math.random(),2000*(Math.random()-0.5))}scene.add(petal);

分别把频谱的立方体和一堆花瓣加到场景中之后,就完成了物体的创建。

然后设置下相机,我们是使用透视相机,要分别指定视角的角度,最近和最远的距离,还有视区的宽高比。

constwidth=window.innerWidth;constheight=window.innerHeight;constcamera=newTHREE.PerspectiveCamera(45,width/height,0.1,1000);camera.position.set(0,300,400);camera.lookAt(scene.position);

之后设置下灯光,用点光源:

constpointLight=newTHREE.PointLight(0xffffff);pointLight.position.set(0,300,40);scene.add(pointLight);

然后就可以用renderer来做渲染了,结合requestAnimationFrame做一帧帧的渲染。

constrenderer=newTHREE.WebGLRenderer();functionrender(){renderer.render(scene,camera);requestAnimationFrame(render);}render();

在渲染的时候,每帧都要计算花瓣的位置,和频谱立方体的高度。

花瓣的位置就是不断下降,到了一定的高度就回到上面:

constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);0

频谱立方体的话,要用analyser获取最新频谱数据,计算每个分组的平均值,然后设置到立方体的scaleY上。

//获取频谱数据constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);//计算每个分组的平均频谱数据constaverageFrequencyData=[];for(leti=0;i<frequencyData.length;i+=STEP){letsum=0;for(letj=i;j<i+STEP;j++){sum+=frequencyData[j];}averageFrequencyData.push(sum/STEP);}//设置立方体的scaleYfor(leti=0;i<averageFrequencyData.length;i++){cubes.children[i].scale.y=Math.floor(averageFrequencyData[i]*0.4);}

还可以做下场景围绕X轴的渲染,每帧转一定的角度。

constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);2

最后,加入轨道控制器就可以了,它的作用是可以用鼠标来调整相机的位置,调整看到的东西的远近、角度等。

constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);3

最终效果就是这样的:花瓣纷飞,频谱立方体随音乐跳动。

完整代码提交到了github:

https://github.com/QuarkGluonPlasma/threejs-exercize

也在这里贴一份:

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>音乐频谱可视化</title><style>body{margin:0;overflow:hidden;}</style><scriptsrc="./js/three.js"></script><scriptsrc="./js/OrbitControls.js"></script></head><body><script>letaudioCtx=newAudioContext();letsource,analyser;functiongetData(){source=audioCtx.createBufferSource();analyser=audioCtx.createAnalyser();returnfetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){audioCtx.decodeAudioData(arrayBuffer,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});});};functiontriggerHandler(){getData().then(function(){source.start(0);create();render();});document.removeEventListener('mousedown',triggerHandler)}document.addEventListener('mousedown',triggerHandler);constSTEP=50;constCUBE_NUM=Math.ceil(1024/STEP);constFLOWER_NUM=400;constwidth=window.innerWidth;constheight=window.innerHeight;constscene=newTHREE.Scene();constcamera=newTHREE.PerspectiveCamera(45,width/height,0.1,1000);constrenderer=newTHREE.WebGLRenderer();/***花瓣分组*/constpetal=newTHREE.Group();/***频谱立方体*/constcubes=newTHREE.Group();functioncreate(){constpointLight=newTHREE.PointLight(0xffffff);pointLight.position.set(0,300,40);scene.add(pointLight);camera.position.set(0,300,400);camera.lookAt(scene.position);renderer.setSize(width,height);document.body.appendChild(renderer.domElement)renderer.render(scene,camera)for(leti=0;i<CUBE_NUM;i++){constgeometry=newTHREE.BoxGeometry(10,10,10);constmaterial=newTHREE.MeshPhongMaterial({color:'yellowgreen'});constcube=newTHREE.Mesh(geometry,material);cube.translateX((10+10)*i);cube.translateY(1);cubes.add(cube);}cubes.translateX(-(10+10)*CUBE_NUM/2);varflowerTexture1=newTHREE.TextureLoader().load("img/flower1.png");varflowerTexture2=newTHREE.TextureLoader().load("img/flower2.png");varflowerTexture3=newTHREE.TextureLoader().load("img/flower3.png");varflowerTexture4=newTHREE.TextureLoader().load("img/flower4.png");varflowerTexture5=newTHREE.TextureLoader().load("img/flower5.png");varimageList=[flowerTexture1,flowerTexture2,flowerTexture3,flowerTexture4,flowerTexture5];for(leti=0;i<FLOWER_NUM;i++){varspriteMaterial=newTHREE.SpriteMaterial({map:imageList[Math.floor(Math.random()*imageList.length)],});varsprite=newTHREE.Sprite(spriteMaterial);petal.add(sprite);sprite.scale.set(40,50,1);sprite.position.set(2000*(Math.random()-0.5),500*Math.random(),2000*(Math.random()-0.5))}scene.add(cubes);scene.add(petal);}functionrender(){petal.children.forEach(sprite=>{sprite.position.y-=5;sprite.position.x+=0.5;if(sprite.position.y<-height/2){sprite.position.y=height/2;}if(sprite.position.x>1000){sprite.position.x=-1000;}});constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);constaverageFrequencyData=[];for(leti=0;i<frequencyData.length;i+=STEP){letsum=0;for(letj=i;j<i+STEP;j++){sum+=frequencyData[j];}averageFrequencyData.push(sum/STEP);}for(leti=0;i<averageFrequencyData.length;i++){cubes.children[i].scale.y=Math.floor(averageFrequencyData[i]*0.4);}constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);2renderer.render(scene,camera);requestAnimationFrame(render);}constfrequencyData=newUint8Array(analyser.frequencyBi

本文如未解决您的问题请添加抖音号:51dongshi(抖音搜索懂视),直接咨询即可。

请问世界之窗浏览器如何查看三天前的浏览纪录 手机有免费下载完整版的音乐的地方吗? 轻松搭建PHPMySQL环境安装包详解phpmysql安装包 为什么我的抖音在其它省登陆后显示的还是之前的 为什么我的抖音ip去了另一个省还是一样的? 红枣枸杞子姜茶有哪些好处,爱美女性注意了 吃何种食物可以清理血管内的垃圾 化工废水处理的原则 app如何上架小米应用商店 防水性好的光动能手表有哪些推荐? 西抗18种植最合适的时间 西抗18什么时候种最合适 如何挑选优质的河蚌? 做地瓜面子凉粉用什么添加剂 充电手电钻如何拆开图 女人手硬好还是手软好 女人手软还是手硬命好 什么蜥蜴最温顺? 世界上最温顺的蜥蜴有哪些 蜥蜴如何上手 佳能打印机打印一半就提示没有墨水了是怎么回事 在地下室车库发现一个不明生物的尸体,求解这是什么。变异蟑螂?? 海参能加醋一起吃吗 海参可以加醋一起吃吗 海参与醋相克吗? nike sbNike SB 品牌介绍 买入价是什么意思 Keep your moral compass at all times什么意思? make a phone call at all times什么意思 土豆丝怎么做好吃的做法 怎么分河蚌和海蚌 海蚌和象拔蚌的肉质口感有什么不同? 皇帝的新装的寓意皇帝的新装表达了什么 WPSexcel筛选后怎么粘贴数据 dw手表用什么电池 买东西不能买四个吗 给公家买东西,买回去有人觉得东西不好,自己心里很别扭,本人承诺没占... 我姐一件东西选半天,女人买东西都这么慢? 在天猫上买东西,一共买了三样都是在一个包裹里,快递受到了,里面却少了... "生存还是死亡,这是一个问题。"出自莎士比亚的哪部作品? to be,or not to be.this a question 男生发给女生to+be+or+not+to+be是什么意思? 小米电视怎么把u盘的东西复制到电视上
Top