Coding Journal – Visualizing Bach with D3 JS

Bach

Johann Sebastian Bach was once censured by the town of Arnstadt for being unnecessarily involved in a sword fight with a student, after accusing said student of playing the bassoon like a goat.

While listening to recordings of The Art of Fugue, it occurred to me that the clear, crisp notes in Bach’s composition would show up nicely in an audio visualization.

The goal was to make an animated SVG using JavaScript w/ jQuery, the HTML5 Web Audio API, and D3.js…

By golly, I think I’ve done just that. With the help of a few tutorials and examples, I was able to slap together a cool little sunflower visualization. Many thanks to mbostock for the raindrop svg path shape and to Garry Smith for the awesome d3js visualization he tutorial wrote, which got me up to speed!

Enjoy the sights and sounds of Bach’s Contrapunctus XIII Inversus:

And, now for the code (feel free to copy/pasta to use however you’d like):

<audio id="bach-contrapunctus-13-inverted" src="path_to_audio" type="audio/mpeg"></audio>

<button class="bach-toggle" id="bach-toggle-play">Play</button>
<button class="bach-toggle" id="bach-toggle-pause">Pause</button>
<button class="bach-toggle" id="bach-toggle-restart">Restart</button>
<button class="bach-toggle" id="bach-toggle-volume-increase">Volume +</button>
<button class="bach-toggle" id="bach-toggle-volume-decrease">Volume -</button>
jQuery(document).ready(function ($) {

	var audio_elem = $('#bach-contrapunctus-13-inverted').get(0),
			frequency_data = new Uint8Array(359),
			audio_context = new (window.AudioContext || window.webkitAudioContext)(),
			audio_src = audio_context.createMediaElementSource(audio_elem),
			analyser = audio_context.createAnalyser(),
			height = 400,
			width = 700,
			svg_visual = createSVG('#bach-animation', height, width);

	audio_src.connect(analyser);
	audio_src.connect(audio_context.destination);

	renderFirstChart();
	renderChart(); // Run the loop

	// Bach Button events
	$('#bach-toggle-play').click(audioPlay);
	$('#bach-toggle-pause').click(audioPause);
	$('#bach-toggle-restart').click(audioRestart);
	$('#bach-toggle-volume-increase').click(audioVolumeIncrease);
	$('#bach-toggle-volume-decrease').click(audioVolumeDecrease);

	// Bach Audio Toggle Functions
	function audioPlay (e) {
		audio_elem.play();
	}

	function audioPause (e) {
		audio_elem.pause();
	}

	function audioRestart (e) {
		audio_elem.pause();
		audio_elem.currentTime = 0;
		audio_elem.play();
	}

	function audioVolumeIncrease (e) {
		audio_elem.volume += 0.2;
	}

	function audioVolumeDecrease (e) {
		audio_elem.volume -= 0.2;
	}

	function createSVG (container, height, width) {
		return d3.select(container)
			.append('svg')
			.attr('height', height)
			.attr('width', width)
			.append("g")
    	.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
	}

	function renderFirstChart () {
		svg_visual.selectAll("path")
	    .data(d3.range(359))
	  	.enter()
	  	.append("path")
	    .attr('fill', function(d) {
	    	return 'rgb(' + d + ', 85, 95)';
	    })
	    .attr("d", function(d) {
	    	return generateRaindrop(frequency_data[d]);
	    })
	    .attr("transform", function(d) {
	      return "rotate(" + d + ")"
	        + "translate(" + frequency_data[d]*1.3 + ",0)"
	        + "rotate(90)";
	    });
	}

	function renderChart () {
		requestAnimationFrame(renderChart);
		analyser.getByteFrequencyData(frequency_data);
		svg_visual.selectAll("path")
	    .data(d3.range(359))
	    .attr('fill', function(d) {
	    	return 'rgb(' + d + ', 85, 95)';
	    })
	    .attr("d", function(d) {
	    	return generateRaindrop(frequency_data[d]);
	    })
	    .attr("transform", function(d) {
	      return "rotate(" + d + ")"
	        + "translate(" + frequency_data[d]*1.3 + ",0)"
	        + "rotate(90)";
	    });
	}

	function generateRaindrop(size) {
	  var r = Math.sqrt(size*2 / Math.PI);
	  return "M" + r + ",0"
      + "A" + r + "," + r + " 0 1,1 " + -r + ",0"
      + "C" + -r + "," + -r + " 0," + -r + " 0," + -3*r
      + "C0," + -r + " " + r + "," + -r + " " + r + ",0"
      + "Z";
	}

});

Leave a Reply

Your email address will not be published. Required fields are marked *