// shim requestAnimFrame for animating playback window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); window.AudioContext = window.AudioContext || window.webkitAudioContext; // helper function for loading one or more sound files function loadSounds(obj, context, soundMap, callback) { var names = []; var paths = []; for (var name in soundMap) { var path = soundMap[name]; names.push(name); paths.push(path); } bufferLoader = new BufferLoader(context, paths, function(bufferList) { for (var i = 0; i < bufferList.length; i++) { var buffer = bufferList[i]; var name = names[i]; obj[name] = buffer; } if (callback) { callback(); } }); bufferLoader.load(); } // class that performs most of the work to load // a new sound file asynchronously // originally from: http://chimera.labs.oreilly.com/books/1234000001552/ch02.html function BufferLoader(context, urlList, callback) { this.context = context; this.urlList = urlList; this.onload = callback; this.bufferList = new Array(); this.loadCount = 0; } BufferLoader.prototype.loadBuffer = function(url, index) { // Load buffer asynchronously var request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "arraybuffer"; var loader = this; request.onload = function() { // Asynchronously decode the audio file data in request.response loader.context.decodeAudioData( request.response, function(buffer) { if (!buffer) { alert('error decoding file data: ' + url); return; } loader.bufferList[index] = buffer; if (++loader.loadCount == loader.urlList.length) loader.onload(loader.bufferList); }, function(error) { console.error('decodeAudioData error', error); } ); } request.onerror = function() { alert('BufferLoader: XHR error'); } request.send(); }; BufferLoader.prototype.load = function() { for (var i = 0; i < this.urlList.length; ++i) this.loadBuffer(this.urlList[i], i); }; // --- // Spectrogram class // constructor takes a filename, selector id to use to figure // out where to display, and a big options hash. // (not a great api - I know!) // sets up most of the configuration for the sound analysis // and then loads the sound using loadSounds. // Once finished loading, the setupVisual callback // is called. // --- function Spectrogram(filename, selector, options) { if (!options) { options = {}; } this.options = options; var SMOOTHING = 0.0; var FFT_SIZE = 2048; // this.sampleRate = 256; this.sampleRate = options.sampleSize || 512; this.decRange = [-80.0, 80.0]; this.width = options.width || 900; this.height = options.height || 440; this.margin = {top: 20, right: 20, bottom: 30, left: 50}; this.selector = selector; this.filename = filename; this.context = context = new AudioContext(); this.analyser = context.createAnalyser(); this.javascriptNode = context.createScriptProcessor(this.sampleRate, 1, 1); this.analyser.minDecibels = this.decRange[0]; this.analyser.maxDecibels = this.decRange[1]; this.analyser.smoothingTimeConstant = SMOOTHING; this.analyser.fftSize = FFT_SIZE; this.freqs = new Uint8Array(this.analyser.frequencyBinCount); this.data = []; this.isPlaying = false; this.isLoaded = false; this.startTime = 0; this.startOffset = 0; this.count = 0; this.curSec = 0; this.maxCount = 0; loadSounds(this, this.context, { buffer: this.filename }, this.setupVisual.bind(this)); } // --- // process // callback executed each onaudioprocess of the javascriptNode. // performs the work of analyzing the sound and storing the results // in a big array (not a great idea, but I haven't thought of something // better. // --- Spectrogram.prototype.process = function(e) { if(this.isPlaying && !this.isLoaded) { this.count += 1; this.curSec = (this.sampleRate * this.count) / this.buffer.sampleRate; this.analyser.getByteFrequencyData(this.freqs); var d = {'key':this.curSec, 'values':new Uint8Array(this.freqs)}; this.data.push(d); if(this.count >= this.maxCount) { this.switchButtonText(); this.togglePlayback(); this.draw(); this.isLoaded = true; console.log(this.data.length); console.log(this.data[0].values.length); } } } // --- // setupVisual // callback executed when the sound has been loaded. // sets up scales and other components needed to visualize. // --- Spectrogram.prototype.setupVisual = function() { console.log(this.context.sampleRate); // can configure these from the options this.timeRange = [0, this.buffer.duration]; var maxFrequency = this.options.maxFrequency || this.getBinFrequency(this.analyser.frequencyBinCount / 2); var minFrequency = this.options.minFrequency || this.getBinFrequency(0); this.freqRange = [minFrequency, maxFrequency]; this.svg = d3.select(this.selector).append("svg") .attr("width", this.width + this.margin.left + this.margin.right) .attr("height", this.height + this.margin.top + this.margin.bottom) .append("g") .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")"); this.canvas = d3.select(this.selector).append("canvas") .attr("class", "vis_canvas") .attr("width", this.width + this.margin.left) .attr("height", this.height + this.margin.top) .style("padding", d3.map(this.margin).values().join("px ") + "px"); this.progressLine = this.svg.append("line"); var that = this; var button_id = this.selector + "_button"; this.button = d3.select(this.selector).append("button") .style("margin-top", this.height + this.margin.top + this.margin.bottom + 20 + "px") .attr("id", button_id) .text("analyze") .on("click", function() { that.togglePlayback(); }); var freqs = []; for(i = 64; i < this.analyser.frequencyBinCount; i += 64) { freqs.push(d3.round(this.getBinFrequency(i), 4)); } this.freqSelect = d3.select(this.selector).append("select") .style("margin-top", this.height + this.margin.top + this.margin.bottom + 20 + "px") .style("margin-left", "20px") .on("change", function() { var newFreq = this.options[this.selectedIndex].value console.log(newFreq); that.yScale.domain([0, newFreq]); that.draw(); }); this.freqSelect.selectAll('option') .data(freqs).enter() .append("option") .attr("value", function(d) { return d;}) .attr("selected", function(d,i) { return (d == 11047) ? "selected" : null;}) .text(function(d) { return d3.round(d / 1000) + "k";}); this.maxCount = (this.context.sampleRate / this.sampleRate) * this.buffer.duration; this.xScale = d3.scale.linear() .domain(this.timeRange) .range([0, this.width]); this.yScale = d3.scale.linear() .domain(this.freqRange) .range([this.height,0]); this.zScale = d3.scale.linear() .domain(this.decRange) .range(["white", "black"]) .interpolate(d3.interpolateLab); var commasFormatter = d3.format(",.1f"); this.xAxis = d3.svg.axis() .scale(this.xScale) .orient("bottom") .tickSize(-this.height - 15) .tickPadding(10) .tickFormat(function(d) {return commasFormatter(d) + "s";}); this.yAxis = d3.svg.axis() .scale(this.yScale) .orient("left") .tickSize(-this.width - 10, 0, 0) .tickPadding(10) .tickFormat(function(d) {return d3.round(d / 1000, 0) + "k";}); this.svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + (this.height + 10) + ")") .call(this.xAxis); this.svg.append("g") .attr("class", "y axis") .attr("transform", "translate(" + (-10) + ",0)") .call(this.yAxis) } // --- // showProgress // --- Spectrogram.prototype.showProgress = function() { if(this.isPlaying && this.isLoaded) { this.curDuration = (this.context.currentTime - this.startTime); // this.count += 1; // this.curSec = (this.sampleRate * this.count) / this.buffer.sampleRate; var that = this; this.progressLine .attr("x1", function() {return that.xScale(that.curDuration);}) .attr("x2", function() {return that.xScale(that.curDuration);}) .attr("y1", 0) .attr("y2", this.height) .attr("stroke",'red') .attr("stroke-width", 2.0); requestAnimFrame(this.showProgress.bind(this)); if(this.curDuration >= this.buffer.duration) { this.progressLine.attr("y2", 0); this.togglePlayback() } } } // --- // Little helper function to change the text on the button // after the sound has been analyzed. // --- Spectrogram.prototype.switchButtonText = function() { this.button.text("play"); } // --- // Toggle playback // --- Spectrogram.prototype.togglePlayback = function() { if (this.isPlaying) { this.source.stop(0); this.startOffset += this.context.currentTime - this.startTime; console.log('paused at', this.startOffset); this.button.attr("disabled", null); } else { this.button.attr("disabled", true); this.startTime = this.context.currentTime; this.count = 0; this.curSec = 0; this.curDuration = 0; this.source = this.context.createBufferSource(); this.source.buffer = this.buffer; this.analyser.buffer = this.buffer; this.javascriptNode.onaudioprocess = this.process.bind(this); // Connect graph this.source.connect(this.analyser); this.analyser.connect(this.javascriptNode); this.source.connect(this.context.destination); this.javascriptNode.connect(this.context.destination); this.source.loop = false; this.source.start(0, this.startOffset % this.buffer.duration); console.log('started at', this.startOffset); if (this.isLoaded) { requestAnimFrame(this.showProgress.bind(this)); } } this.isPlaying = !this.isPlaying; } // --- // --- Spectrogram.prototype.draw = function() { var that = this; var min = d3.min(this.data, function(d) { return d3.min(d.values)}); var max = d3.max(this.data, function(d) { return d3.max(d.values)}); this.zScale.domain([min + 20, max - 20]); this.dotWidth = this.width / this.maxCount; this.dotHeight = this.height / this.analyser.frequencyBinCount; var visContext = d3.select(this.selector).select(".vis_canvas")[0][0].getContext('2d'); this.svg.select(".x.axis").call(this.xAxis); this.svg.select(".y.axis").call(this.yAxis); visContext.clearRect( 0, 0, this.width + this.margin.left, this.height ); // display as canvas here. this.data.forEach(function(d) { for(var i = 0; i < d.values.length - 1; i++) { var v = d.values[i]; var x = that.xScale(d.key); var y = that.yScale(that.getBinFrequency(i)); visContext.fillStyle = that.zScale(v); visContext.fillRect(x,y,that.dotWidth, that.dotHeight); } }); } // --- // --- Spectrogram.prototype.getFrequencyValue = function(freq) { var nyquist = this.context.sampleRate/2; var index = Math.round(freq/nyquist * this.freqs.length); return this.freqs[index]; } // --- // --- Spectrogram.prototype.getBinFrequency = function(index) { var nyquist = this.context.sampleRate/2; var freq = index / this.freqs.length * nyquist; return freq; }