├── LICENSE
├── README.md
├── index.html
├── script.js
└── style.css
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Santosh Arron
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Music Visualiser
2 |
3 | 
4 |
5 | ## Documentation
6 |
7 | In an attempt to learn THREE.js — the 3D rendering WebGL framework and WebAudio API, I made something that visualises the music in a very simple way. This article documents the whole process.
8 | Final thing first:
9 |
10 |
11 | (Just use a .mp3 / .mp4 / .wav file to see it work. If you are out, you can use this)
12 |
13 |
14 | ## [Live demo](https://santosharron.github.io/audio-visualizer-three-js/)
15 |
16 | ## License
17 |
18 | [MIT](https://choosealicense.com/licenses/mit/)
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Audio Visualizer based on Three.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Choose an audio file
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | var noise = new SimplexNoise();
2 | var vizInit = function (){
3 |
4 | var file = document.getElementById("thefile");
5 | var audio = document.getElementById("audio");
6 | var fileLabel = document.querySelector("label.file");
7 |
8 | document.onload = function(e){
9 | console.log(e);
10 | audio.play();
11 | play();
12 | }
13 | file.onchange = function(){
14 | fileLabel.classList.add('normal');
15 | audio.classList.add('active');
16 | var files = this.files;
17 |
18 | audio.src = URL.createObjectURL(files[0]);
19 | audio.load();
20 | audio.play();
21 | play();
22 | }
23 |
24 | function play() {
25 | var context = new AudioContext();
26 | var src = context.createMediaElementSource(audio);
27 | var analyser = context.createAnalyser();
28 | src.connect(analyser);
29 | analyser.connect(context.destination);
30 | analyser.fftSize = 512;
31 | var bufferLength = analyser.frequencyBinCount;
32 | var dataArray = new Uint8Array(bufferLength);
33 | var scene = new THREE.Scene();
34 | var group = new THREE.Group();
35 | var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
36 | camera.position.set(0,0,100);
37 | camera.lookAt(scene.position);
38 | scene.add(camera);
39 |
40 | var renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
41 | renderer.setSize(window.innerWidth, window.innerHeight);
42 |
43 | var planeGeometry = new THREE.PlaneGeometry(800, 800, 20, 20);
44 | var planeMaterial = new THREE.MeshLambertMaterial({
45 | color: 0x6904ce,
46 | side: THREE.DoubleSide,
47 | wireframe: true
48 | });
49 |
50 | var plane = new THREE.Mesh(planeGeometry, planeMaterial);
51 | plane.rotation.x = -0.5 * Math.PI;
52 | plane.position.set(0, 30, 0);
53 | group.add(plane);
54 |
55 | var plane2 = new THREE.Mesh(planeGeometry, planeMaterial);
56 | plane2.rotation.x = -0.5 * Math.PI;
57 | plane2.position.set(0, -30, 0);
58 | group.add(plane2);
59 |
60 | var icosahedronGeometry = new THREE.IcosahedronGeometry(10, 4);
61 | var lambertMaterial = new THREE.MeshLambertMaterial({
62 | color: 0xff00ee,
63 | wireframe: true
64 | });
65 |
66 | var ball = new THREE.Mesh(icosahedronGeometry, lambertMaterial);
67 | ball.position.set(0, 0, 0);
68 | group.add(ball);
69 |
70 | var ambientLight = new THREE.AmbientLight(0xaaaaaa);
71 | scene.add(ambientLight);
72 |
73 | var spotLight = new THREE.SpotLight(0xffffff);
74 | spotLight.intensity = 0.9;
75 | spotLight.position.set(-10, 40, 20);
76 | spotLight.lookAt(ball);
77 | spotLight.castShadow = true;
78 | scene.add(spotLight);
79 |
80 | scene.add(group);
81 |
82 | document.getElementById('out').appendChild(renderer.domElement);
83 |
84 | window.addEventListener('resize', onWindowResize, false);
85 |
86 | render();
87 |
88 | function render() {
89 | analyser.getByteFrequencyData(dataArray);
90 |
91 | var lowerHalfArray = dataArray.slice(0, (dataArray.length/2) - 1);
92 | var upperHalfArray = dataArray.slice((dataArray.length/2) - 1, dataArray.length - 1);
93 |
94 | var overallAvg = avg(dataArray);
95 | var lowerMax = max(lowerHalfArray);
96 | var lowerAvg = avg(lowerHalfArray);
97 | var upperMax = max(upperHalfArray);
98 | var upperAvg = avg(upperHalfArray);
99 |
100 | var lowerMaxFr = lowerMax / lowerHalfArray.length;
101 | var lowerAvgFr = lowerAvg / lowerHalfArray.length;
102 | var upperMaxFr = upperMax / upperHalfArray.length;
103 | var upperAvgFr = upperAvg / upperHalfArray.length;
104 |
105 | makeRoughGround(plane, modulate(upperAvgFr, 0, 1, 0.5, 4));
106 | makeRoughGround(plane2, modulate(lowerMaxFr, 0, 1, 0.5, 4));
107 |
108 | makeRoughBall(ball, modulate(Math.pow(lowerMaxFr, 0.8), 0, 1, 0, 8), modulate(upperAvgFr, 0, 1, 0, 4));
109 |
110 | group.rotation.y += 0.005;
111 | renderer.render(scene, camera);
112 | requestAnimationFrame(render);
113 | }
114 |
115 | function onWindowResize() {
116 | camera.aspect = window.innerWidth / window.innerHeight;
117 | camera.updateProjectionMatrix();
118 | renderer.setSize(window.innerWidth, window.innerHeight);
119 | }
120 |
121 | function makeRoughBall(mesh, bassFr, treFr) {
122 | mesh.geometry.vertices.forEach(function (vertex, i) {
123 | var offset = mesh.geometry.parameters.radius;
124 | var amp = 7;
125 | var time = window.performance.now();
126 | vertex.normalize();
127 | var rf = 0.00001;
128 | var distance = (offset + bassFr ) + noise.noise3D(vertex.x + time *rf*7, vertex.y + time*rf*8, vertex.z + time*rf*9) * amp * treFr;
129 | vertex.multiplyScalar(distance);
130 | });
131 | mesh.geometry.verticesNeedUpdate = true;
132 | mesh.geometry.normalsNeedUpdate = true;
133 | mesh.geometry.computeVertexNormals();
134 | mesh.geometry.computeFaceNormals();
135 | }
136 |
137 | function makeRoughGround(mesh, distortionFr) {
138 | mesh.geometry.vertices.forEach(function (vertex, i) {
139 | var amp = 2;
140 | var time = Date.now();
141 | var distance = (noise.noise2D(vertex.x + time * 0.0003, vertex.y + time * 0.0001) + 0) * distortionFr * amp;
142 | vertex.z = distance;
143 | });
144 | mesh.geometry.verticesNeedUpdate = true;
145 | mesh.geometry.normalsNeedUpdate = true;
146 | mesh.geometry.computeVertexNormals();
147 | mesh.geometry.computeFaceNormals();
148 | }
149 |
150 | audio.play();
151 | };
152 | }
153 |
154 | window.onload = vizInit();
155 |
156 | document.body.addEventListener('touchend', function(ev) { context.resume(); });
157 |
158 |
159 |
160 |
161 |
162 | function fractionate(val, minVal, maxVal) {
163 | return (val - minVal)/(maxVal - minVal);
164 | }
165 |
166 | function modulate(val, minVal, maxVal, outMin, outMax) {
167 | var fr = fractionate(val, minVal, maxVal);
168 | var delta = outMax - outMin;
169 | return outMin + (fr * delta);
170 | }
171 |
172 | function avg(arr){
173 | var total = arr.reduce(function(sum, b) { return sum + b; });
174 | return (total / arr.length);
175 | }
176 |
177 | function max(arr){
178 | return arr.reduce(function(a, b){ return Math.max(a, b); })
179 | }
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | :root{
2 | --bgColor : hsla(242, 86%, 6%, 1);
3 | --bgColorLight : hsla(242, 86%, 24%, 1);
4 | --textColor : hsla(242, 86%, 88%, 1);
5 | --textColorDark : hsla(242, 36%, 0%, 1);
6 | --paperColor: hsla(242, 86%, 44%, 1);
7 | --paperColorDark: hsla(242, 86%, 34%, 1);
8 | --shadowColorFaint: hsla(0, 0%, 0%, 0.2);
9 | }
10 |
11 | ::selected{
12 | color: var(--textColorDark);
13 | }
14 |
15 | html, body{
16 | margin: 0;
17 | padding: 0;
18 | overflow: hidden;
19 | height: 100vh;
20 | width: 100vw;
21 | background: var(--bgColor);
22 | background: linear-gradient(135deg, var(--bgColor), var(--bgColorLight));
23 | color: var(--textColor);
24 | font-family: 'Saira', sans-serif;
25 | position: relative;
26 | }
27 |
28 | *{
29 | box-sizing: border-box;
30 | transition: all 0.12s cubic-bezier(0.42, 0.54, 0.22, 1.26);
31 | }
32 |
33 | #canvas {
34 | position: fixed;
35 | left: 0;
36 | top: 0;
37 | width: 100%;
38 | height: 100%;
39 | }
40 |
41 | audio {
42 | position: fixed;
43 | left: 10px;
44 | bottom: -10px;
45 | width: calc(100% - 20px);
46 | }
47 |
48 | audio.active{
49 | bottom: 10px;
50 | }
51 |
52 | #thefile{
53 | width: 0.1px;
54 | height: 0.1px;
55 | opacity: 0;
56 | overflow: hidden;
57 | position: absolute;
58 | z-index: 1;
59 | }
60 |
61 | label.file{
62 | display: inline-block;
63 | position: absolute;
64 | left: 50%;
65 | top: 50%;
66 | transform: translate3d(-50%, -50%, 0);
67 | padding: 1rem 2rem;
68 | border-radius: 4px;
69 |
70 | background: var(--paperColor);
71 | color: var(--textColor);
72 | font-size: 1.25em;
73 | font-weight: 700;
74 | box-shadow: 0 20px 60px var(--shadowColorFaint);
75 |
76 | cursor: pointer;
77 | }
78 |
79 |
80 | label.file:hover{
81 | background: var(--paperColorDark);
82 | transform: translate3d(-50%, -55%, 0);
83 | }
84 |
85 | label.file:active{
86 | background: var(--paperColorDark);
87 | transform: translate3d(-50%, -45%, 0);
88 | }
89 |
90 | label.file.normal{
91 | transform: translate3d(10%, 50%, 0);
92 | padding: 0.2rem 2rem;
93 | font-size: 1rem;
94 | top: 0;
95 | left: 0;
96 | }
--------------------------------------------------------------------------------