I use this library to do an fft on an audio file, after this I want to visualize the result with canvasjs, but I do not know how to do this.
I am not sure what should I use as x
and y
axes. If it is frequency and amplitude, how to do it? The max x
axis value should be equal to the max frequency, if yes than what is the step value? (I computed the magnitude, and the max frequency).
I would be grateful if anyone could help.
Edit:
I try to reproduce this, but i got the following result. The magnitude is not that bad, but the phase is horrible. I thought the Math.atan2()
will be the problem, because that is calculate from two numbers, so i tried with Math.js and with arrays, but got the same result. (Expected result in the link)
for (var i = 0; i < 10 - 1/50; i+=1/50) {
realArray.push(Math.sin(2 * Math.PI * 15 * i) + Math.sin(2 * Math.PI * 20 * i));
}
//Phase
counter = 0;
for (var i = 0; i < realArray.length ; i++) {
rnd.push({x: i, y: (Math.atan2(imag[counter], realArray[counter]) * (180 / Math.PI))});
counter++;
}
//Magnitude
counter = 0 ;
for (var i = 0; i < realArray.length ; i++) {
rnd1.push({x: i , y: Math.abs(realArray[counter])});
counter++;
}
I am totally lost please give me some adive.
Please find below an implementation of the visualizations shown at the Matlab page linked to in the original question.
I re-implemented part of the functionality of the graph drawing code from the Sprectrum analyzer in one of my earlier comments. I never got around to handling labels on the y-axis and scaling of the output, but that didn't really matter to me, since we're really talking about the visualisations and the underlying data used to create the them remains faithful to that computed by Matlab and Octave - note particularly the fact that I've had to normalize the data shown in the 2nd and 3rd graphs. I wrote the code originally as a means to visualize data during the various steps of performing a convolution of two audio signals with the aid of an FFT for speed. (I've included DFT code here instead for brevity)
Note also, that you're using floating point addition to determine the current time when you're generating the samples. This means that you will have accumalated an error close to 500 times by the time you're finished computing them, this is why you had to write
for (var i = 0; i < 10 - 1/50; i+=1/50)
rather than
for (var i = 0; i < 10; i+=1/50)
A better approach is to multiply the current step number by the interval between each step, as I've done in fillSampleBuffer
- this ensures you don't accumulate floating point error. If you examine the currentTime at each iteration of the loop, the difference becomes immediately apparent. ;)
var complex_t = function(real, imag)
{
this.real = real;
this.imag = imag;
return this;
}
// Discrete Fourier Transform
// much slower than an FFT, but also considerably shorter
// and less complex (no pun intended!) - result the same
// returns an array of complex values
function dft( complexArray )
{
var nSamples = complexArray.length;
var result = [];
for (var outIndex=0; outIndex<nSamples; outIndex++)
{
var sumReal=0, sumImag=0;
for (var inIndex=0; inIndex<nSamples; inIndex++)
{
var angle = 2 * Math.PI * inIndex * outIndex / nSamples;
var cosA = Math.cos(angle);
var sinA = Math.sin(angle);
//sumReal += complexArray[inIndex].real*Math.cos(angle) + complexArray[inIndex].imag*Math.sin(angle);
//sumImag += -complexArray[inIndex].real*Math.sin(angle) + complexArray[inIndex].imag*Math.cos(angle);
sumReal += complexArray[inIndex].real*cosA + complexArray[inIndex].imag*sinA;
sumImag += -complexArray[inIndex].real*sinA + complexArray[inIndex].imag*cosA;
}
result.push( new complex_t(sumReal, sumImag) );
}
return result;
}
function graphFormatData_t()
{
this.margins = {left:0,top:0,right:0,bottom:0};
this.graphTitle = '';
this.xAxisLabel = '';
this.yAxisLabel = '';
this.windowWidth = ''; //0.0107;
this.xAxisFirstTickLabel = '';
this.xAxisLastTickLabel = '';
return this;
}
/*
Code is incomplete. Amongst other short-comings, Y axis labels are not applied (note from 4th May 2017 - enhzflep )
*/
function drawGraph(canvasElem, data, normalize, formatData)
{
var can = canvasElem, ctx = can.getContext('2d');
let width=can.width, height=can.height;
ctx.strokeStyle = '#ecf6eb';
ctx.fillStyle = '#313f32';
ctx.fillRect(0,0,width,height);
var margins = {left:52, top:24, right:8, bottom:24}; // left, top, right, bottom
var drawWidth = width - (margins.left+margins.right);
var drawHeight = height - (margins.top+margins.bottom);
var lineWidth = ctx.lineWidth;
ctx.lineWidth = 0.5;
ctx.strokeRect( margins.left, margins.top, drawWidth, drawHeight);
ctx.lineWidth = lineWidth;
// draw/label axis
//
//
let numHorizDivs = 10;
let numVertDivs = 10;
{
var strokeStyle = ctx.strokeStyle;
ctx.strokeStyle = '#FFFFFF';
let y = height - margins.bottom;
var x = margins.left;
var dx = drawWidth / numHorizDivs;
ctx.beginPath();
for (var i=0; i<numHorizDivs+1; x+=dx,i++)
{
ctx.moveTo(x,y);
ctx.lineTo(x,y+4);
}
y = margins.top;
let dy = drawHeight / numVertDivs;
x = margins.left;
for (var i=0; i<numVertDivs+1; y+=dy,i++)
{
ctx.moveTo(x,y);
ctx.lineTo(x-4,y);
}
ctx.stroke();
ctx.strokeStyle = strokeStyle;
}
//
// draw the grid lines
//
{
var lineDash = ctx.getLineDash();
ctx.setLineDash([2, 2]);
x = margins.left + dx;
var y = margins.top;
var dx = drawWidth / numHorizDivs;
i = 0;
ctx.lineWidth = 0.5;
ctx.beginPath();
for (var i=0; i<numHorizDivs-1; x+=dx,i++)
{
ctx.moveTo(x,y);
ctx.lineTo(x,y+drawHeight);
}
let dy = drawHeight / numVertDivs;
y = margins.top+dy;
x = margins.left;
for (var i=0; i<numVertDivs-1; y+=dy,i++)
{
ctx.moveTo(x,y);
ctx.lineTo(x+drawWidth,y);
}
ctx.stroke();
ctx.setLineDash(lineDash);
}
//
// plot the actual data
//
{
var mMin=data[0], mMax=data[0], i, n;
if (normalize != 0)
for (i=0,n=data.length; i<n; i++)
{
if (data[i] < mMin) mMin = data[i];
if (data[i] > mMax) mMax = data[i];
}
else
{
/*
mMin = mMax = data[0];
data.forEach( function(elem){if (elem<mMin) mMin=elem; if (elem>mMax) mMax = elem;} );
var tmp = mMax;
if (Math.abs(mMin) > mMax)
tmp = Math.abs(mMin);
mMax = tmp;
mMin = -tmp;
*/
mMin = -2;
mMax = 2;
}
let strokeStyle = ctx.strokeStyle;
ctx.strokeStyle = '#ffffff';
ctx.moveTo(0,margins.top + drawHeight/2);
ctx.beginPath();
for (i=0,n=data.length; i<n; i++)
{
var x = (i*drawWidth) / (n-1);
var y = drawHeight * (data[i]-mMin) / (mMax-mMin);
ctx.lineTo(x+margins.left,height-margins.bottom-y);//y+margins.top);
// ctx.lineTo(x+margins.left,y+margins.top);
}
ctx.stroke();
ctx.strokeStyle = strokeStyle;
ctx.closePath();
}
if (formatData != undefined)
{
//label the graph
if (formatData.graphTitle != undefined)
{
ctx.font = '12px arial';
var titleText = formatData.graphTitle;
ctx.fillStyle = '#ffffff';
ctx.fillText(titleText, margins.left, (margins.top+12)/2);
}
// x-axis first tick label
if (formatData.xAxisFirstTickLabel != undefined)
{
ctx.font = '10px arial';
ctx.fillText(formatData.xAxisFirstTickLabel, margins.left, can.height-margins.bottom+10*1.5);
}
// x-axis label
if (formatData.xAxisLabel != undefined)
{
var xAxisText = formatData.xAxisLabel; //'1.1 msec/div';
ctx.font = '12px arial';
var axisTextWidth = ctx.measureText(xAxisText).width;
var drawWidth = can.width - margins.left - margins.right;
var axisPosX = (drawWidth - axisTextWidth) / 2;
ctx.fillText(xAxisText, margins.left+axisPosX, can.height-margins.bottom+10*1.5);
}
// x-axis last tick label
if (formatData.xAxisLastTickLabel != undefined)
{
var tickText = formatData.xAxisLastTickLabel;
ctx.font = '10px arial';
var textSize = ctx.measureText(tickText);
var posX = can.width - margins.right - textSize.width;
ctx.fillText(tickText, posX, can.height-margins.bottom+10*1.5);
}
}
else
{
// console.log("No format data present");
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function byId(id){return document.getElementById(id)}
window.addEventListener('load', onDocLoaded, false);
var samples = [];
var complexSamples = [];
function rad2deg(rad)
{
return rad * (180/Math.PI);
}
function onDocLoaded(evt)
{
// create and graph some samples
fillSampleBuffer();
var sampleGraphData = new graphFormatData_t();
sampleGraphData.graphTitle = 'Samples (50 per unit of time)';
sampleGraphData.xAxisFirstTickLabel = '0';
sampleGraphData.xAxisLastTickLabel = '10';
sampleGraphData.xAxisLabel = 'time';
drawGraph( byId('sampleVis'), samples, false, sampleGraphData);
// make a complex array from these samples - the real part are the samples' values
// the complex part is all 0
samples.forEach( function(sampleReal, index, srcArray){ complexSamples[index] = new complex_t(sampleReal, 0); } );
// do an fft on them
var fftSamples = dft( complexSamples );
// compute and graph the magnitude
var magnitude = [];
fftSamples.forEach(
function(complexValue, index)
{
magnitude[index] = Math.sqrt( (complexValue.real*complexValue.real) + (complexValue.imag*complexValue.imag) );
}
);
var magGraphData = new graphFormatData_t();
magGraphData.graphTitle = 'Magnitude (#samples - normalized)';
magGraphData.xAxisFirstTickLabel = '0';
magGraphData.xAxisLastTickLabel = '50';
magGraphData.xAxisLabel = 'freq';
drawGraph( byId('magnitudeVis'), magnitude, true, magGraphData);
// compute and graph the phase
var phase = [];
fftSamples.forEach(
function(complexValue, index)
{
phase[index] = rad2deg( Math.atan2(complexValue.imag, complexValue.real) );
}
);
var phaseGraphData = new graphFormatData_t();
phaseGraphData.graphTitle = 'Phase (-PI <--> PI)';
phaseGraphData.xAxisFirstTickLabel = '0';
phaseGraphData.xAxisLastTickLabel = '50';
phaseGraphData.xAxisLabel = 'freq';
drawGraph( byId('phaseVis'), phase, true, phaseGraphData);
}
function fillSampleBuffer()
{
var time = 0;
var deltaTime = 1 / 50.0;
var sampleNumber = 0;
for (sampleNumber=0; sampleNumber<500; sampleNumber++)
{
time = sampleNumber * deltaTime;
var curSample = Math.sin(2.0 * Math.PI * 15.0 * time) + Math.sin(2.0 * Math.PI * 20.0 * time);
samples.push(curSample);
}
}
canvas
{
border: solid 1px red;
}
<canvas id='sampleVis' width=430 height=340></canvas><br>
<canvas id='magnitudeVis' width=430 height=140></canvas><br>
<canvas id='phaseVis' width=430 height=140></canvas>