text: // 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();
      }
    }
  });
  