1
Three.js Based Audio Spectrum Visualizer
Stefan Liemawan (2101694690), Asoka Wotulo (2101693611), Samuel Putra (2101693630)
BINUS University International
2
Background
Music visualization was popularized by the existence of windows media player. At that
time, it was considered a replacement for video. It was interactive and was default in every PC
out there. It generates a certain animated imagery or illustration based on the audio source
provided, in this case music. It usually is generated in real time, syncs, rendered and reacts with
the music as we’re hearing them live. There are various techniques that can be used to visualize
the music. It ranges from simple ones, to composite effects. Composite effects are audio
visualization effects that are combines various simple visualization algorithms to create a
visualization that is much more complex. Most of the time though, the music’s frequency (pitch)
and amplitude (loudness) are the two main determining factors. Since music is a subjective
matter, to us an effective music visualization is one that attains a high degree of correlation
between the music’s frequency and amplitude. The music needs to correlate and make sense to
the music.
In this computer graphics project, we were asked to create something that uses JavaScript
and WebGL to produce a visualization on browsers. We then decided to create a web-based
music visualization based on Three.js. Early on, we learned Babylon.js, we found that Babylon.js
even though it has a lot o easy sources to learn from, and is still actively developed, for our uses
Three.js was much more convenient. It is much more feature rich ranging from effects, scenes,
cameras, animations, lights, materials, shaders, objects, geometry, data loaders, utility, and much
more. To add to that, it is better documented than Babylon.js, but our pivoting reason was that it
could support most industry standard file formats, this proved useful as we needed to import
audio files and we wanted to minimalize the off chance of a troubleshoot happening.
3
Problems
There were some problems that we had encounter during the creation of this project. Our
aesthetic was black and white, we wanted to make a minimalist visualization with a higher
complexity to it. We didn’t think of adding various colors to aid the visualization. Another
problem is with a live microphone feed to the computer. At first, we wanted to be able to
visualize a live microphone feed to the computer, however we didn’t manage to get it to work.
Adding the camera shake proved to be tricky as well, but we managed to figure that out.
Related Work
To our best knowledge, there are Three.js based audio visualizer projects. One of them is
a project created by Janine, a student of Fullstack Academy on September 29, 2017. She created
the project using the JavaScript library, Three.js, as well as the Web Audio API. In this project
users can choose four songs that exemplify the different average frequencies and can also decide
how wide the visualization will appear on screen. The height of each cube is directly mapped to
an array of frequency data available at every moment, and the color changes based on the
average frequency. The following link, redirects to her project;
https://www.youtube.com/watch?v=wlvLEdYmK1o.
The difference between her projects and ours, is the way the music was visualized. It was
visualized as a wave spectrum comprised of smaller cubes. Our project visualizes the music
using arrayed circles.
4
Implementation
5
Code Snippet
<html>
<head>
<title>Audio</title>
<style>
body {
margin: 0;
color: black;
}
canvas {
width: 100%;
height: 100%;
}
#audio {
position: absolute;
bottom: 2%;
left: 50%;
transform: translateX(-50%);
width: 50%;
height: 5%;
outline: none;
}
#file {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
</style>
</head>
<body>
<input type="file" id="file" accept="audio/*" />
<audio id="audio" controls></audio>
<script src="/node_modules/three/build/three.min.js"></script>
<script
src="/node_modules/three/examples/js/controls/OrbitControls.js"></script>
<script>
6
var id = null;
var white = new THREE.Color('white');
var color = new THREE.Color('black');
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// create scene
var scene = new THREE.Scene();
scene.background = new THREE.Color(white);
var camera = new THREE.PerspectiveCamera(100, window.innerWidth /
window.innerHeight, 0.1, 2000);
camera.position.set(0, 0, 600);
camera.updateProjectionMatrix();
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.minDistance = 700;
controls.maxDistance = 900;
controls.update();
var file = document.getElementById("file");
var audio = document.getElementById("audio");
file.onchange = function () {
if (id !== null) cancelAnimationFrame(id);
var files = this.files;
audio.src = URL.createObjectURL(files[0]);
audio.load();
audio.play();
render();
}
var context = new AudioContext();
var src = context.createMediaElementSource(audio);
var analyser = context.createAnalyser();
src.connect(analyser);
analyser.connect(context.destination);
analyser.fftSize = 1024;
var bufferLength = analyser.frequencyBinCount;
var data = new Uint8Array(bufferLength);
//------------------------------------------------------GEOMETRY----------
----------------------------------------------
//group1 circle spectrum
var geometry = new THREE.TorusBufferGeometry(10, 0.03, 16, 100);
var material = new THREE.MeshBasicMaterial({ color: color });
var group = new THREE.Object3D();
scene.add(group);
7
for (var i = 0; i < data.length; i++) {
var mesh = new THREE.Mesh(geometry, material);
if (i % 2 == 0) mesh.position.set(0, 0, i);
else mesh.position.set(0, 0, -i);
group.add(mesh);
}
//group2 outer diamond
var geometry2 = new THREE.OctahedronGeometry(20, 0);
var material2 = new THREE.MeshBasicMaterial({ color: color });
var group2 = new THREE.Object3D();
scene.add(group2);
for (var i=0; i<4; i++) {
var mesh2 = new THREE.Mesh(geometry2, material2);
group2.add(mesh2);
}
group2.children[0].position.set(500, 0, 0);
group2.children[1].position.set(-500, 0, 0);
group2.children[2].position.set(0, 500, 0);
group2.children[3].position.set(0, -500, 0);
//group3 inner diamond
var geometry3 = new THREE.OctahedronGeometry(12, 0);
var material3 = new THREE.MeshBasicMaterial({ color: color });
var group3 = new THREE.Object3D();
scene.add(group3);
for (var i = 0; i < 4; i++) {
var mesh3 = new THREE.Mesh(geometry3, material3);
group3.add(mesh3);
}
group3.children[0].position.set(250, 0, 0);
group3.children[1].position.set(-250, 0, 0);
group3.children[2].position.set(0, 250, 0);
group3.children[3].position.set(0, -250, 0);
//group 4 line background
var group4 = new THREE.Object3D();
scene.add(group4);
var parameters = [[3.5, 0xaaaaaa, 0.25], [4.0, 0xaaaaaa, 0.5], [4.5,
0xaaaaaa, 0.75]];
var geometry4 = createGeometry();
for (var i = 0; i < parameters.length; ++i) {
var p = parameters[i];
var material4 = new THREE.LineBasicMaterial({ color: p[1], opacity:
p[2] });
8
var line = new THREE.LineSegments(geometry4, material4);
line.scale.x = line.scale.y = line.scale.z = p[0];
line.userData.originalScale = p[0];
line.rotation.y = Math.random() * Math.PI;
line.updateMatrix();
group4.add(line);
}
// line sphere geometry from
https://github.com/mrdoob/three.js/blob/master/examples/webgl_lines_sphere.html
function createGeometry() {
var geometry = new THREE.BufferGeometry();
var vertices = [];
var vertex = new THREE.Vector3();
for (var i = 0; i < 1500; i++) {
vertex.x = Math.random() * 2 - 1;
vertex.y = Math.random() * 2 - 1;
vertex.z = Math.random() * 2 - 1;
vertex.normalize();
vertex.multiplyScalar(200);
vertices.push(vertex.x, vertex.y, vertex.z);
vertex.multiplyScalar(Math.random() * 0.09 + 1);
vertices.push(vertex.x, vertex.y, vertex.z);
}
geometry.addAttribute('position', new
THREE.Float32BufferAttribute(vertices, 3));
return geometry;
}
controls.enabled = false;
renderer.render(scene, camera);
//------------------------------------------------------------------------
--------------------------------------------
function render() {
// get the frequency of the sound
controls.enabled = true;
analyser.getByteFrequencyData(data);
var avgdata = avg(data);
var lowerHalfArray = data.slice(0, (data.length / 2) - 1);
var upperHalfArray = data.slice((data.length / 2) - 1, data.length -
1);
var lowerAvg = avg(lowerHalfArray);
var upperAvg = avg(upperHalfArray);
9
// color = new THREE.Color(parseInt(lowerAvg) % 255,
parseInt(upperAvg) % 255, parseInt(avgdata) % 255);
// console.log(color);
for (var i = 0; i < data.length; i++) {
group.children[i].scale.x = (data[i] * 0.1) + 0.001;
group.children[i].scale.y = (data[i] * 0.1) + 0.001;
// group.children[i].material.color.setHex(Math.random() *
0xffffff );
}
var speed2 = upperAvg * 0.002;
group2.rotateZ(speed2);
var range2 = upperAvg * 6 + 500;
group2.children[0].position.set(range2, 0, 0);
group2.children[1].position.set(-range2, 0, 0);
group2.children[2].position.set(0, range2, 0);
group2.children[3].position.set(0, -range2, 0);
// for (var i = 0; i < 4; i++)
group2.children[i].material.color.setHex(Math.random() * 0xffffff );
var speed3 = lowerAvg * -0.0002;
group3.rotateZ(speed3);
var range3 = lowerAvg * 1.5 + 250;
group3.children[0].position.set(range3, 0, 0);
group3.children[1].position.set(-range3, 0, 0);
group3.children[2].position.set(0, range3, 0);
group3.children[3].position.set(0, -range3, 0);
// for (var i = 0; i < 4; i++)
group3.children[i].material.color.setHex(Math.random() * 0xffffff );
group4.rotateY(avgdata * 0.0004);
if (lowerAvg / upperAvg < avgdata) {
camera.strength = 15;
camera.damper = 3;
}
// camera shake from http://anpetersen.me/2015/01/16/for-the-sake-of-
screen-shake.html
if (camera.strength > 0) {
var value1 = Math.random() * 2 * camera.strength -
camera.strength;
var value2 = Math.random() * 2 * camera.strength -
camera.strength;
var value3 = Math.random() * 2 * camera.strength -
camera.strength;
camera.position.x += value1;
camera.position.y += value2;
10
camera.position.z += value3;
camera.strength -= camera.damper;
}
id = requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
}
function max(params) {
return Math.max.apply(Math, params);
}
function avg(params) {
var sum = 0;
for (var i = 0; i < params.length; i++) sum += params[i];
return sum / params.length;
}
</script>
</body>
</html>
11
Links & References
https://github.com/asokawotulo/CG-Final-Project/blob/master
https://threejs.org/docs/index.html#manual/en
https://threejs.org/docs/#api/en/audio/AudioAnalyser
https://www.youtube.com/watch?v=wlvLEdYmK1o