// Assumes context is an AudioContext defined outside of this class. Polymer('g-spectrogram', { // Show the controls UI. controls: false, // Log mode. log: false, // Show axis labels, and how many ticks. labels: false, ticks: 5, speed: 2, // FFT bin size, fftsize: 2048, oscillator: false, color: false, attachedCallback: function() { this.tempCanvas = document.createElement('canvas'), console.log('Created spectrogram'); // Get input from the microphone. if (navigator.mozGetUserMedia) { navigator.mozGetUserMedia({audio: true}, this.onStream.bind(this), this.onStreamError.bind(this)); } else if (navigator.webkitGetUserMedia) { navigator.webkitGetUserMedia({audio: true}, this.onStream.bind(this), this.onStreamError.bind(this)); } this.ctx = this.$.canvas.getContext('2d'); }, render: function() { //console.log('Render'); this.width = window.innerWidth; this.height = window.innerHeight; var didResize = false; // Ensure dimensions are accurate. if (this.$.canvas.width != this.width) { this.$.canvas.width = this.width; this.$.labels.width = this.width; didResize = true; } if (this.$.canvas.height != this.height) { this.$.canvas.height = this.height; this.$.labels.height = this.height; didResize = true; } //this.renderTimeDomain(); this.renderFreqDomain(); if (this.labels && didResize) { this.renderAxesLabels(); } requestAnimationFrame(this.render.bind(this)); var now = new Date(); if (this.lastRenderTime_) { this.instantaneousFPS = now - this.lastRenderTime_; } this.lastRenderTime_ = now; }, renderTimeDomain: function() { var times = new Uint8Array(this.analyser.frequencyBinCount); this.analyser.getByteTimeDomainData(times); for (var i = 0; i < times.length; i++) { var value = times[i]; var percent = value / 256; var barHeight = this.height * percent; var offset = this.height - barHeight - 1; var barWidth = this.width/times.length; this.ctx.fillStyle = 'black'; this.ctx.fillRect(i * barWidth, offset, 1, 1); } }, renderFreqDomain: function() { var freq = new Uint8Array(this.analyser.frequencyBinCount); this.analyser.getByteFrequencyData(freq); var ctx = this.ctx; // Copy the current canvas onto the temp canvas. this.tempCanvas.width = this.width; this.tempCanvas.height = this.height; //console.log(this.$.canvas.height, this.tempCanvas.height); var tempCtx = this.tempCanvas.getContext('2d'); tempCtx.drawImage(this.$.canvas, 0, 0, this.width, this.height); // Iterate over the frequencies. for (var i = 0; i < freq.length; i++) { var value; // Draw each pixel with the specific color. if (this.log) { logIndex = this.logScale(i, freq.length); value = freq[logIndex]; } else { value = freq[i]; } ctx.fillStyle = (this.color ? this.getFullColor(value) : this.getGrayColor(value)); var percent = i / freq.length; var y = Math.round(percent * this.height); // draw the line at the right side of the canvas ctx.fillRect(this.width - this.speed, this.height - y, this.speed, this.speed); } // Translate the canvas. ctx.translate(-this.speed, 0); // Draw the copied image. ctx.drawImage(this.tempCanvas, 0, 0, this.width, this.height, 0, 0, this.width, this.height); // Reset the transformation matrix. ctx.setTransform(1, 0, 0, 1, 0, 0); }, /** * Given an index and the total number of entries, return the * log-scaled value. */ logScale: function(index, total, opt_base) { var base = opt_base || 2; var logmax = this.logBase(total + 1, base); var exp = logmax * index / total; return Math.round(Math.pow(base, exp) - 1); }, logBase: function(val, base) { return Math.log(val) / Math.log(base); }, renderAxesLabels: function() { var canvas = this.$.labels; canvas.width = this.width; canvas.height = this.height; var ctx = canvas.getContext('2d'); var startFreq = 440; var nyquist = context.sampleRate/2; var endFreq = nyquist - startFreq; var step = (endFreq - startFreq) / this.ticks; var yLabelOffset = 5; // Render the vertical frequency axis. for (var i = 0; i <= this.ticks; i++) { var freq = startFreq + (step * i); // Get the y coordinate from the current label. var index = this.freqToIndex(freq); var percent = index / this.getFFTBinCount(); var y = (1-percent) * this.height; var x = this.width - 60; // Get the value for the current y coordinate. var label; if (this.log) { // Handle a logarithmic scale. var logIndex = this.logScale(index, this.getFFTBinCount()); // Never show 0 Hz. freq = Math.max(1, this.indexToFreq(logIndex)); } var label = this.formatFreq(freq); var units = this.formatUnits(freq); ctx.font = '16px Inconsolata'; // Draw the value. ctx.textAlign = 'right'; ctx.fillText(label, x, y + yLabelOffset); // Draw the units. ctx.textAlign = 'left'; ctx.fillText(units, x + 10, y + yLabelOffset); // Draw a tick mark. ctx.fillRect(x + 40, y, 30, 2); } }, clearAxesLabels: function() { var canvas = this.$.labels; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, this.width, this.height); }, formatFreq: function(freq) { return (freq >= 1000 ? (freq/1000).toFixed(1) : Math.round(freq)); }, formatUnits: function(freq) { return (freq >= 1000 ? 'KHz' : 'Hz'); }, indexToFreq: function(index) { var nyquist = context.sampleRate/2; return nyquist/this.getFFTBinCount() * index; }, freqToIndex: function(frequency) { var nyquist = context.sampleRate/2; return Math.round(frequency/nyquist * this.getFFTBinCount()); }, getFFTBinCount: function() { return this.fftsize / 2; }, onStream: function(stream) { var input = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); analyser.smoothingTimeConstant = 0; analyser.fftSize = this.fftsize; // Connect graph. input.connect(analyser); this.analyser = analyser; // Setup a timer to visualize some stuff. this.render(); }, onStreamError: function(e) { console.error(e); }, getGrayColor: function(value) { return 'rgb(V, V, V)'.replace(/V/g, 255 - value); }, getFullColor: function(value) { var fromH = 62; var toH = 0; var percent = value / 255; var delta = percent * (toH - fromH); var hue = fromH + delta; return 'hsl(H, 100%, 50%)'.replace(/H/g, hue); }, logChanged: function() { if (this.labels) { this.renderAxesLabels(); } }, ticksChanged: function() { if (this.labels) { this.renderAxesLabels(); } }, labelsChanged: function() { if (this.labels) { this.renderAxesLabels(); } else { this.clearAxesLabels(); } } });