chiro-canto/public/larynx/scripts/spectro.borismus.js
2021-04-15 12:29:53 +02:00

259 lines
7.7 KiB
JavaScript
Executable File

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