├── 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 | ![audio-visualizer made by Santosh Arron](https://miro.medium.com/max/2000/1*CfnrtLr2KoTDs_r7afQ1gQ.png) 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 | 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 | } --------------------------------------------------------------------------------