Profound Sound is a music visualization app that allows users to watch, listen and dance along to visual representations of This Must Be The Place by The Talking Heads.
The app is powered by vanilla JavaScript DOM manipulation of an HTML canvas element, along with Web Audio API and D3 interaction which allow the translation of waves of sound into visual displays.
Access the live site here.
Note: Due to a cross-browser resource sharing error, Web Audio API may fail to load on certain browsers.
The project is implemented with the following technologies:
JavaScript ES6
for integrating various apis,Web Audio API
for extracting data relating to the waveform of the sound of an html5 audio elementD3
to display scalable vector graphics dependent on the data extracted fromWeb Audio API
HTML5
for formattingCSS3
for styling componentsWebpack4
to bundle js files
Some technical highlights of the app are:
- Recursive rendering of a binary tree
- Extraction of frequency data
- Dynamic re-rendering of a canvas sphere
- Dynamic re-rendering of an SVG chart by integrating D3 API
At the centerpiece of Profound Sound is a recursively rendered binary tree in Canvas, which begins with a single branch length and grows until the deepest branches (those with length < 10) are filled with leaves.
// from tree.js
export function drawTree(startX, startY, len, angle, branchWidth, treeContext, canvasEl,
branchColor, leafColor, shadowColor) {
treeContext.beginPath();
treeContext.save();
treeContext.lineWidth = branchWidth;
treeContext.strokeStyle = branchColor;
treeContext.fillStyle = leafColor;
treeContext.shadowBlur = 15;
treeContext.shadowColor = shadowColor;
treeContext.translate(startX, startY);
treeContext.rotate(angle * Math.PI/180);
treeContext.moveTo(0,0);
treeContext.lineTo(0,-len);
treeContext.stroke();
if (len < 10) {
treeContext.beginPath();
treeContext.arc(0,-len, 10, 0, Math.PI/2);
treeContext.fill();
treeContext.restore();
return;
}
drawTree(0, -len, (len*0.8) - 0.5, angle + 10, branchWidth * 0.8, treeContext, canvasEl);
drawTree(0, -len, (len*0.8) - 0.5, angle - 10, branchWidth * 0.8, treeContext, canvasEl);
treeContext.restore();
}
In order to extract data from its audio file, Profound Sound integrates with Web Audio API. In the following code snippet we create a media element source and an audio analyzer. We then proceed to connect the two and create an array of unsigned integers from the frequencyBinCount of the analyzer, which will later be used for visualizations.
// from main.js
let audio = document.getElementById("audioElement");
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let audioElement = document.getElementById('audioElement');
audioElement.crossOrigin = "anonymous";
let audioSrc = audioCtx.createMediaElementSource(audioElement);
let analyser = audioCtx.createAnalyser();
analyser.smoothingTimeConstant = 0.5;
analyser.fftSize = 256;
audioSrc.connect(analyser);
audioSrc.connect(audioCtx.destination);
let frequencyData = new Uint8Array(analyser.frequencyBinCount);
To render the dancing sun using canvas, we take the first element the array of unsigned integers from our frequencyData and pass that into the draw function as the parameter for the radius of our sun. Since we need to clear the canvas upon each re-render, we're forced to set the animation on a separate canvas element. We're also able to dynamically set the hue of the sun.
// from main.js
function drawSun(context, frequencyDataPoint) {
let transparency;
let hue = 40;
let x = ((canvas.width - 400)),
y = ((200));
if (false){
transparency = 0.01;
} else {
transparency = 0.08;
}
context.beginPath();
context.arc(x, y, (Math.abs(frequencyDataPoint-100)) * 2 , 0, 2 * Math.PI);
context.strokeStyle = 'hsla(' + hue + ', ' + 100 + '%,' + 40 + '%,' + transparency + ')';
context.fillStyle = "hsla(" + hue + ", 100%, 60%, .01)";
context.fill();
context.lineWidth = 2;
context.stroke();
}
function animateSun(ctx, canvasWidth, canvasHeight, analyser) {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
analyser.getByteTimeDomainData(frequencyData);
for (let i = 0; i < frequencyData.length; i += 1) {
let frequencyDataPoint = frequencyData[i];
drawSun(context, frequencyDataPoint);
}
requestAnimationFrame(animateSun.bind(this, ctx, canvasWidth, canvasHeight, analyser));
}
To render the dancing grass we pull a scalable vector graph from the D3 library and pass in our frequency data. We're able to constantly re-render the chart using request animation frame from Canvas. The color also adjusts dynamically based on the height of each rectangle in the chart.
// from main.js
let svgHeight= window.innerHeight;
let svgWidth= window.innerWidth + 1700;
let barPadding='15';
let svg = D3BAR.createSvg('.frequency-bar', svgHeight, svgWidth);
svg.selectAll('rect')
.data(frequencyData)
.enter()
.append('rect')
.attr('x', function(d,i) {
return i * (svgWidth / frequencyData.length);
})
.attr('width', svgWidth / frequencyData.length - barPadding);
function renderChart() {
requestAnimationFrame(renderChart);
analyser.getByteFrequencyData(frequencyData);
svg.selectAll('rect')
.data(frequencyData)
.attr('y', function(d) {
return svgHeight - d;
})
.attr('height', function(d) {
return d;
})
.attr('fill', function(d) {
return 'rgb(0, 200,' + d + ')';
});
}